Ger*_*VdE 13 python python-3.7 python-dataclasses
我一直在阅读Python 3.7的数据类作为namedtuples的替代品(我在组织中对数据进行分组时通常使用的).我想知道dataclass是否与属性装饰器兼容,以定义数据类的数据元素的getter和setter函数.如果是这样,这是在某处描述的吗?或者有可用的例子吗?
小智 31
具有最少附加代码且没有隐藏变量的解决方案是重写该__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")
Run Code Online (Sandbox Code Playgroud)
shm*_*mee 11
它确实有效:
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')
Run Code Online (Sandbox Code Playgroud)
事实上,为什么不呢?最后,你得到的只是一个很好的旧类,派生自类型:
print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>
Run Code Online (Sandbox Code Playgroud)
也许这就是为什么没有具体提到房产的原因.但是,PEP-557的摘要提到了众所周知的Python类特性的一般可用性:
因为数据类使用普通的类定义语法,所以您可以自由地使用继承,元类,文档字符串,用户定义的方法,类工厂和其他Python类功能.
支持默认值的两个版本
大多数出版方法不提供可读的方式来设置该属性的默认值,这是相当的重要组成部分数据类。这里有两种可能的方法来做到这一点。
第一种方法基于@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()
Run Code Online (Sandbox Code Playgroud)
所述第二种方法是基于相同的方法@Conchylicultor:绕过数据类通过重写类定义之外的字段机械。
我个人认为这种方式比第一种方式更清晰、更具可读性,因为它遵循正常的数据类习惯用法来定义默认值,并且不需要在 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()
Run Code Online (Sandbox Code Playgroud)
目前,我发现的最好方法是按单独子类中的属性覆盖数据类字段。
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
Run Code Online (Sandbox Code Playgroud)
该类的行为类似于常规数据类。并且将正确定义__repr__and__init__字段(A(x=4)而不是A(_x=4)。缺点是属性不能是只读的。
这篇博文尝试用property同名覆盖wheels dataclass属性。但是,@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}
Run Code Online (Sandbox Code Playgroud)
解决此问题的一种方法是在调用数据类元类之后覆盖类定义外部的字段,同时避免继承。
@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'
Run Code Online (Sandbox Code Playgroud)
通过创建一些自定义元类并设置一些field(metadata={'setter': _x_setter, 'getter': _x_getter}).
An@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
Run Code Online (Sandbox Code Playgroud)
现在可以像对具有数据成员的数据类所期望的那样使用它name:
my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)
Run Code Online (Sandbox Code Playgroud)
上面的实现做了以下几件事:
name类成员将被用作公共接口,但它实际上并没有真正存储任何_name级会员店的实际内容。with 的赋值field(init=False, repr=False)确保@dataclass装饰器在构造__init__()and__repr__()方法时忽略它。name实际返回/设置的getter/setter的内容_name@dataclass器将使用我们刚刚定义的 setter。它不会_name显式初始化,因为我们告诉它不要这样做。一些包装可能会很好:
# 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
Run Code Online (Sandbox Code Playgroud)
你可以这样使用它:
@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
Run Code Online (Sandbox Code Playgroud)
这是我将字段定义为 中的属性所做的操作__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))
Run Code Online (Sandbox Code Playgroud)
这将打印:
Test(name='schbell')
schbell
{'name': 'not-schbell', '_name': 'not-schbell'}
Test(name='llebhcs')
llebhcs
{'name': 'llebhcs', '_name': 'llebhcs'}
Run Code Online (Sandbox Code Playgroud)
实际上,我是从这篇 SO 中某处提到的博客文章开始的,但遇到了由于装饰器应用于类而将数据类字段设置为类型的问题property。那是,
Test(name='schbell')
schbell
{'name': 'not-schbell', '_name': 'not-schbell'}
Test(name='llebhcs')
llebhcs
{'name': 'llebhcs', '_name': 'llebhcs'}
Run Code Online (Sandbox Code Playgroud)
会使name成为 typeproperty而不是str. 因此,设置器实际上将接收property对象作为参数,而不是字段默认值。
| 归档时间: |
|
| 查看次数: |
3889 次 |
| 最近记录: |