同时使用dataclass和属性时出现奇怪的问题

2024-06-16 12:17:38 发布

您现在位置:Python中文网/ 问答频道 /正文

我在尝试将数据类与属性一起使用时遇到了一个奇怪的问题

我把它复制到一分钟之内:

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>

我希望一个也得不到,而不是一个

我是做错了什么,还是偶然发现了一个错误

在阅读了@juanpa.arrivillaga的回答之后,我认为通过和InitVar创建Upload_可能会解决这个问题,但它仍然返回一个属性对象。我认为正是因为这个原因,他说:

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

我能找到的使用默认值的唯一选项是从dataclass定义中删除uploadedby,并编写一个实际的__init__。这有一个不幸的副作用,即要求您手动为dataclass编写一个__init__,这会否定使用dataclass的某些值。以下是我所做的:

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

这样做的负面影响:

  • 您必须编写样板文件__init__(我的实际用例有大约 20个国家/地区)
  • 你在__repr__中丢失了上传的_,但它在那里 在
  • 不支持对asdict、astuple、dataclasses.replace的调用 正确处理

所以这并不是解决问题的办法

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


Tags: selfnonebyinitsavedefpropertyattrs
1条回答
网友
1楼 · 发布于 2024-06-16 12:17:38

因此,不幸的是,@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)

我不认为有一个干净的方法可以解决这个问题,也许它可以被认为是一个bug,但实际上,不确定解决方案应该是什么,因为本质上,datalcass机制将对类主体中类型注释变量的任何赋值解释为创建的__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>)

但是请注意,您不能设置有用的默认值!它将永远占据这片土地。。。更糟糕的是,在我看来,如果你不想要一个默认值,它总是会创建一个

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

这应该是显而易见的,但是您可以在类上设置property对象

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)
网友
2楼 · 发布于 2024-06-16 12:17:38

使用元类方法稍微修改了原始问题的解决方案-希望有帮助:)

import dataclasses
import dataclass_wizard

@dataclasses.dataclass
class FileObject(metaclass=dataclass_wizard.property_wizard):
    _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  None
None

免责声明:我是此库的创建者(和维护者)

网友
3楼 · 发布于 2024-06-16 12:17:38

另一种选择是设置属性的@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

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

相关问题 更多 >