数据类和属性装饰器

88

我一直在研究Python 3.7的dataclass,作为namedtuple的替代品(通常在需要对数据进行分组处理时使用)。我想知道dataclass是否与property装饰器兼容,以定义getter和setter函数来操作dataclass的数据元素。如果是这样的话,这个描述在哪里?或者有没有可用的示例?


1
https://florimond.dev/blog/articles/2018/10/reconciling-dataclasses-and-properties-in-python/ - demberto
1
这只是指出了问题,但并没有解决它。例如,在__init__中未传递默认值的属性是一个未处理的情况。 - rv.kvetch
18个回答

1
只需在属性后放置字段定义:


@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的数据类默认值只存储在类上而不是实例上。

1

这里可以找到有关数据类和属性的非常详细的帖子,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

1
你既不需要也不想创建一个名为wheels的实例属性。如果你想让__init__通过setter初始化_wheels,请使用wheels = InitVar[int],然后使用__post_init__来设置self.wheels = wheels - chepner

0
从上述思路中,我创建了一个类装饰器函数resolve_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覆盖抽象属性。


0

我使用这个习语来解决在__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)

类型提示:

带有正确默认值的类型提示


0
在尝试了这个帖子中的不同建议之后,我对 @Samsara Apathika 的答案进行了一些小的修改。简而言之: 我从 __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}

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)

虽然我没有解决表示问题。


0
我发现的更简洁的语法是通过重新实现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
  • 如果在实例化描述符时fulldelTrue(在示例代码中为默认值),则会在删除后引发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)

-1

对于我来到这个页面的使用情况,即要有一个不可变的数据类,有一个简单的选项可以使用@dataclass(frozen=True)。这将删除所有相当冗长的显式定义getter和setter。选项eq=True也很有帮助。

来源:joshorr对this post的回复,链接在被接受的答案的评论中。同时也是一个经典的RTFM案例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接