由于dataclasses
在较新的Python版本中向@dataclass(...)
添加了新的参数,例如Python 3.10中的kw_only
,因此使用装饰器来包装@dataclass
装饰器可能不是一个理想的选择。
另一种选择是在Python 3中使用更新的描述符方法。虽然下面的解决方案在将slots=True
传递给@dataclass
装饰器时无法工作,但在一般情况下它似乎足够好用。
这里是一个简单的描述符类Frozen
的实现,如果设置了属性超过一次(即在__init__()
之外),则会引发错误:
class Frozen:
__slots__ = ('private_name', )
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
return value
def __set__(self, obj, value):
if hasattr(obj, self.private_name):
msg = f'Attribute `{self.private_name[1:]}` is immutable!'
raise TypeError(msg) from None
setattr(obj, self.private_name, value)
使用方法:
from dataclasses import dataclass
@dataclass
class Foo:
__slots__ = ('_x', '_y', 'z')
x: int = Frozen()
y: int = Frozen()
z: int
f = Foo(1, 2, 3)
print(f)
f.z = 4
f.z = 5
f.x = 4
关于 Frozen
,它允许您为字段设置一个默认值
,请参见我的帖子,其中说明了如何设置。
时间
如果您感到好奇,我还使用自定义__setattr__()
方法对上述描述方法进行了计时,具体请参见最佳答案。
以下是我使用timeit
模块的示例代码:
from timeit import timeit
@dc
class Foo:
x: int = Frozen()
y: int = Frozen()
z: int
@dataclass(semi=True)
class Foo2:
__slots__ = ('__dict__', 'x', 'y')
x: int
y: int
z: int
n = 100_000
print('Foo.__init__() -> descriptor: ', timeit('Foo(1, 2, 3)', number=n, globals=globals()))
print('Foo.__init__() -> setattr: ', timeit('Foo2(1, 2, 3)', number=n, globals=globals()))
f1 = Foo(1, 2, 3)
f2 = Foo2(1, 2, 3)
print('foo.z -> descriptor: ', timeit('f1.z', number=n, globals=globals()))
print('foo.z -> setattr: ', timeit('f2.z', number=n, globals=globals()))
在我的 Mac M1 上的结果:
Foo.__init__() -> descriptor: 0.0345854579936713
Foo.__init__() -> setattr: 3.2137108749884646
foo.z -> descriptor: 0.003795791999436915
foo.z -> setattr: 0.002478832990163937
这意味着使用描述符方法创建新的Foo
实例速度更快(高达100倍),但使用自定义setattr
方法调用__setattr__()
略微更快,可能是因为实现__slots__
属性减少了内存开销,并且还减少了实例属性的平均查找时间。