[Pythhon] 볼록기를 사용하지 않고 데이터클래스가 됩니다.

66766 단어 Pythontech

배경.


Python에 대한 데이터클래스입니다.
데이터클래스에 대한 설명은 다음과 같습니다.
https://zenn.dev/enven/articles/8b80ff38461b4ff329aa
제목만 나오면 이해하기 어려울 수도 있지만 ↓ 느낌으로 하고 싶어요.
추상류(또는 일반류)를 계승하여 자류dataclass를 만들 수 있습니까?이런 느낌이야.
class SubClass(SuperClass):
    foo: str = ''

>>> sub = SubClass()
>>> print(sub)
SubClass('foo'='')
>>> is_dataclass(sub)
True
원래 SubClass의 위@dataclass에서 부가 장식물을 통해 클래스를 데이터클래스로 정의했다.
여기.에도 이런 말이 쓰여 있다. 오리지널 장식물을 만들어 장식한 클라스를 데이터클라스로 만들었지만 __new__()부터 클래스 대상에 대해 속성setattr을 하면 클래스에 정의된 것이 아니기 때문에 VScode 등에서 코드 보충을 잘 하지 못했다.
그러면 아날로그 대신 추상 클래스를 정의하여 계승하는 형식으로 하위 클래스를 dataclass로 처리할 수 없습니까?이런 실험.

사례 1:new__ 꾸미다


from abc import ABCMeta
from dataclasses import dataclass, is_dataclass

class SuperDataclass(metaclass=ABCMeta):
    bar: str = ''
    def __new__(cls, *args, **kwargs):
        dataclass(cls)
        return super().__new__(cls)

class DataclassImpl(SuperDataclass):
    foo: str = ''
>>> dc_impl = DataclassImpl()
>>> print(dc_impl)
DataclassImpl(foo='')
>>> print(is_dataclass(dc_impl))
True
>>> print(DataclassImpl(foo='fu'))
DataclassImpl(foo='fu')
하위 클래스는 완전 데이터클래스입니다.__new__()에args와kwargs가 지정되지 않으면 DataclassImpl('')DataclassImpl(foo='')처럼 매개 변수에 필드를 지정하고 실례를 만들 때 오류가 발생할 수 있습니다. 주의하십시오.
물론 슈퍼클래스는 데이터클래스가 아니기 때문에 슈퍼클래스에 정의된 클래스 변수bar는 슈퍼클래스 데이터 클래스의 필드로 식별되지 않는다.__new__()의 매개 변수는 클래스 대상DataclassImpl에 전달되지만 한 과정에서?같은 클래스 대상을 전송했기 때문에 생성DataclassImpl 실례가 발생할 때마다 같은 클래스 대상dataclass(cls)에 대해 두 번째 호출이 잘못될 수 있습니다.
구체적으로 다음과 같이 하위 클래스의 클래스 변수field(default_factory=list) 등에서 정음형을 정의했을 때.
from abc import ABCMeta
from dataclasses import dataclass

class SuperDataclass(metaclass=ABCMeta):
    bar: str = ''
    def __new__(cls, *args, **kwargs):
        dataclass(cls)
        return super().__new__(cls)

class DataclassImpl(SuperDataclass):
    foo: str = ''
    lis: list = field(default_factory=list)
