Python数据类和属性装饰器

时间:2018-06-28 09:37:32

标签: python python-3.7 python-dataclasses

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

12 个答案:

答案 0 :(得分:8)

确定有效:

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's Abstract提到了众所周知的Python类功能的一般可用性:

  

由于数据类使用常规的类定义语法,因此您很自由   使用继承,元类,文档字符串,用户定义的方法,   类工厂和其他Python类功能。

答案 1 :(得分:4)

这就是我在__post_init__中将字段定义为属性的方法。这是一个完全的hack,但是它可以与基于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中提到的blog post开始的,但是遇到了一个问题,因为装饰器应用于该类,所以dataclass字段被设置为类型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的类型为property,而不是str。因此,设置器实际上将接收property对象作为参数,而不是默认字段。

答案 2 :(得分:3)

支持默认值的两个版本

大多数已发布的方法都没有提供一种可读的方式来设置属性的默认值,这是 dataclass 的重要组成部分。这是两种可行的方法。

第一种方式是基于 @JorenV 引用的方法。它在_name = field()中定义默认值,并利用以下观察结果:如果未指定初始值,则将setter传递给 property 对象本身:

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 相同的方法:通过覆盖类定义之外的字段来绕过 dataclass 机制。

我个人认为这种方法比第一种更干净,更易读,因为它遵循普通的 dataclass 习惯用法来定义默认值,并且在设置方法中不需要“魔术”。

即使如此,我还是希望所有内容都是独立的……也许某个聪明的人可以找到一种方法将字段更新纳入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()

答案 3 :(得分:2)

具有最少附加代码且没有隐藏变量的解决方案是覆盖 __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")

答案 4 :(得分:1)

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

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____init__字段(A(x=4)而不是A(_x=4)。缺点是属性不能为只读。

This blog post,尝试用同名的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}

一种解决此问题的方法,同时避免继承的方法是,在调用数据类元类之后,覆盖类定义之外的字段。

@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})来自动覆盖它。

答案 5 :(得分:1)

这种在数据类中使用属​​性的方法也适用于 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

答案 6 :(得分:0)

一些包装可能会很好:

# Copyright 2019 Xu Siyuan
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
# http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License. 

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

答案 7 :(得分:0)

在有关TL; DR版本here的数据类和属性的一篇非常详尽的文章之后,它解决了一些非常丑陋的情况,其中您必须调用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

答案 8 :(得分:0)

@property通常用于通过getter和setter将看似公共参数(例如name)存储到私有属性(例如_name)中,而数据类则生成{{1 }}方法。 问题在于,此生成的__init__()方法应在内部设置私有属性__init__()时通过公共参数name进行接口。 数据类不会自动完成此操作。

为了具有用于设置值和创建对象的相同接口(通过_name),可以使用以下策略(基于this blogpost,该策略也提供了更多说明):< / p>

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 的数据类中的期望值:

name

上面的实现执行以下操作:

  • my_test = Test(name='foo') my_test.name = 'bar' my_test.name('foobar') print(my_test.name) 类成员将用作公共接口,但实际上并没有真正存储任何内容
  • name类成员存储实际内容。使用_name进行的分配可确保field(init=False, repr=False)装饰器在构造@dataclass__init__()方法时会忽略它。
  • __repr__()的获取器/设置器实际上返回/设置了name的内容
  • 通过_name生成的初始化程序将使用我们刚刚定义的setter。它不会显式初始化@dataclass,因为我们告诉它不要这样做。

答案 9 :(得分:0)

在尝试了此线程的不同建议之后,我附带了@Samsara Apathika答案的一些修改版本。简而言之:我从__init__中删除了“下划线”字段变量(因此可以在内部使用,但是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}

答案 10 :(得分:0)

这是另一种允许您使用不带下划线的字段的方法:

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

答案 11 :(得分:0)

好的,这是我第一次尝试将所有内容都包含在类中。

我尝试了几种不同的方法,包括在类定义上方的 @dataclass 旁边放置一个类装饰器。装饰器版本的问题在于,如果我决定使用它,我的 IDE 会抱怨,然后我会丢失 dataclass 装饰器提供的大部分类型提示。例如,如果我尝试将字段名称传递给构造函数方法,则当我添加新的类装饰器时,它不再自动完成。我认为这是有道理的,因为 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