Python Dataclass和属性装饰器

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)

  • 这是一个非常可靠的解决方案。您无需使用属性方法,该方法可以是加号,也可以是减号。就我个人而言,我喜欢属性的概念,因为我觉得它确实是Pythonic,但我仍然继续并投票,因为这绝对是一个有效的方法。 (2认同)
  • 我的用例是根据数据类字段值覆盖一些模板化的“Path”实例,因此“property”过于冗长:对于每个变量,“_”前缀变量+属性定义+带有“Path”覆盖的设置器。这个解决方案真是太明智了!非常感谢! (2认同)

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类功能.

  • @Marc我也有同样的担忧.[here](https://blog.florimondmanca.com/reconciling-dataclasses-and-properties-in-python)是如何解决这个问题的一个很好的解释. (8认同)
  • 我想我有点希望数据类允许属性覆盖获取或设置,而不必使用前划线来命名字段。数据类糖的一部分是初始化,这意味着您将以`Test(_name ='foo')`结束-这意味着您的界面将不同于您的创建。这是一个很小的代价,但是,数据类和命名元组之间的差异很小,以至于还有其他用处(这会使它与众不同,从而赋予它更多的用途)。 (4认同)
  • 提供私有成员作为公共数据类字段是一种反模式。 (2认同)

Mar*_* CR 8

支持默认值的两个版本

大多数出版方法不提供可读的方式来设置该属性的默认值,这是相当的重要组成部分数据类。这里有两种可能的方法来做到这一点。

一种方法基于@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)


Con*_*tor 7

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

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


Jor*_*enV 6

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显式初始化,因为我们告诉它不要这样做。


no1*_*yzy 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
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)

  • 美观,但**计算量大。** `property_dataclass()` 的时间复杂度为 `O(n)`(其中 `n` 是修饰类的属性数量),具有不可忽略的大常量。对于简单的数据类来说,这可能没问题,但对于重要的数据类来说,这很快就会导致 CPU 的混乱——尤其是在涉及继承的情况下。核心“@dataclass”装饰器本身的计算强度只会加剧这个问题。 (3认同)

Sam*_*ika 5

这是我将字段定义为 中的属性所做的操作__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对象作为参数,而不是字段默认值。