45

我一直在阅读 Python 3.7 的数据类作为命名元组的替代品(我通常在必须对结构中的数据进行分组时使用它)。我想知道数据类是否与属性装饰器兼容,以便为数据类的数据元素定义 getter 和 setter 函数。如果是这样,这是在某处描述的吗?或者有没有可用的例子?

4

15 回答 15

41

它确实有效:

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'>

也许这就是为什么没有特别提到属性的原因。然而,PEP-557 的摘要提到了众所周知的 Python 类特性的一般可用性:

因为数据类使用普通的类定义语法,所以您可以自由使用继承、元类、文档字符串、用户定义的方法、类工厂和其他 Python 类特性。

于 2018-06-28T10:10:30.903 回答
13

支持默认值的两个版本

大多数已发布的方法都没有提供一种可读的方式来设置属性的默认值,这是dataclass的一个非常重要的部分。这里有两种可能的方法来做到这一点。

一种方法基于@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相同的方法:通过覆盖类定义之外的字段来绕过数据类机制。

我个人认为这种方式比第一种方式更干净、更易读,因为它遵循普通的数据类习惯用法来定义默认值,并且在 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()
于 2020-04-28T13:08:13.603 回答
10

An@property通常用于通过getter 和setter 将看似公共的参数(例如name)存储到私有属性(例如_name)中,而数据类会为您生成__init__()方法。问题是这个生成__init__()的方法应该通过公共参数进行接口name,同时在内部设置私有属性_name。这不是由数据类自动完成的。

为了具有相同的接口(通过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

现在可以像预期的那样使用具有数据成员的数据类name

my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)

上面的实现做了以下事情:

  • 类成员将name用作公共接口,但它实际上并没有真正存储任何东西
  • _name成员存储实际内容。with 的赋值field(init=False, repr=False)确保@dataclass装饰器在构造__init__()and__repr__()方法时忽略它。
  • name实际返回/设置内容的getter/setter_name
  • 通过 生成的初始化器@dataclass将使用我们刚刚定义的 setter。它不会_name显式初始化,因为我们告诉它不要这样做。
于 2020-04-13T16:06:22.777 回答
6

使用最少的附加代码且没有隐藏变量的解决方案是覆盖该__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")
于 2021-02-28T18:53:30.573 回答
5

一些包装可能很好:

#         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
于 2019-11-25T16:29:04.580 回答
4

这是我将字段定义为__post_init__. 这完全是一个技巧,但它适用于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 中某处提到的这篇博客文章开始的,但遇到了一个问题,即数据类字段被设置为类型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成为 typeproperty而不是str。因此,setter 实际上将接收property对象作为参数而不是字段默认值。

于 2020-05-10T08:15:31.630 回答
4

目前,我发现最好的方法是在单独的子类中按属性覆盖数据类字段。

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__and__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}).

于 2019-10-08T00:01:54.510 回答
2

这种在数据类中使用属​​性的方法也适用于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
于 2021-02-04T21:06:07.530 回答
2

这是另一种允许您拥有没有前导下划线的字段的方法:

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')
于 2020-12-03T14:15:56.763 回答
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
于 2020-02-17T12:10:26.347 回答
0

在尝试了这个线程的不同建议后,我得到了@Samsara Apathika 答案的一些修改版本。简而言之:我从 中删除了“下划线”字段变量__init__(因此它可供内部使用,但看不到 byasdict()或 by __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}
于 2020-11-17T13:21:38.197 回答
0

只需将字段定义放在属性之后:

@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 中使用来回避整个问题,避免dataclasses初始化。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数据类默认值仅存储在类而不是实例上。

于 2021-10-08T16:40:52.780 回答
0

好的,所以这是我第一次尝试让课堂上的一切都自成一体。

我尝试了几种不同的方法,包括@dataclass在类定义的正上方放置一个类装饰器。dataclass装饰器版本的问题是,如果我决定使用它,我的 IDE 会抱怨,然后我会丢失装饰器提供的大部分类型提示。例如,如果我尝试将字段名称传递给构造函数方法,那么当我添加新的类装饰器时,它不再自动完成。我认为这是有道理的,因为 IDE 假设装饰器以某种重要方式覆盖了原始定义,但这成功地说服了我不要尝试使用装饰器方法。

我最终添加了一个元类来更新与数据类字段关联的属性,以检查传递给 的值是否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]

我个人更喜欢后一种方法,因为在我看来它看起来更干净一些,并且例如_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()

最后,这里是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中,以防任何人对此感兴趣。您还可以在链接文档中找到有关使用字段属性的更多信息。快乐编码!

更新(11/2021):

一种更高效的方法是使用元类在类上生成一个__post_init__()只运行一次以修复字段属性的类,以便它与数据类一起使用。你可以在这里查看我添加的要点。我能够对其进行测试,并且在创建多个类实例时,这种方法得到了优化,因为它在第一次__post_init__()运行时就正确设置了所有内容。

于 2021-07-22T16:06:57.513 回答
0

根据上面的想法,我创建了一个类装饰器函数resolve_abc_prop,该函数创建一个包含 @shmee 建议的 getter 和 setter 函数的新类。

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

于 2020-01-20T13:59:14.337 回答
-1

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

信用:joshorr 对这篇文章的回复,在评论中链接到已接受的答案。还有一点RTFM的经典案例。

于 2021-11-05T15:42:08.750 回答