一起使用数据类和 属性 时的奇怪问题

Weird Issue when using dataclass and property together

我 运行 在尝试将数据类与 属性.

一起使用时遇到了 st运行ge 问题

我已将其复制到最低限度:

import dataclasses

@dataclasses.dataclass
class FileObject:
    _uploaded_by: str = dataclasses.field(default=None, init=False)
    uploaded_by: str = None

    def save(self):
        print(self.uploaded_by)

    @property
    def uploaded_by(self):
        return self._uploaded_by

    @uploaded_by.setter
    def uploaded_by(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

p = FileObject()
p.save()

这输出:

Setter Called with Value  <property object at 0x7faeb00150b0>
<property object at 0x7faeb00150b0>

我希望得到 None 而不是

我是不是做错了什么,还是遇到了错误?

阅读@juanpa.arrivillaga 的回答后,我认为制作 uploaded_by 和 InitVar 可能会解决问题,但它仍然是 return 一个 属性 对象。我想是因为他说的这个:

the datalcass machinery interprets any assignment to a type-annotated variable in the class body as the default value to the created __init__.

我能找到的唯一适用于默认值的选项是从数据类定义中删除 uploadedby 并编写一个实际的 __init__。这有一个不幸的副作用,要求您手动为数据类编写 __init__,这否定了使用数据类的一些价值。这是我所做的:

import dataclasses

@dataclasses.dataclass
class FileObject:
    _uploaded_by: str = dataclasses.field(default=None, init=False)
    uploaded_by: dataclasses.InitVar=None
    other_attrs: str = None

    def __init__(self, uploaded_by=None, other_attrs=None):
        self._uploaded_by = uploaded_by
        self.other_attrs = other_attrs

    def save(self):
        print("Uploaded by: ", self.uploaded_by)
        print("Other Attrs: ", self.other_attrs)

    @property
    def uploaded_by(self):
        if not self._uploaded_by:
            print("Doing expensive logic that should not be repeated")
        return self._uploaded_by

p = FileObject(other_attrs="More Data")
p.save()

p2 = FileObject(uploaded_by='Already Computed', other_attrs="More Data")
p2.save()

输出:

Doing expensive logic that should not be repeated
Uploaded by:  None
Other Attrs:  More Data
Uploaded by:  Already Computed
Other Attrs:  More Data

这样做的负面影响:

所以这真的不能解决问题

我已经在 Python 漏洞追踪器上提交了一个漏洞: https://bugs.python.org/issue39247

因此,不幸的是,@property 语法总是被解释为对 uploaded_by 的赋值(因为,嗯,它 )。 dataclass 机器将其解释为默认值,因此它传递 属性 对象的原因!相当于:

In [11]: import dataclasses
    ...:
    ...: @dataclasses.dataclass
    ...: class FileObject:
    ...:     uploaded_by: str
    ...:     _uploaded_by: str = dataclasses.field(repr=False, init=False)
    ...:     def save(self):
    ...:         print(self.uploaded_by)
    ...:
    ...:     def _get_uploaded_by(self):
    ...:         return self._uploaded_by
    ...:
    ...:     def _set_uploaded_by(self, uploaded_by):
    ...:         print('Setter Called with Value ', uploaded_by)
    ...:         self._uploaded_by = uploaded_by
    ...:     uploaded_by = property(_get_uploaded_by, _set_uploaded_by)
    ...: p = FileObject()
    ...: p.save()
Setter Called with Value  <property object at 0x10761e7d0>
<property object at 0x10761e7d0>

本质上是这样的:

In [13]: @dataclasses.dataclass
    ...: class Foo:
    ...:     bar:int = 1
    ...:     bar = 2
    ...:

In [14]: Foo()
Out[14]: Foo(bar=2)

我不认为有一个干净的方法来解决这个问题,也许它可以被认为是一个错误,但实际上,不确定解决方案应该是什么,因为基本上,datalcass 机制解释对类型的任何赋值-在 class 主体中注释的变量作为创建的 __init__ 的默认值。您也许可以对 @property 语法进行特殊处理,或者只是对 property 对象本身进行特殊处理,因此至少 @propertyx = property(set_x, get_x) 的行为是一致的...

需要说明的是,以下作品类型

In [22]: import dataclasses
    ...:
    ...: @dataclasses.dataclass
    ...: class FileObject:
    ...:     uploaded_by: str
    ...:     _uploaded_by: str = dataclasses.field(repr=False, init=False)
    ...:     @property
    ...:     def uploaded_by(self):
    ...:         return self._uploaded_by
    ...:     @uploaded_by.setter
    ...:     def uploaded_by(self, uploaded_by):
    ...:         print('Setter Called with Value ', uploaded_by)
    ...:         self._uploaded_by = uploaded_by
    ...:
    ...: p = FileObject(None)
    ...: print(p.uploaded_by)
Setter Called with Value  None
None

In [23]: FileObject()
Setter Called with Value  <property object at 0x1086debf0>
Out[23]: FileObject(uploaded_by=<property object at 0x1086debf0>)

但是请注意,您无法设置有用的默认值!它总是需要 属性...更糟糕的是,IMO,如果你不想要默认值,它总是会创建一个!

编辑:找到了一个潜在的解决方法!

这应该是显而易见的,但您可以在 class 上设置 属性 对象。

import dataclasses
import typing
@dataclasses.dataclass
class FileObject:
    uploaded_by:typing.Optional[str]=None

    def _uploaded_by_getter(self):
        return self._uploaded_by

    def _uploaded_by_setter(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

FileObject.uploaded_by = property(
    FileObject._uploaded_by_getter,
    FileObject._uploaded_by_setter
)
p = FileObject()
print(p)
print(p.uploaded_by)

为了完整性,并感谢 @juanpa.arrivillaga,这里是对使用装饰器的原始问题的建议答案。

它至少适用于显示的用例,我更喜欢它而不是描述的方法 here 因为它允许我们使用正常的 dataclass 分配默认值成语。

关键是通过在 'dummy' 上创建 getter 和 setter 来击败 @dataclass 机器属性(此处为“_uploaded_by”),然后从 class.

外部覆盖原始属性

也许比我知识渊博的人可以找到一种方法在 __post_init__() 内进行覆盖 ...

import dataclasses


@dataclasses.dataclass
class FileObject:
    uploaded_by: str = None

    def save(self):
        print(self.uploaded_by)

    @property
    def _uploaded_by(self):
        return self._uploaded_by_attr

    @_uploaded_by.setter
    def _uploaded_by(self, uploaded_by):
        # print('Setter Called with Value ', uploaded_by)
        self._uploaded_by_attr = uploaded_by


# --- has to be called at module level ---
FileObject.uploaded_by = FileObject._uploaded_by


def main():
    p = FileObject()
    p.save()                            # displays 'None'

    p = FileObject()
    p.uploaded_by = 'foo'
    p.save()                            # displays 'foo'

    p = FileObject(uploaded_by='bar')
    p.save()                            # displays 'bar'


if __name__ == '__main__':
    main()

另一种采用 @juanpa.arrivillaga 设置属性的解决方案,可能看起来更面向对象,最初在 python-list by Peter Otten

上提出
import dataclasses
from typing import Optional


@dataclasses.dataclass
class FileObject:
    uploaded_by: Optional[str] = None

class FileObjectExpensive(FileObject):
    @property
    def uploaded_by(self):
        return self._uploaded_by

    @uploaded_by.setter
    def uploaded_by(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

    def save(self):
        print(self.uploaded_by)

p = FileObjectExpensive()
p.save()
p2 = FileObjectExpensive(uploaded_by='Already Computed')
p2.save()

这输出:

Setter Called with Value  None
None
Setter Called with Value  Already Computed
Already Computed

对我来说,这种方法虽然在删除样板方面并不完美,但在分离纯数据容器和该数据的行为方面具有更多的可读性和明确性。并且它使所有变量和属性的名称保持相同,因此可读性似乎是相同的。

使用元类方法对原始问题的解决方案略作修改 - 希望它有所帮助:)

from __future__ import annotations
import dataclasses
from dataclass_wizard import property_wizard

@dataclasses.dataclass
class FileObject(metaclass=property_wizard):
    uploaded_by: str | None
    # uncomment and use for better IDE support
    # _uploaded_by: str | None = dataclasses.field(default=None)

    def save(self):
        print(self.uploaded_by)

    @property
    def uploaded_by(self):
        return self._uploaded_by

    @uploaded_by.setter
    def uploaded_by(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

p = FileObject()
p.save()

这输出(我假设是期望的行为):

Setter Called with Value  None
None

编辑 (4/1/22): 为未来的观众添加说明。 dataclass-wizard 是我创建的一个库,用于解决 字段属性 的默认值 dataclasses 等问题。它可以安装 pip:

$ pip install dataclass-wizard

如果您对仅依赖于 stdlib 的优化方法感兴趣,我创建了一个使用 a metaclass approach.

的简单要点

下面是一般用法。当 name 字段未传递给构造函数时,这将引发错误 正如预期的那样

@dataclass
class Test(metaclass=field_property_support):
    my_int: int
    name: str
    my_bool: bool = True

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, val):
        print(f'Setting name to: {val!r}')
        self._name = val

基于@juanpa.arrivillaga的解决方案,我编写了以下函数,使其可以作为附加装饰器重用:

from dataclasses import fields

def dataprops(cls):
    """A decorator to make dataclasses fields acting as properties
    getter and setter methods names must initate with `get_` and `set_`"""
    
    for field in fields(cls):
        setattr(cls,
                field.name,
                property(
                    getattr(cls,f'get_{field.name}'),
                    getattr(cls,f'set_{field.name}')
                    )
                )
    return cls

简单用法:

from dataclasses import dataclass

@dataprops
@dataclass
class FileObject:
    uploaded_by: str = "no_one"

    def save(self):
        print(self.uploaded_by)

    def get_uploaded_by(self):
        return self._uploaded_by

    def set_uploaded_by(self, uploaded_by):
        print('Setter Called with Value: ', uploaded_by)
        self._uploaded_by = uploaded_by

输出结果:

p = FileObject()
p.save()

# output:
# Setter Called with Value:  no_one
# no_one

p = FileObject("myself")
p.save()

# output:
# Setter Called with Value:  myself
# myself