[Pythhon] 볼록기를 사용하지 않고 데이터클래스가 됩니다.
배경.
Python에 대한 데이터클래스입니다.
데이터클래스에 대한 설명은 다음과 같습니다.
제목만 나오면 이해하기 어려울 수도 있지만 ↓ 느낌으로 하고 싶어요.
추상류(또는 일반류)를 계승하여 자류
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
의 특수 속성, 특수 방법, 유형 변수 등 일람표를 얻을 수 있다.여기 출력bar
과foo
두 종류입니다.이어서 출력
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__
하면 bar
와foo
모두 포함된다.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)
처리 중cls
에 a_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
로 지정해야 한다.경우에 따라 길고 짧음이 있기 때문에 자신에게 맞는 방법으로 실시하는 것이 어떨까.
Reference
이 문제에 관하여([Pythhon] 볼록기를 사용하지 않고 데이터클래스가 됩니다.), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/enven/articles/9902e768f34472bd9214텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)