数据类和 属性 装饰器
Dataclasses and property decorator
我一直在阅读 Python 3.7 的数据类作为命名元组的替代方法(我通常在必须将数据分组到结构中时使用)。我想知道 dataclass 是否与 属性 装饰器兼容,以便为数据类的数据元素定义 getter 和 setter 函数。如果是这样,这在某处有描述吗?或者有可用的例子吗?
确实有效:
from dataclasses import dataclass
@dataclass
class Test:
_name: str="schbell"
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, v: str) -> None:
self._name = v
t = Test()
print(t.name) # schbell
t.name = "flirp"
print(t.name) # flirp
print(t) # Test(_name='flirp')
事实上,为什么不呢?最后,你得到的只是一个很好的旧 class,派生自类型:
print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>
也许这就是为什么没有特别提到属性的原因。但是,PEP-557's Abstract 提到了众所周知的 Python class 功能的一般可用性:
Because Data Classes use normal class definition syntax, you are free
to use inheritance, metaclasses, docstrings, user-defined methods,
class factories, and other Python class features.
目前,我发现的最好方法是在单独的子 class.
中用 属性 覆盖数据 class 字段
from dataclasses import dataclass, field
@dataclass
class _A:
x: int = 0
class A(_A):
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
class 的行为类似于常规数据class。并且会正确定义__repr__
和__init__
字段(A(x=4)
而不是A(_x=4)
。缺点是属性不能只读。
This blog post,尝试用同名的 property
覆盖车轮数据 class 属性。
但是,@property
会覆盖默认值 field
,这会导致意外行为。
from dataclasses import dataclass, field
@dataclass
class A:
x: int
# same as: `x = property(x) # Overwrite any field() info`
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
A() # `A(x=<property object at 0x7f0cf64e5fb0>)` Oups
print(A.__dataclass_fields__) # {'x': Field(name='x',type=<class 'int'>,default=<property object at 0x>,init=True,repr=True}
解决这个问题的一种方法是在调用 dataclass metaclass 之后覆盖 class 定义之外的字段,同时避免继承。
@dataclass
class A:
x: int
def x_getter(self):
return self._x
def x_setter(self, value):
self._x = value
A.x = property(x_getter)
A.x = A.x.setter(x_setter)
print(A(x=1))
print(A()) # missing 1 required positional argument: 'x'
应该可以通过创建一些自定义元数据class 并设置一些 field(metadata={'setter': _x_setter, 'getter': _x_getter})
.
来自动覆盖它
一些包装可能会很好:
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2020 Xu Siyuan <inqb@protonmail.com>
#
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
from dataclasses import dataclass, field
MISSING = object()
__all__ = ['property_field', 'property_dataclass']
class property_field:
def __init__(self, fget=None, fset=None, fdel=None, doc=None, **kwargs):
self.field = field(**kwargs)
self.property = property(fget, fset, fdel, doc)
def getter(self, fget):
self.property = self.property.getter(fget)
return self
def setter(self, fset):
self.property = self.property.setter(fset)
return self
def deleter(self, fdel):
self.property = self.property.deleter(fdel)
return self
def property_dataclass(cls=MISSING, / , **kwargs):
if cls is MISSING:
return lambda cls: property_dataclass(cls, **kwargs)
remembers = {}
for k in dir(cls):
if isinstance(getattr(cls, k), property_field):
remembers[k] = getattr(cls, k).property
setattr(cls, k, getattr(cls, k).field)
result = dataclass(**kwargs)(cls)
for k, p in remembers.items():
setattr(result, k, p)
return result
你可以这样使用它:
@property_dataclass
class B:
x: int = property_field(default_factory=int)
@x.getter
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
根据上面的想法,我创建了一个 class 装饰器函数 resolve_abc_prop
,它创建了一个包含 getter 和 setter 函数的新 class建议
通过@shmee.
def resolve_abc_prop(cls):
def gen_abstract_properties():
""" search for abstract properties in super classes """
for class_obj in cls.__mro__:
for key, value in class_obj.__dict__.items():
if isinstance(value, property) and value.__isabstractmethod__:
yield key, value
abstract_prop = dict(gen_abstract_properties())
def gen_get_set_properties():
""" for each matching data and abstract property pair,
create a getter and setter method """
for class_obj in cls.__mro__:
if '__dataclass_fields__' in class_obj.__dict__:
for key, value in class_obj.__dict__['__dataclass_fields__'].items():
if key in abstract_prop:
def get_func(self, key=key):
return getattr(self, f'__{key}')
def set_func(self, val, key=key):
return setattr(self, f'__{key}', val)
yield key, property(get_func, set_func)
get_set_properties = dict(gen_get_set_properties())
new_cls = type(
cls.__name__,
cls.__mro__,
{**cls.__dict__, **get_set_properties},
)
return new_cls
这里我们定义了一个数据classAData
和一个mixinAOpMixin
实现操作
关于数据。
from dataclasses import dataclass, field, replace
from abc import ABC, abstractmethod
class AOpMixin(ABC):
@property
@abstractmethod
def x(self) -> int:
...
def __add__(self, val):
return replace(self, x=self.x + val)
最后,装饰器 resolve_abc_prop
用于创建一个新的 class
使用来自 AData
的数据和来自 AOpMixin
.
的操作
@resolve_abc_prop
@dataclass
class A(AOpMixin):
x: int
A(x=4) + 2 # A(x=6)
编辑 #1:我创建了一个 python 包,可以用数据 class 覆盖抽象属性:dataclass-abc
继 post 关于数据 类 和可以找到的属性 here 之后,TL;DR 版本解决了一些非常丑陋的情况,您必须调用 MyClass(_my_var=2)
和奇怪的 __repr__
输出:
from dataclasses import field, dataclass
@dataclass
class Vehicle:
wheels: int
_wheels: int = field(init=False, repr=False)
def __init__(self, wheels: int):
self._wheels = wheels
@property
def wheels(self) -> int:
return self._wheels
@wheels.setter
def wheels(self, wheels: int):
self._wheels = wheels
@property
通常用于通过 getter 和 [=57 将看似 public 的参数(例如 name
)存储到私有属性(例如 _name
)中=]s,而 dataclasses 会为您生成 __init__()
方法。
问题是这个生成的 __init__()
方法应该通过 public 参数 name
接口,同时在内部设置私有属性 _name
。
这不是由 dataclasses.
自动完成的
为了在设置值和创建对象时拥有相同的接口(通过name
),可以使用以下策略(基于this blogpost,这也提供了更多解释) :
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False)
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
现在可以像人们期望的那样使用数据class 和数据成员 name
:
my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)
以上实现做了以下事情:
name
class 成员将用作 public 接口,但它实际上并没有真正存储任何东西
-
_name
class 成员存储实际内容。 field(init=False, repr=False)
的赋值确保 @dataclass
装饰器在构造 __init__()
和 __repr__()
方法时忽略它。
- getter/setter为
name
实际上returns/sets_name
的内容
- 通过
@dataclass
生成的初始化器将使用我们刚刚定义的setter。它不会显式初始化 _name
,因为我们告诉它不要这样做。
两个支持默认值的版本
大多数已发布的方法都没有提供一种可读的方式来设置 属性 的默认值,这是 dataclass[=36= 的一个非常重要的部分].这里有两种可能的方法。
第一种方式基于@JorenV引用的方法。它在 _name = field()
中定义默认值,并利用观察结果,即如果未指定初始值,则 setter 将传递给 属性 对象本身:
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False, default='baz')
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if type(value) is property:
# initial value not specified, use default
value = Test._name
self._name = value
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
第二种方式基于与@Conchylicultor相同的方法:绕过dataclass 通过覆盖 class 定义之外的字段来实现机器。
我个人认为这种方式比第一种方式更清晰、更易读,因为它遵循正常的 dataclass 习惯用法来定义默认值并且不需要 'magic' 在 setter.
即便如此,我还是希望一切都是独立的...也许一些聪明的人可以找到一种方法将字段更新合并到 dataclass.__post_init__()
或类似的?
from dataclasses import dataclass
@dataclass
class Test:
name: str = 'foo'
@property
def _name(self):
return self._my_str_rev[::-1]
@_name.setter
def _name(self, value):
self._my_str_rev = value[::-1]
# --- has to be called at module level ---
Test.name = Test._name
def main():
obj = Test()
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'baz'
print(obj) # displays: Test(name='baz')
obj = Test(name='bar')
print(obj) # displays: Test(name='bar')
if __name__ == '__main__':
main()
这是我在 __post_init__
中将字段定义为 属性 的操作。这是一个完全 hack,但它适用于 dataclasses
基于 dict 的初始化,甚至适用于 marshmallow_dataclasses.
from dataclasses import dataclass, field, asdict
@dataclass
class Test:
name: str = "schbell"
_name: str = field(init=False, repr=False)
def __post_init__(self):
# Just so that we don't create the property a second time.
if not isinstance(getattr(Test, "name", False), property):
self._name = self.name
Test.name = property(Test._get_name, Test._set_name)
def _get_name(self):
return self._name
def _set_name(self, val):
self._name = val
if __name__ == "__main__":
t1 = Test()
print(t1)
print(t1.name)
t1.name = "not-schbell"
print(asdict(t1))
t2 = Test("llebhcs")
print(t2)
print(t2.name)
print(asdict(t2))
这将打印:
Test(name='schbell')
schbell
{'name': 'not-schbell', '_name': 'not-schbell'}
Test(name='llebhcs')
llebhcs
{'name': 'llebhcs', '_name': 'llebhcs'}
我实际上是从这个 SO 某处提到的 blog post 开始的,但是 运行 进入了数据 class 字段被设置为类型 property
的问题因为装饰器应用于 class。也就是说,
@dataclass
class Test:
name: str = field(default='something')
_name: str = field(init=False, repr=False)
@property
def name():
return self._name
@name.setter
def name(self, val):
self._name = val
会使 name
成为 property
而不是 str
类型。因此,setter 实际上将接收 property
对象作为参数而不是字段默认值。
在尝试了来自该线程的不同建议后,我对@Samsara Apathika 的回答进行了一些修改。简而言之:我从 __init__
中删除了“下划线”字段变量(因此它可供内部使用,但 asdict()
或 __dataclass_fields__
看不到)。
from dataclasses import dataclass, InitVar, field, asdict
@dataclass
class D:
a: float = 10. # Normal attribut with a default value
b: InitVar[float] = 20. # init-only attribute with a default value
c: float = field(init=False) # an attribute that will be defined in __post_init__
def __post_init__(self, b):
if not isinstance(getattr(D, "a", False), property):
print('setting `a` to property')
self._a = self.a
D.a = property(D._get_a, D._set_a)
print('setting `c`')
self.c = self.a + b
self.d = 50.
def _get_a(self):
print('in the getter')
return self._a
def _set_a(self, val):
print('in the setter')
self._a = val
if __name__ == "__main__":
d1 = D()
print(asdict(d1))
print('\n')
d2 = D()
print(asdict(d2))
给出:
setting `a` to property
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
in the setter
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
这是另一种方式,它允许您拥有没有前导下划线的字段:
from dataclasses import dataclass
@dataclass
class Person:
name: str = property
@name
def name(self) -> str:
return self._name
@name.setter
def name(self, value) -> None:
self._name = value
def __post_init__(self) -> None:
if isinstance(self.name, property):
self.name = 'Default'
结果是:
print(Person().name) # Prints: 'Default'
print(Person('Joel').name) # Prints: 'Joel'
print(repr(Person('Jane'))) # Prints: Person(name='Jane')
这种在数据类中使用属性的方法也适用于 asdict
,而且也更简单。为什么?使用 ClassVar
键入的字段会被数据类忽略,但我们仍然可以在我们的属性中使用它们。
@dataclass
def SomeData:
uid: str
_uid: ClassVar[str]
@property
def uid(self) -> str:
return self._uid
@uid.setter
def uid(self, uid: str) -> None:
self._uid = uid
具有最少附加代码且没有隐藏变量的解决方案是覆盖 __setattr__
方法以对字段进行任何检查:
@dataclass
class Test:
x: int = 1
def __setattr__(self, prop, val):
if prop == "x":
self._check_x(val)
super().__setattr__(prop, val)
@staticmethod
def _check_x(x):
if x <= 0:
raise ValueError("x must be greater than or equal to zero")
好的,这是我第一次尝试将所有内容都包含在 class.
中
我尝试了几种不同的方法,包括在 class 定义上方的 @dataclass
旁边有一个 class 装饰器。装饰器版本的问题是,如果我决定使用它,我的 IDE 会抱怨,然后我丢失了 dataclass
装饰器提供的大部分类型提示。例如,如果我试图将一个字段名称传递给构造函数方法,当我添加一个新的 class 装饰器时它不再自动完成。我认为这是有道理的,因为 IDE 假定装饰器以某种重要的方式覆盖了原始定义,但这成功地说服了我不要尝试使用装饰器方法。
我最后添加了一个元class来更新与数据class字段关联的属性,以检查传递给setter
的值是否是一个属性对象正如其他一些解决方案所提到的,现在看来效果很好。以下两种方法中的任何一种都应该适用于测试(基于@Martin CR的解决方案)
from dataclasses import dataclass, field
@dataclass
class Test(metaclass=dataclass_property_support):
name: str = property
_name: str = field(default='baz', init=False, repr=False)
@name
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
# --- other properties like these should not be affected ---
@property
def other_prop(self) -> str:
return self._other_prop
@other_prop.setter
def other_prop(self, value):
self._other_prop = value
这里有一种方法(隐含地)将以下划线开头的 属性 _name
映射到数据 class 字段 name
:
@dataclass
class Test(metaclass=dataclass_property_support):
name: str = 'baz'
@property
def _name(self) -> str:
return self._name[::-1]
@_name.setter
def _name(self, value: str):
self._name = value[::-1]
我个人更喜欢后一种方法,因为在我看来它看起来更干净一些,而且在调用数据class 辅助函数时字段 _name
不会显示 asdict
例如。
以下内容适用于上述任一方法的测试目的。最好的部分是我的 IDE 也不会抱怨任何代码。
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
最后,这里是 metaclass dataclass_property_support
的定义,现在似乎可以正常工作:
from dataclasses import MISSING, Field
from functools import wraps
from typing import Dict, Any, get_type_hints
def dataclass_property_support(*args, **kwargs):
"""Adds support for using properties with default values in dataclasses."""
cls = type(*args, **kwargs)
# the args passed in to `type` will be a tuple of (name, bases, dict)
cls_dict: Dict[str, Any] = args[2]
# this accesses `__annotations__`, but should also work with sub-classes
annotations = get_type_hints(cls)
def get_default_from_annotation(field_: str):
"""Get the default value for the type annotated on a field"""
default_type = annotations.get(field_)
try:
return default_type()
except TypeError:
return None
for f, val in cls_dict.items():
if isinstance(val, property):
public_f = f.lstrip('_')
if val.fset is None:
# property is read-only, not settable
continue
if f not in annotations and public_f not in annotations:
# adding this to check if it's a regular property (not
# associated with a dataclass field)
continue
try:
# Get the value of the field named without a leading underscore
default = getattr(cls, public_f)
except AttributeError:
# The public field is probably type-annotated but not defined
# i.e. my_var: str
default = get_default_from_annotation(public_f)
else:
if isinstance(default, property):
# The public field is a property
# Check if the value of underscored field is a dataclass
# Field. If so, we can use the `default` if one is set.
f_val = getattr(cls, '_' + f, None)
if isinstance(f_val, Field) \
and f_val.default is not MISSING:
default = f_val.default
else:
default = get_default_from_annotation(public_f)
def wrapper(fset, initial_val):
"""
Wraps the property `setter` method to check if we are passed
in a property object itself, which will be true when no
initial value is specified (thanks to @Martin CR).
"""
@wraps(fset)
def new_fset(self, value):
if isinstance(value, property):
value = initial_val
fset(self, value)
return new_fset
# Wraps the `setter` for the property
val = val.setter(wrapper(val.fset, default))
# Replace the value of the field without a leading underscore
setattr(cls, public_f, val)
# Delete the property if the field name starts with an underscore
# This is technically not needed, but it supports cases where we
# define an attribute with the same name as the property, i.e.
# @property
# def _wheels(self)
# return self._wheels
if f.startswith('_'):
delattr(cls, f)
return cls
更新 (10/2021):
我已经设法将上述逻辑(包括对其他边缘情况的支持)封装到链接文档中的帮助程序库 dataclass-wizard
, in case this is of interest to anyone. You can find out more about using field properties 中。编码愉快!
更新 (11/2021):
一种更高效的方法是使用 metaclass 在 class 上生成一个 __post_init__()
,仅 运行s 一次来修复字段属性,因此它可以工作有数据classes。您可以在此处查看我添加的 the gist。我能够对其进行测试,并且在创建多个 class 实例时,这种方法得到了优化,因为它在第一次 __post_init__()
是 运行.
时正确设置了所有内容
只需将字段定义放在属性之后:
@dataclasses.dataclass
class Test:
@property
def driver(self):
print("In driver getter")
return self._driver
@driver.setter
def driver(self, value):
print("In driver setter")
self._driver = value
_driver: typing.Optional[str] =\
dataclasses.field(init=False, default=None, repr=False)
driver: typing.Optional[str] =\
dataclasses.field(init=False, default=driver)
>>> t = Test(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() takes 1 positional argument but 2 were given
>>> t = Test()
>>> t._driver is None
True
>>> t.driver is None
In driver getter
True
>>> t.driver = "asdf"
In driver setter
>>> t._driver == "asdf"
True
>>> t
In driver getter
Test(driver='asdf')
我很惊讶这还不是一个答案,但我质疑它的智慧。这个答案的唯一原因是在表示中包含 属性 - 因为 属性 的后备存储 (_driver
) 已经包含在比较测试和相等性测试等中。例如,这是一个常见的成语:
class Test:
def __init__(self):
self._driver = "default"
@property
def driver(self):
if self._driver == "default":
self._driver = "new"
return self._driver
>>> t = Test()
>>> t
<__main__.Test object at 0x6fffffec11f0>
>>> t._driver
'default'
>>> t.driver
'new'
这是等效的数据class - 除了它在表示中添加了 属性。在标准class中,(t._driver,t.driver)
的结果是("default","new")
。请注意,数据 class 的结果是 ("new","new")
。这是一个非常简单的示例,但您必须认识到,在特殊方法中包含可能具有副作用的属性可能不是最好的主意。
@dataclasses.dataclass
class Test:
@property
def driver(self):
print("In driver getter")
if self._driver == "default":
self._driver = "new"
return self._driver
_driver: typing.Optional[str] =\
dataclasses.field(init=False, default="default", repr=False)
driver: typing.Optional[str] =\
dataclasses.field(init=False, default=driver)
>>> t = Test()
>>> t
In driver getter
Test(driver='new')
>>> t._driver
'new'
>>> t.driver
In driver getter
'new'
所以我建议只使用:
@dataclasses.dataclass
class Test:
_driver: typing.Optional[str] =\
dataclasses.field(init=False, default="default", repr=False)
@property
def driver(self):
print("In driver getter")
if self._driver == "default":
self._driver = "new"
return self._driver
>>> t
Test()
>>> t._driver
'default'
>>> t.driver
In driver getter
'new'
您可以回避整个问题,避免使用 dataclasses
进行初始化,只需在 属性 getter.
中使用 hasattr
@dataclasses.dataclass
class Test:
@property
def driver(self):
print("In driver getter")
if not hasattr(self, "_driver"):
self._driver = "new"
return self._driver
或使用 __post_init__
:
@dataclasses.dataclass
class Test:
def __post_init__(self):
self._driver = None
@property
def driver(self):
print("In driver getter")
if self._driver is None:
self._driver = "new"
return self._driver
为什么要这样做?因为 init=False
数据 class 默认值仅存储在 class 而不是实例。
对于将我带到此页面的用例,即拥有一个不可变的数据类,有一个简单的选项可以使用 @dataclass(frozen=True)
。这删除了 getter 和 setter 的所有相当冗长的显式定义。选项 eq=True
也很有用。
来源:joshorr 对 this post, linked in a comment to the accepted answer. Also a bit of a classical case of RTFM 的回复。
我一直在阅读 Python 3.7 的数据类作为命名元组的替代方法(我通常在必须将数据分组到结构中时使用)。我想知道 dataclass 是否与 属性 装饰器兼容,以便为数据类的数据元素定义 getter 和 setter 函数。如果是这样,这在某处有描述吗?或者有可用的例子吗?
确实有效:
from dataclasses import dataclass
@dataclass
class Test:
_name: str="schbell"
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, v: str) -> None:
self._name = v
t = Test()
print(t.name) # schbell
t.name = "flirp"
print(t.name) # flirp
print(t) # Test(_name='flirp')
事实上,为什么不呢?最后,你得到的只是一个很好的旧 class,派生自类型:
print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>
也许这就是为什么没有特别提到属性的原因。但是,PEP-557's Abstract 提到了众所周知的 Python class 功能的一般可用性:
Because Data Classes use normal class definition syntax, you are free to use inheritance, metaclasses, docstrings, user-defined methods, class factories, and other Python class features.
目前,我发现的最好方法是在单独的子 class.
中用 属性 覆盖数据 class 字段from dataclasses import dataclass, field
@dataclass
class _A:
x: int = 0
class A(_A):
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
class 的行为类似于常规数据class。并且会正确定义__repr__
和__init__
字段(A(x=4)
而不是A(_x=4)
。缺点是属性不能只读。
This blog post,尝试用同名的 property
覆盖车轮数据 class 属性。
但是,@property
会覆盖默认值 field
,这会导致意外行为。
from dataclasses import dataclass, field
@dataclass
class A:
x: int
# same as: `x = property(x) # Overwrite any field() info`
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
A() # `A(x=<property object at 0x7f0cf64e5fb0>)` Oups
print(A.__dataclass_fields__) # {'x': Field(name='x',type=<class 'int'>,default=<property object at 0x>,init=True,repr=True}
解决这个问题的一种方法是在调用 dataclass metaclass 之后覆盖 class 定义之外的字段,同时避免继承。
@dataclass
class A:
x: int
def x_getter(self):
return self._x
def x_setter(self, value):
self._x = value
A.x = property(x_getter)
A.x = A.x.setter(x_setter)
print(A(x=1))
print(A()) # missing 1 required positional argument: 'x'
应该可以通过创建一些自定义元数据class 并设置一些 field(metadata={'setter': _x_setter, 'getter': _x_getter})
.
一些包装可能会很好:
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2020 Xu Siyuan <inqb@protonmail.com>
#
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
from dataclasses import dataclass, field
MISSING = object()
__all__ = ['property_field', 'property_dataclass']
class property_field:
def __init__(self, fget=None, fset=None, fdel=None, doc=None, **kwargs):
self.field = field(**kwargs)
self.property = property(fget, fset, fdel, doc)
def getter(self, fget):
self.property = self.property.getter(fget)
return self
def setter(self, fset):
self.property = self.property.setter(fset)
return self
def deleter(self, fdel):
self.property = self.property.deleter(fdel)
return self
def property_dataclass(cls=MISSING, / , **kwargs):
if cls is MISSING:
return lambda cls: property_dataclass(cls, **kwargs)
remembers = {}
for k in dir(cls):
if isinstance(getattr(cls, k), property_field):
remembers[k] = getattr(cls, k).property
setattr(cls, k, getattr(cls, k).field)
result = dataclass(**kwargs)(cls)
for k, p in remembers.items():
setattr(result, k, p)
return result
你可以这样使用它:
@property_dataclass
class B:
x: int = property_field(default_factory=int)
@x.getter
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
根据上面的想法,我创建了一个 class 装饰器函数 resolve_abc_prop
,它创建了一个包含 getter 和 setter 函数的新 class建议
通过@shmee.
def resolve_abc_prop(cls):
def gen_abstract_properties():
""" search for abstract properties in super classes """
for class_obj in cls.__mro__:
for key, value in class_obj.__dict__.items():
if isinstance(value, property) and value.__isabstractmethod__:
yield key, value
abstract_prop = dict(gen_abstract_properties())
def gen_get_set_properties():
""" for each matching data and abstract property pair,
create a getter and setter method """
for class_obj in cls.__mro__:
if '__dataclass_fields__' in class_obj.__dict__:
for key, value in class_obj.__dict__['__dataclass_fields__'].items():
if key in abstract_prop:
def get_func(self, key=key):
return getattr(self, f'__{key}')
def set_func(self, val, key=key):
return setattr(self, f'__{key}', val)
yield key, property(get_func, set_func)
get_set_properties = dict(gen_get_set_properties())
new_cls = type(
cls.__name__,
cls.__mro__,
{**cls.__dict__, **get_set_properties},
)
return new_cls
这里我们定义了一个数据classAData
和一个mixinAOpMixin
实现操作
关于数据。
from dataclasses import dataclass, field, replace
from abc import ABC, abstractmethod
class AOpMixin(ABC):
@property
@abstractmethod
def x(self) -> int:
...
def __add__(self, val):
return replace(self, x=self.x + val)
最后,装饰器 resolve_abc_prop
用于创建一个新的 class
使用来自 AData
的数据和来自 AOpMixin
.
@resolve_abc_prop
@dataclass
class A(AOpMixin):
x: int
A(x=4) + 2 # A(x=6)
编辑 #1:我创建了一个 python 包,可以用数据 class 覆盖抽象属性:dataclass-abc
继 post 关于数据 类 和可以找到的属性 here 之后,TL;DR 版本解决了一些非常丑陋的情况,您必须调用 MyClass(_my_var=2)
和奇怪的 __repr__
输出:
from dataclasses import field, dataclass
@dataclass
class Vehicle:
wheels: int
_wheels: int = field(init=False, repr=False)
def __init__(self, wheels: int):
self._wheels = wheels
@property
def wheels(self) -> int:
return self._wheels
@wheels.setter
def wheels(self, wheels: int):
self._wheels = wheels
@property
通常用于通过 getter 和 [=57 将看似 public 的参数(例如 name
)存储到私有属性(例如 _name
)中=]s,而 dataclasses 会为您生成 __init__()
方法。
问题是这个生成的 __init__()
方法应该通过 public 参数 name
接口,同时在内部设置私有属性 _name
。
这不是由 dataclasses.
为了在设置值和创建对象时拥有相同的接口(通过name
),可以使用以下策略(基于this blogpost,这也提供了更多解释) :
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False)
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
现在可以像人们期望的那样使用数据class 和数据成员 name
:
my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)
以上实现做了以下事情:
name
class 成员将用作 public 接口,但它实际上并没有真正存储任何东西-
_name
class 成员存储实际内容。field(init=False, repr=False)
的赋值确保@dataclass
装饰器在构造__init__()
和__repr__()
方法时忽略它。 - getter/setter为
name
实际上returns/sets_name
的内容
- 通过
@dataclass
生成的初始化器将使用我们刚刚定义的setter。它不会显式初始化_name
,因为我们告诉它不要这样做。
两个支持默认值的版本
大多数已发布的方法都没有提供一种可读的方式来设置 属性 的默认值,这是 dataclass[=36= 的一个非常重要的部分].这里有两种可能的方法。
第一种方式基于@JorenV引用的方法。它在 _name = field()
中定义默认值,并利用观察结果,即如果未指定初始值,则 setter 将传递给 属性 对象本身:
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False, default='baz')
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if type(value) is property:
# initial value not specified, use default
value = Test._name
self._name = value
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
第二种方式基于与@Conchylicultor相同的方法:绕过dataclass 通过覆盖 class 定义之外的字段来实现机器。
我个人认为这种方式比第一种方式更清晰、更易读,因为它遵循正常的 dataclass 习惯用法来定义默认值并且不需要 'magic' 在 setter.
即便如此,我还是希望一切都是独立的...也许一些聪明的人可以找到一种方法将字段更新合并到 dataclass.__post_init__()
或类似的?
from dataclasses import dataclass
@dataclass
class Test:
name: str = 'foo'
@property
def _name(self):
return self._my_str_rev[::-1]
@_name.setter
def _name(self, value):
self._my_str_rev = value[::-1]
# --- has to be called at module level ---
Test.name = Test._name
def main():
obj = Test()
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'baz'
print(obj) # displays: Test(name='baz')
obj = Test(name='bar')
print(obj) # displays: Test(name='bar')
if __name__ == '__main__':
main()
这是我在 __post_init__
中将字段定义为 属性 的操作。这是一个完全 hack,但它适用于 dataclasses
基于 dict 的初始化,甚至适用于 marshmallow_dataclasses.
from dataclasses import dataclass, field, asdict
@dataclass
class Test:
name: str = "schbell"
_name: str = field(init=False, repr=False)
def __post_init__(self):
# Just so that we don't create the property a second time.
if not isinstance(getattr(Test, "name", False), property):
self._name = self.name
Test.name = property(Test._get_name, Test._set_name)
def _get_name(self):
return self._name
def _set_name(self, val):
self._name = val
if __name__ == "__main__":
t1 = Test()
print(t1)
print(t1.name)
t1.name = "not-schbell"
print(asdict(t1))
t2 = Test("llebhcs")
print(t2)
print(t2.name)
print(asdict(t2))
这将打印:
Test(name='schbell')
schbell
{'name': 'not-schbell', '_name': 'not-schbell'}
Test(name='llebhcs')
llebhcs
{'name': 'llebhcs', '_name': 'llebhcs'}
我实际上是从这个 SO 某处提到的 blog post 开始的,但是 运行 进入了数据 class 字段被设置为类型 property
的问题因为装饰器应用于 class。也就是说,
@dataclass
class Test:
name: str = field(default='something')
_name: str = field(init=False, repr=False)
@property
def name():
return self._name
@name.setter
def name(self, val):
self._name = val
会使 name
成为 property
而不是 str
类型。因此,setter 实际上将接收 property
对象作为参数而不是字段默认值。
在尝试了来自该线程的不同建议后,我对@Samsara Apathika 的回答进行了一些修改。简而言之:我从 __init__
中删除了“下划线”字段变量(因此它可供内部使用,但 asdict()
或 __dataclass_fields__
看不到)。
from dataclasses import dataclass, InitVar, field, asdict
@dataclass
class D:
a: float = 10. # Normal attribut with a default value
b: InitVar[float] = 20. # init-only attribute with a default value
c: float = field(init=False) # an attribute that will be defined in __post_init__
def __post_init__(self, b):
if not isinstance(getattr(D, "a", False), property):
print('setting `a` to property')
self._a = self.a
D.a = property(D._get_a, D._set_a)
print('setting `c`')
self.c = self.a + b
self.d = 50.
def _get_a(self):
print('in the getter')
return self._a
def _set_a(self, val):
print('in the setter')
self._a = val
if __name__ == "__main__":
d1 = D()
print(asdict(d1))
print('\n')
d2 = D()
print(asdict(d2))
给出:
setting `a` to property
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
in the setter
setting `c`
in the getter
in the getter
{'a': 10.0, 'c': 30.0}
这是另一种方式,它允许您拥有没有前导下划线的字段:
from dataclasses import dataclass
@dataclass
class Person:
name: str = property
@name
def name(self) -> str:
return self._name
@name.setter
def name(self, value) -> None:
self._name = value
def __post_init__(self) -> None:
if isinstance(self.name, property):
self.name = 'Default'
结果是:
print(Person().name) # Prints: 'Default'
print(Person('Joel').name) # Prints: 'Joel'
print(repr(Person('Jane'))) # Prints: Person(name='Jane')
这种在数据类中使用属性的方法也适用于 asdict
,而且也更简单。为什么?使用 ClassVar
键入的字段会被数据类忽略,但我们仍然可以在我们的属性中使用它们。
@dataclass
def SomeData:
uid: str
_uid: ClassVar[str]
@property
def uid(self) -> str:
return self._uid
@uid.setter
def uid(self, uid: str) -> None:
self._uid = uid
具有最少附加代码且没有隐藏变量的解决方案是覆盖 __setattr__
方法以对字段进行任何检查:
@dataclass
class Test:
x: int = 1
def __setattr__(self, prop, val):
if prop == "x":
self._check_x(val)
super().__setattr__(prop, val)
@staticmethod
def _check_x(x):
if x <= 0:
raise ValueError("x must be greater than or equal to zero")
好的,这是我第一次尝试将所有内容都包含在 class.
中我尝试了几种不同的方法,包括在 class 定义上方的 @dataclass
旁边有一个 class 装饰器。装饰器版本的问题是,如果我决定使用它,我的 IDE 会抱怨,然后我丢失了 dataclass
装饰器提供的大部分类型提示。例如,如果我试图将一个字段名称传递给构造函数方法,当我添加一个新的 class 装饰器时它不再自动完成。我认为这是有道理的,因为 IDE 假定装饰器以某种重要的方式覆盖了原始定义,但这成功地说服了我不要尝试使用装饰器方法。
我最后添加了一个元class来更新与数据class字段关联的属性,以检查传递给setter
的值是否是一个属性对象正如其他一些解决方案所提到的,现在看来效果很好。以下两种方法中的任何一种都应该适用于测试(基于@Martin CR的解决方案)
from dataclasses import dataclass, field
@dataclass
class Test(metaclass=dataclass_property_support):
name: str = property
_name: str = field(default='baz', init=False, repr=False)
@name
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
# --- other properties like these should not be affected ---
@property
def other_prop(self) -> str:
return self._other_prop
@other_prop.setter
def other_prop(self, value):
self._other_prop = value
这里有一种方法(隐含地)将以下划线开头的 属性 _name
映射到数据 class 字段 name
:
@dataclass
class Test(metaclass=dataclass_property_support):
name: str = 'baz'
@property
def _name(self) -> str:
return self._name[::-1]
@_name.setter
def _name(self, value: str):
self._name = value[::-1]
我个人更喜欢后一种方法,因为在我看来它看起来更干净一些,而且在调用数据class 辅助函数时字段 _name
不会显示 asdict
例如。
以下内容适用于上述任一方法的测试目的。最好的部分是我的 IDE 也不会抱怨任何代码。
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
最后,这里是 metaclass dataclass_property_support
的定义,现在似乎可以正常工作:
from dataclasses import MISSING, Field
from functools import wraps
from typing import Dict, Any, get_type_hints
def dataclass_property_support(*args, **kwargs):
"""Adds support for using properties with default values in dataclasses."""
cls = type(*args, **kwargs)
# the args passed in to `type` will be a tuple of (name, bases, dict)
cls_dict: Dict[str, Any] = args[2]
# this accesses `__annotations__`, but should also work with sub-classes
annotations = get_type_hints(cls)
def get_default_from_annotation(field_: str):
"""Get the default value for the type annotated on a field"""
default_type = annotations.get(field_)
try:
return default_type()
except TypeError:
return None
for f, val in cls_dict.items():
if isinstance(val, property):
public_f = f.lstrip('_')
if val.fset is None:
# property is read-only, not settable
continue
if f not in annotations and public_f not in annotations:
# adding this to check if it's a regular property (not
# associated with a dataclass field)
continue
try:
# Get the value of the field named without a leading underscore
default = getattr(cls, public_f)
except AttributeError:
# The public field is probably type-annotated but not defined
# i.e. my_var: str
default = get_default_from_annotation(public_f)
else:
if isinstance(default, property):
# The public field is a property
# Check if the value of underscored field is a dataclass
# Field. If so, we can use the `default` if one is set.
f_val = getattr(cls, '_' + f, None)
if isinstance(f_val, Field) \
and f_val.default is not MISSING:
default = f_val.default
else:
default = get_default_from_annotation(public_f)
def wrapper(fset, initial_val):
"""
Wraps the property `setter` method to check if we are passed
in a property object itself, which will be true when no
initial value is specified (thanks to @Martin CR).
"""
@wraps(fset)
def new_fset(self, value):
if isinstance(value, property):
value = initial_val
fset(self, value)
return new_fset
# Wraps the `setter` for the property
val = val.setter(wrapper(val.fset, default))
# Replace the value of the field without a leading underscore
setattr(cls, public_f, val)
# Delete the property if the field name starts with an underscore
# This is technically not needed, but it supports cases where we
# define an attribute with the same name as the property, i.e.
# @property
# def _wheels(self)
# return self._wheels
if f.startswith('_'):
delattr(cls, f)
return cls
更新 (10/2021):
我已经设法将上述逻辑(包括对其他边缘情况的支持)封装到链接文档中的帮助程序库 dataclass-wizard
, in case this is of interest to anyone. You can find out more about using field properties 中。编码愉快!
更新 (11/2021):
一种更高效的方法是使用 metaclass 在 class 上生成一个 __post_init__()
,仅 运行s 一次来修复字段属性,因此它可以工作有数据classes。您可以在此处查看我添加的 the gist。我能够对其进行测试,并且在创建多个 class 实例时,这种方法得到了优化,因为它在第一次 __post_init__()
是 运行.
只需将字段定义放在属性之后:
@dataclasses.dataclass
class Test:
@property
def driver(self):
print("In driver getter")
return self._driver
@driver.setter
def driver(self, value):
print("In driver setter")
self._driver = value
_driver: typing.Optional[str] =\
dataclasses.field(init=False, default=None, repr=False)
driver: typing.Optional[str] =\
dataclasses.field(init=False, default=driver)
>>> t = Test(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() takes 1 positional argument but 2 were given
>>> t = Test()
>>> t._driver is None
True
>>> t.driver is None
In driver getter
True
>>> t.driver = "asdf"
In driver setter
>>> t._driver == "asdf"
True
>>> t
In driver getter
Test(driver='asdf')
我很惊讶这还不是一个答案,但我质疑它的智慧。这个答案的唯一原因是在表示中包含 属性 - 因为 属性 的后备存储 (_driver
) 已经包含在比较测试和相等性测试等中。例如,这是一个常见的成语:
class Test:
def __init__(self):
self._driver = "default"
@property
def driver(self):
if self._driver == "default":
self._driver = "new"
return self._driver
>>> t = Test()
>>> t
<__main__.Test object at 0x6fffffec11f0>
>>> t._driver
'default'
>>> t.driver
'new'
这是等效的数据class - 除了它在表示中添加了 属性。在标准class中,(t._driver,t.driver)
的结果是("default","new")
。请注意,数据 class 的结果是 ("new","new")
。这是一个非常简单的示例,但您必须认识到,在特殊方法中包含可能具有副作用的属性可能不是最好的主意。
@dataclasses.dataclass
class Test:
@property
def driver(self):
print("In driver getter")
if self._driver == "default":
self._driver = "new"
return self._driver
_driver: typing.Optional[str] =\
dataclasses.field(init=False, default="default", repr=False)
driver: typing.Optional[str] =\
dataclasses.field(init=False, default=driver)
>>> t = Test()
>>> t
In driver getter
Test(driver='new')
>>> t._driver
'new'
>>> t.driver
In driver getter
'new'
所以我建议只使用:
@dataclasses.dataclass
class Test:
_driver: typing.Optional[str] =\
dataclasses.field(init=False, default="default", repr=False)
@property
def driver(self):
print("In driver getter")
if self._driver == "default":
self._driver = "new"
return self._driver
>>> t
Test()
>>> t._driver
'default'
>>> t.driver
In driver getter
'new'
您可以回避整个问题,避免使用 dataclasses
进行初始化,只需在 属性 getter.
hasattr
@dataclasses.dataclass
class Test:
@property
def driver(self):
print("In driver getter")
if not hasattr(self, "_driver"):
self._driver = "new"
return self._driver
或使用 __post_init__
:
@dataclasses.dataclass
class Test:
def __post_init__(self):
self._driver = None
@property
def driver(self):
print("In driver getter")
if self._driver is None:
self._driver = "new"
return self._driver
为什么要这样做?因为 init=False
数据 class 默认值仅存储在 class 而不是实例。
对于将我带到此页面的用例,即拥有一个不可变的数据类,有一个简单的选项可以使用 @dataclass(frozen=True)
。这删除了 getter 和 setter 的所有相当冗长的显式定义。选项 eq=True
也很有用。
来源:joshorr 对 this post, linked in a comment to the accepted answer. Also a bit of a classical case of RTFM 的回复。