# 1回目のインスタンス生成 エラーにならない
>>> DataclassImpl()
DataclassImpl(foo='', lis=[])
# 2回目のインスタンス生成
>>> DataclassImpl()
Traceback (most recent call last):
  File "/workspace/myprj/dir/dataclass_test.py", line 151, in <module>
    print(DataclassImpl(foo='fu'))
  File "/workspace/myprj/dir/dataclass_test.py", line 84, in __new__
    dataclass(cls)
  File "/.pyenv/versions/3.9.1/lib/python3.9/dataclasses.py", line 1021, in dataclass
    return wrap(cls)
  File "/.pyenv/versions/3.9.1/lib/python3.9/dataclasses.py", line 1013, in wrap
    return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
  File "/.pyenv/versions/3.9.1/lib/python3.9/dataclasses.py", line 927, in _process_class
    _init_fn(flds,
  File "/.pyenv/versions/3.9.1/lib/python3.9/dataclasses.py", line 504, in _init_fn
    raise TypeError(f'non-default argument {f.name!r} '
TypeError: non-default argument 'lis' follows default argument
왜 이렇게 됐는지 이유주의점에 적혀 있다.

상황2: 추상 클래스를 데이터클래스로 변경


from abc import ABCMeta
from dataclasses import dataclass

@dataclass
class SuperDataclass2(metaclass=ABCMeta):
    bar: str = ''

class DataclassImpl2(SuperDataclass2):
    foo: str = ''
>>> dc_impl = DataclassImpl2()
>>> print(dc_impl)
DataclassImpl2(bar='')
>>> print(is_dataclass(dc_impl))
True
>>> print(DataclassImpl2(bar='ba'))
DataclassImpl2(bar='ba')
는 추상 클래스만 dataclass로 정의되기 때문에 데이터 클래스 필드는 추상 클래스 정의bar로만 식별된다.
그럼에도 불구하고 SubClass는 dataclass로 정확하게 식별되었고 __init__에서도 필드에 수치가 있다.

사례 3: 기술 통합


from abc import ABCMeta
from dataclasses import dataclass, is_dataclass

@dataclass
class SuperDataclass3(metaclass=ABCMeta):
    bar: str = ''

    def __new__(cls, *args, **kwargs):
        dataclass(cls)
        return super().__new__(cls)

class DataclassImpl3(SuperDataclass3):
    foo: str = ''
>>> dc_impl = DataclassImpl3()
>>> print(dc_impl)
DataclassImpl3(bar='', foo='')
>>> print(is_dataclass(dc_impl))
True
>>> print(DataclassImpl3(bar='ba', foo='fu'))
DataclassImpl3(bar='ba', foo='fu')
사례 1+2 두 가지 수법.
하위 클래스 DataclassImpl3 에서foo와bar 두 필드를 정의했습니다.
그러나 출력의 순서는 추상류의 정의→자류의 정의 순서이다.
하위 클래스__init__()에서foo와bar 두 필드가 모두 매개 변수로 정의되었음을 확인할 수 있습니다.

왜 이렇게 됐을까


Python의 대상은 dict형의 특수 속성__dict__을 가지고 있으며, 클래스 변수나 특수 방법의 방법 정보 등 대상의 쓰기 가능한 속성을 포함한다.dataclass 함수는 첫 번째 매개 변수에 지정된 클래스 대상__dict__을 참조하여 유형 변형이 있는 클래스 변수를 데이터 클래스 필드__dataclass_fields__로 정의하고 특수 속성으로 추가합니다.
상황2의 경우 추상류dataclass만 녹았기 때문에 하위류의 류 변수를 식별할 수 없다.
상자 1의 상황을 봅시다.하위 클래스의 정보를 출력하기 위해 print를 추가했습니다.
class SuperDataclass(metaclass=ABCMeta):
    bar: str = ''

    def __new__(cls, *args, **kwargs):
        print('## print(cls)')
        print(cls)
        print('## print(dir(cls))')
        print(dir(cls))
        print(cls.__dict__)
        print('print(cls.__dict__)')
        dataclass(cls)
        return super().__new__(cls)

class DataclassImpl(SuperDataclass):
    foo: str = ''
>>> DataclassImpl()
## print(cls)
<class '__main__.DataclassImpl'>
## print(dir(cls))
['__abstractmethods__', '__annotations__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_abc_impl', 'bar', 'foo']
## print(cls.__dict__)
{'__module__': '__main__', '__annotations__': {'foo': <class 'str'>}, 'foo': '', '__doc__': None, '__abstractmethods__': frozenset(), '_abc_impl': <_abc._abc_data object at 0x7f00d1ffcd40>}
DataclassImpl(foo='')
print(cls)에서 클래스의 대상이 자류임을 알 수 있다.dir(cls)에서 DataclassImpl의 특수 속성, 특수 방법, 유형 변수 등 일람표를 얻을 수 있다.여기 출력barfoo 두 종류입니다.
이어서 출력cls.__dict__의 내용을 출력하는데 여기에는foo만 포함됩니다.
위에서 설명한 바와 같이 __dict__에는'고칠 수 있는'속성 정보만 포함되어 있고, Python에서 슈퍼 클래스에서 계승된 클래스 변수는'고칠 수 없는 속성'이다.
위에서 말한 바와 같이dataclass__dict__의 내용에 따라 데이터 클래스 필드를 결정하기 때문에 이런 상황에서 foo만 추가했다.
상자다음 코드를 실행해 보십시오.
@dataclass
class SuperClass(metaclass=ABCMeta):
    bar: str = ''

    def __new__(cls, *args, **kwargs):
        print('## before dataclass')
        print(cls)
        print(cls.__dict__)
        print(cls.__dataclass_fields__)
        dataclass(cls)
        print('## after dataclass')
        print(cls.__dict__)
        print('\n')
        print(cls.__dataclass_fields__)
        return super().__new__(cls)

class SubClass(SuperClass):

    foo: str = ''

if __name__ == '__main__':
    sub = SubClass()
    print(sub)
약간 긴 출력은 다음과 같다.
## before dataclass
<class '__main__.SubClass'>
{'__module__': '__main__', '__annotations__': {'foo': <class 'str'>}, 'foo': '', '__doc__': None, '__abstractmethods__': frozenset(), '_abc_impl': <_abc._abc_data object at 0x7f5e4e1bcc40>}
{'bar': Field(name='bar',type=<class 'str'>,default='',default_factory=<dataclasses._MISSING_TYPE object at 0x7f5e4e347790>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD)}
## after dataclass
{'__module__': '__main__', '__annotations__': {'foo': <class 'str'>}, 'foo': '', '__doc__': 'SubClass(*args, **kwargs)', '__abstractmethods__': frozenset(), '_abc_impl': <_abc._abc_data object at 0x7f5e4e1bcc40>, '__dataclass_params__': _DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False), '__dataclass_fields__': {'bar': Field(name='bar',type=<class 'str'>,default='',default_factory=<dataclasses._MISSING_TYPE object at 0x7f5e4e347790>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), 'foo': Field(name='foo',type=<class 'str'>,default='',default_factory=<dataclasses._MISSING_TYPE object at 0x7f5e4e347790>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD)}, '__init__': <function __create_fn__.<locals>.__init__ at 0x7f5e4e12f040>, '__repr__': <function __create_fn__.<locals>.__repr__ at 0x7f5e4e127ee0>, '__eq__': <function __create_fn__.<locals>.__eq__ at 0x7f5e4e12f1f0>, '__hash__': None}


{'bar': Field(name='bar',type=<class 'str'>,default='',default_factory=<dataclasses._MISSING_TYPE object at 0x7f5e4e347790>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), 'foo': Field(name='foo',type=<class 'str'>,default='',default_factory=<dataclasses._MISSING_TYPE object at 0x7f5e4e347790>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD)}
SubClass(bar='', foo='')
__new__()에서 클래스 대상에 대한 집행dataclass 이전cls.__dataclass_fields__에서 추상적인 클래스의 클래스 변수bar가 데이터 클래스로 등록된 필드를 볼 수 있다.SubClass 실례를 생성하기 전SuperClass에서 지정한 dataclass 지시가 실행되었습니다.
다만 cls.__dict__의 내용을 보면 하위 클래스에 정의된 클래스 변수foo만 있다.
이후 시행dataclass(cls)한 뒤 cls.__dataclass_fields__하면 barfoo 모두 포함된다.cls.__dict__에 포함되지 않았고bar 데이터 클래스로 식별된 필드.
데이터 보조의 원본 코드에서 보듯이 참조dataclass화된 클래스의 기초류 목록cls.__mrro__은 기류가 __dataclass_fields__ 속성을 가지고 있을 때 기류__dataclass_fields__dataclass화된 클래스의 필드에 추가한다.
따라서 추상 클래스의 데이터 클래스 필드도 하위 클래스에 추가되었다.

주의점

dataclass가 실행되면 이 처리에서 목표 클래스에 정의된 클래스 변수의 값을 기본값으로 바꿉니다.
아래 코드는
@dataclass
class Foo:
    foo: str = field(default='fu')
↓ 이렇게 전환.
class Foo:
    foo: str = 'fu'
이때 특정 클래스 변수, 예를 들어defaultfactory가 지정된 필드에 대해 클래스 변수는 클래스에서 삭제됩니다.
해당하는 소스 코드는 다음과 같습니다.
# If the class attribute (which is the default value for this
# field) exists and is of type 'Field', replace it with the
# real default.  This is so that normal class introspection
# sees a real default value, not a Field.
if isinstance(getattr(cls, f.name, None), Field):
    if f.default is MISSING:
        # If there's no default, delete the class attribute.
        # This happens if we specify field(repr=False), for
        # example (that is, we specified a field object, but
        # no default value).  Also if we're using a default
        # factory.  The class attribute should not be set at
        # all in the post-processed class.
        delattr(cls, f.name)
field에서 default가 MISSING인 경우if문장의 분기field는default 또는default이다factory 중 하나를 지정해야 하기 때문에default는 MISSING이고default입니다factory가 지정되었습니다.
따라서 delattr(cls, f.name)default지정한 클래스 변수가 삭제됩니다.
이후 두 번째 실행 시 아래 처리된 곳에 왔을 때
def _get_field(cls, a_name, a_type):
    # Return a Field object for this field name and type.  ClassVars
    # and InitVars are also returned, but marked as such (see
    # f._field_type).

    # If the default value isn't derived from Field, then it's only a
    # normal default value.  Convert it to a Field().
    default = getattr(cls, a_name, MISSING)
    if isinstance(default, Field):
        f = default
    else:
        if isinstance(default, types.MemberDescriptorType):
            # This is a field in __slots__, so it has no default value.
            default = MISSING
        f = field(default=default)
delattr(cls, f.name)에서 이 클래스 변수를 삭제했기 때문에default = getattr(cls, a_name, MISSING) 처리 중clsa_name 클래스 변수가 존재하지 않았기 때문에default =MISSING로 나타났다.
이후 도착f = field(default=default)했고default=MISSING이기 때문에field(default=MISSING)에 클래스 변수의 Field를 설정했습니다.default도 defult입니다.factory의 초기값도 MISSING이기 때문에default,defaultfacotry는 모두 MISSING입니다.
그리고 설정 함수__init__의 처리에서default와defult-facotry 쌍방이 모두 MISSING인 상황에서 Typerror를 보내는 처리가 있습니다.
seen_default = False
    for f in fields:
        # Only consider fields in the __init__ call.
        if f.init:
            if not (f.default is MISSING and f.default_factory is MISSING):
                seen_default = True
            elif seen_default:
                # ここでエラーになる。
                raise TypeError(f'non-default argument {f.name!r} '
                                'follows default argument')
이렇게 클래스 변수에 대한defaultfacotry를 지정한field는 이 클래스를 2회dataclass 실행하면 두 번째 실행할 때TyperError가 됩니다.

회피책

if not is_dataclass(cls)처럼 cls가 데이터클래스가 되었는지 검사를 통해 두 번째 이후의 호출에서 오류가 발생하지 않도록 할 수 있습니다.
from abc import ABCMeta
from dataclasses import dataclass, is_dataclass

class SuperDataclass(metaclass=ABCMeta):
    bar: str = ''
    def __new__(cls, *args, **kwargs):
        if not is_dataclass(cls):
            # サブクラスのオブジェクトがデータクラスでなければ
            # dataclassを呼び出す
            dataclass(cls)
        return super().__new__(cls)
단, 계승된 클래스가 데이터 클래스라면 첫 번째 처리에서 __new__를 뛰어넘은 클래스 대상cls는 데이터 클래스이기 때문에 하위 클래스가 정의한 클래스 변수는 데이터 클래스의 필드에 설정되지 않습니다.
@dataclass
class SuperDataclass(metaclass=ABCMeta):
    bar: str = ''

    def __new__(cls, *args, **kwargs):
        # この時点でclsはデータクラスになってい、
        # フィールドには"bar"のみ定義されている
        # if not is_dataclass(cls):としてしまうと、
        # DataclassImplの"foo"がフィールドに設定されない
        dataclass(cls)
        return super().__new__(cls)

class DataclassImpl(SuperDataclass):
    foo: str = ''
따라서 다른 회피 전략이 필요하다.예를 들어, "클래스 객체cls에 DataclassImpl에 의해 정의된 클래스 변수가 있는지 여부"를 예로 들 수 있습니다.
@dataclass
class SuperDataclass(metaclass=ABCMeta):
    bar: str = ''

    def __new__(cls, *args, **kwargs):
        if hasattr(cls, '__dataclass_fields__'):
            fields_ = cls.__dataclass_fields__  # barの情報のみ
            annotations_ = cls.__dict__.get('__annotations__', {})  # fooの型情報
            if all(f not in fields_.keys() for f in annotations_.keys()):
                dataclass(cls)
        return super().__new__(cls)

class DataclassImpl(SuperDataclass):
    foo: str = ''
귀찮아!

총결산


그나저나 추상류에서 제외__dataclass_fields__한 부분을 일반류로 바꾼 경우도 여러 경우의 결과는 마찬가지였다.
추상 클래스의 클래스 변수를 하위 클래스의 데이터 클래스 필드로 지정하려면 추상 클래스와 하위 클래스를 모두 metaclass=ABCMeta로 지정해야 한다.
경우에 따라 길고 짧음이 있기 때문에 자신에게 맞는 방법으로 실시하는 것이 어떨까.

좋은 웹페이지 즐겨찾기