我一直在研究Python 3.7的dataclass,作为namedtuple的替代品(通常在需要对数据进行分组处理时使用)。我想知道dataclass是否与property装饰器兼容,以定义getter和setter函数来操作dataclass的数据元素。如果是这样的话,这个描述在哪里?或者有没有可用的示例?
我一直在研究Python 3.7的dataclass,作为namedtuple的替代品(通常在需要对数据进行分组处理时使用)。我想知道dataclass是否与property装饰器兼容,以定义getter和setter函数来操作dataclass的数据元素。如果是这样的话,这个描述在哪里?或者有没有可用的示例?
。
@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'
(t._driver,t.driver)
的结果是 ("default","new")
。请注意,数据类的结果是 ("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'
你可以通过在属性getter中简单地使用hasattr
来避免整个初始化问题,从而避开dataclasses
。
@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
的数据类默认值只存储在类上而不是实例上。在这里可以找到有关数据类和属性的非常详细的帖子,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
wheels
的实例属性。如果你想让__init__
通过setter初始化_wheels
,请使用wheels = InitVar[int]
,然后使用__post_init__
来设置self.wheels = wheels
。 - chepnerresolve_abc_prop
,它创建一个包含 getter 和 setter 函数的新类,如 @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
AData
和一个实现对数据进行操作的 mixin AOpMixin
。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
被用来创建一个新类,该类包含来自AData
的数据和来自AOpMixin
的操作。@resolve_abc_prop
@dataclass
class A(AOpMixin):
x: int
A(x=4) + 2 # A(x=6)
编辑 #1:我创建了一个Python包,使得可以使用数据类dataclass-abc覆盖抽象属性。
我使用这个习语来解决在__init__
期间默认值的问题。如果传递了属性对象(在__init__
期间是这种情况),则从__set__
返回None
将保持初始默认值不变。将私有属性的默认值定义为先前定义的公共属性的默认值,确保私有属性可用。类型提示显示正确的默认值,注释消除了pylint和mypy警告:
from dataclasses import dataclass, field
from pprint import pprint
from typing import Any
class dataclass_property(property): # pylint: disable=invalid-name
def __set__(self, __obj: Any, __value: Any) -> None:
if isinstance(__value, self.__class__):
return None
return super().__set__(__obj, __value)
@dataclass
class Vehicle:
wheels: int = 1
_wheels: int = field(default=wheels, init=False, repr=False)
@dataclass_property # type: ignore
def wheels(self) -> int:
print("Get wheels")
return self._wheels
@wheels.setter # type: ignore
def wheels(self, val: int):
print("Set wheels to", val)
self._wheels = val
if __name__ == "__main__":
pprint(Vehicle())
pprint('#####')
pprint(Vehicle(wheels=4))
输出:
└─ $ python wheels.py
Get wheels
Vehicle(wheels=1)
'#####'
Set wheels to 4
Get wheels
Vehicle(wheels=4)
类型提示:
__init__
中删除了 "underscore" 字段变量(因此它可用于内部使用,但是不会被 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
import wrapt
def dataclass_properties(cls, property_starts='_'):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
properties = [prop for prop in dir(cls) if isinstance(getattr(cls, prop), property)]
new_kwargs = {f"{property_starts}{k}" if k in properties else k: v for k, v in kwargs.items()}
return wrapped(*args, **new_kwargs)
return wrapt.FunctionWrapper(cls, wrapper)()
@dataclass_properties
@dataclass
class State:
_a: int
b: int
_c: int
@property
def a(self):
return self._a
@a.setter
def time(self, value):
self._a = value
if __name__=='__main__':
s = State(b=1,a=2,_c=1)
print(s) # returns: State(_a=2, b=1, _c=1)
print(s.a) # returns: 2
它可以过滤属性和那些不是属性但以“_”开头的变量。它还支持提供属性的真实名称进行实例化。在这种情况下,“_a”。
if __name__=='__main__':
s = State(b=1,_a=2,_c=1)
print(s) # returns: State(_a=2, b=1, _c=1)
虽然我没有解决表示问题。
property
来实现的,这样就不需要定义底层的_name
属性了,代码如下:(见下文)@dataclass
class myclass:
name: MyProperty[in] = MyProperty[int](5)
@name.getter
def name_get(self) -> int:
return self._name
@name.setter
def name_set(self, val: int) -> None
self._name = val
这也显然是可以做到的:
@dataclass
class myclass:
def name_get(self) -> int:
return self._name
name: MyProperty[in] = MyProperty[int](5, fget=name_get)
但这感觉不太干净。
为了加倍...新的property
装饰器本身也是dataclass
可以控制属性的删除,如下:
AttributeError
fulldel
为True
(在示例代码中为默认值),则会在删除后引发AttributeError
无论是mypy
还是pyright
都对typing
很满意。
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, ClassVar, Generic, Optional, overload, TypeVar, Union
from typing_extensions import TypeAlias # python 3.10
from typing_extensions import Self # python 3.11
# Type to be wrapped by the descriptor
T = TypeVar('T')
GETTER: TypeAlias = Callable[[Any], T]
SETTER: TypeAlias = Callable[[Any, T], None]
DELETER: TypeAlias = Callable[[Any], None]
@dataclass
class MyProperty(Generic[T]):
MISSING: ClassVar[object] = object()
val: Union[T, object] = MISSING
fget: Optional[GETTER] = None
fset: Optional[SETTER] = None
fdel: Optional[DELETER] = None
p_prefix: str = field(default='_', kw_only=True)
p_suffix: str = field(default='', kw_only=True)
fulldel: bool = field(default=True, kw_only=True)
name: str = field(default='', init=False) # property name
pname: str = field(init=False) # property underlying name
def __set_name__(self, owner: type[Any], name: str) -> None:
self.name = name
self.pname = pname = f'{self.p_prefix}{name}{self.p_suffix}'
setattr(owner, pname, self.val)
def __set__(self, instance: Any, val: T) -> None:
if self.fset is None:
raise AttributeError(f'{self.name} cannot be set (no setter)')
if val is self: # dataclass setting descriptor as default value
return
self.fset(instance, val)
# overloads allow typecheckers to discriminate actual return type
@overload
def __get__(self, instance: None, owner: type[Any]) -> Self:
...
@overload
def __get__(self, instance: Any, owner: type[Any]) -> T:
...
def __get__(self, instance: Optional[Any], owner: type[Any]) -> Union[Self, T]:
if self.fget is None:
raise AttributeError(f'{self.name} cannot be got (no getter)')
if instance is None: # class level access ... return descriptor
return self
if (val := self.fget(instance)) is self.MISSING:
raise AttributeError(f'{self.name} not set or deleted')
return val
def __delete__(self, instance: Optional[Any]) -> None:
if self.fdel is None:
raise AttributeError(f'{self.name} cannot be deleted (no deleter)')
if instance is None: # class level access ... return descriptor
return
self.fdel(instance)
if self.fulldel:
setattr(instance, self.pname, self.MISSING)
# descriptor attributes for method decoration
def getter(self, f: GETTER) -> None:
self.fget = f
def setter(self, f: SETTER) -> None:
self.fset = f
def deleter(self, f: DELETER) -> None:
self.fdel = f
测试代码
@dataclass
class test:
a_value: MyProperty[int] = MyProperty[int](5, fulldel=False)
b_value: MyProperty[int] = MyProperty[int](10, fulldel=True)
c_value: MyProperty[int] = MyProperty[int]()
@a_value.getter
def a_value_get(self) -> int:
return self._a_value
@a_value.setter
def a_value_set(self, val: int) -> None:
self._a_value = val
@a_value.deleter
def a_value_del(self) -> None:
delattr(self, '_a_value')
@b_value.getter
def b_value_get(self) -> int:
return self._b_value
@b_value.setter
def b_value_set(self, val: int) -> None:
self._b_value = val
@b_value.deleter
def b_value_del(self) -> None:
delattr(self, '_b_value')
@c_value.getter
def c_value_get(self) -> int:
return self._c_value
@c_value.setter
def c_value_set(self, val: int) -> None:
self._c_value = val
@c_value.deleter
def c_value_del(self) -> None:
delattr(self, '_c_value')
# -----------------------------------------------------------------------------
t = test()
print('-' * 10, 'a')
print(t.a_value)
t.a_value = 25
print(t.a_value)
delattr(t, 'a_value')
print(t.a_value) # default class value again
print('-' * 10, 'b')
print(t.b_value)
t.b_value = 35
print(t.b_value)
delattr(t, 'b_value')
try:
print(t.b_value) # default class value again
except AttributeError:
print('Got AttributeError after deletion')
print('-' * 10, 'c')
try:
print(t.c_value)
except AttributeError:
print('AttributeError ... because it has no default value')
t.c_value = 45
print(t.c_value)
delattr(t, 'c_value')
try:
print(t.c_value) # default class value again
except AttributeError:
print('Got AttributeError after deletion')
print('-' * 10, 'repr')
t.b_value = 10
t.c_value = 20
print(f'{t!r}')
输出
---------- a
5
25
5
---------- b
10
35
Got AttributeError after deletion
---------- c
AttributeError ... because it has no default value
45
Got AttributeError after deletion
---------- repr
test(a_value=5, b_value=10, c_value=20)
__init__
中未传递默认值的属性是一个未处理的情况。 - rv.kvetch