我一直在研究Python 3.7的dataclass,作为namedtuple的替代品(通常在需要对数据进行分组处理时使用)。我想知道dataclass是否与property装饰器兼容,以定义getter和setter函数来操作dataclass的数据元素。如果是这样的话,这个描述在哪里?或者有没有可用的示例?
我一直在研究Python 3.7的dataclass,作为namedtuple的替代品(通常在需要对数据进行分组处理时使用)。我想知道dataclass是否与property装饰器兼容,以定义getter和setter函数来操作dataclass的数据元素。如果是这样的话,这个描述在哪里?或者有没有可用的示例?
确实起作用:
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')
实际上,为什么不呢?最终,你得到的只是一个来自类型派生出来的好老的类:
print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>
Test(_name='foo')
。虽然这只是一个小问题,但是因为dataclasses和命名元组之间的区别很小,所以这会成为另一个有用的特性(使其更加独特,从而赋予它更多的目的)。 - Marcdef set_booking_ref(self, value:str): self._booking_ref = value.strip()
... booking_ref = property(get_booking_ref, set_booking_ref)
... def __init__(self, booking_ref :str): self.set_booking_ref(self, booking_ref)
。不确定如何使用@property
装饰器来完成这个操作。 - Alan__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")
支持默认值的两个版本
大多数已发布的方法没有提供一种可读的方式来设置属性的默认值,这是dataclass相当重要的一部分。以下是两种可能的方法。
第一种方式基于@JorenV所引用的方法。它在_name = field()
中定义默认值,并利用如下观察结果:如果未指定初始值,则setter将传递property对象本身:
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机制。
个人认为这种方法比第一种更清晰易读,因为它遵循正常的dataclass习惯用法来定义默认值,并且在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()
Test.name = Test._name
这样的琐碎单行代码根本算不上“麻烦”。虽然这是令人讨厌的样板代码,但它仍然比等效的普通类(即非 @dataclass
)要少得多。 - Cecil Curry__post_init__
,仅在初始设置属性时运行一次,因此它与数据类很好地配合使用。你可以在 gist 这里 找到它。 - rv.kvetch@property
通常用于通过getter和setter将看似公共参数(例如name
)存储到私有属性(例如_name
)中,而数据类为您生成__init __()
方法。
问题在于,这个生成的__init __()
方法应该通过公共参数name
进行接口交互,同时在内部设置私有属性_name
。
数据类不能自动完成此操作。name
)来设置值和创建对象,可以使用以下策略(基于这篇博客文章,提供了更多解释):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
现在可以像使用带有数据成员name
的数据类那样使用它:
my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)
上述实现做了以下几件事情:
name
类成员将作为公共接口使用,但实际上并不真正存储任何内容。_name
类成员存储实际内容。使用 field(init=False, repr=False)
进行赋值,确保 @dataclass
装饰器在构建 __init__()
和 __repr__()
方法时忽略它。name
的 getter/setter 实际上返回/设置 _name
的内容。@dataclass
生成的初始化程序将使用我们刚刚定义的 setter。它不会显式地初始化 _name
,因为我们告诉它不要这样做。name
的双重定义!但不会有运行时错误。 - gmagno目前,我发现最好的方法是在一个独立的子类中通过属性覆盖数据类字段。
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
__repr__
和__init__
字段(A(x=4)
而不是A(_x=4)
)。缺点是属性不能是只读的。
这篇博客文章试图通过同名的property
覆盖wheels数据类属性。
然而,@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
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'
通过创建自定义元类并设置一些 field(metadata={'setter': _x_setter, 'getter': _x_getter})
,很可能可以自动覆盖它。
_A
,同时使用 @dataclass
外部 A(_A)
。 - InQβ一些包装可能是有益的:
# 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
property_dataclass()
的时间复杂度为 O(n)
(其中 n
是装饰类的属性数量),具有大量不可忽略的常数。对于简单的数据类可能还可以接受,但对于非平凡的数据类,特别是涉及继承的情况下,很快就会变成 CPU 负载的混合物。核心 @dataclass
装饰器本身的计算强度只会加剧这种担忧。 - Cecil Curry以下是我在__post_init__
中定义字段的属性的方式。虽然这是一个有点投机取巧的方法,但它可以与基于字典的dataclasses
初始化和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'}
实际上,我是从这篇在Stack Overflow中提到的博文开始的,但遇到了一个问题,即由于装饰器被应用于类,数据类字段被设置为property
类型。
@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
对象作为参数,而不是字段默认值。
使用数据类中的属性方法也适用于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
uid: str = None
。当然,另一个问题是如果没有通过构造函数提供值,则uid
将被设置为属性对象,但可以轻松地通过使用装饰器来解决这个问题。 - rv.kvetch这里介绍另一种方法,可以让您使用没有前导下划线的字段:
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')
print(p.name)
assert p.name == 'test'
.
我猜解决方法可能是像这样分配:name: str = None
并用 @property
装饰它本身;虽然 PyCharm 在实现层面上仍然会发出警告,但在客户端,警告似乎已经消失了。 - rv.kvetch好的,这是我第一次尝试将所有内容都包含在类中。
我尝试了几种不同的方法,包括在类定义上方使用类装饰器@dataclass
。使用装饰器版本的问题在于,如果我决定使用它,我的IDE会抱怨,然后我就失去了大部分dataclass
装饰器提供的类型提示。例如,如果我正在尝试将字段名称传递到构造函数方法中,当我添加一个新的类装饰器时,它不再自动完成。我想这是有道理的,因为IDE认为装饰器以某种重要的方式覆盖了原始定义,但这让我相信不要尝试使用装饰器方法。
最终,我添加了一个元类来更新与dataclass
字段相关联的属性,以检查传递给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
映射到数据类字段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]
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()
最后,这是元类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
更新(2021年10月):
我已经将上述逻辑 - 包括对额外边缘情况的支持 - 封装到助手库 dataclass-wizard
中,以防这对任何人有兴趣。您可以在链接的文档中了解更多关于使用字段属性的内容。快乐编码!
更新(2021年11月):
一种更高效的方法是使用元类来生成一个仅运行一次的 __post_init__()
,以修复字段属性,使其与数据类配合使用。您可以在此处查看代码片段。我已经测试过了,当创建多个类实例时,这种方法被优化了,因为它设置了第一次运行 __post_init__()
时需要的所有东西。
__init__
中未传递默认值的属性是一个未处理的情况。 - rv.kvetch