【问题标题】:Python dataclasses: What type to use if __post_init__ performs type conversion?Python 数据类:如果 __post_init__ 执行类型转换,使用什么类型?
【发布时间】:2019-01-15 04:42:11
【问题描述】:

我有一个 Python 类,它的字段可以传递几种序列类型之一。为了简化,我将坚持使用元组和列表。 __init__ 将参数转换为MyList

from typing import Union
from dataclasses import dataclass, InitVar, field

class MyList(list):
    pass

@dataclass
class Struct:
    field: Union[tuple, list, MyList]

    def __post_init__(self):
        self.field = MyList(self.field)

field 声明应该使用什么类型?

  • 如果我提供所有可能输入类型的联合,则代码不会记录field访问时始终是MyList
  • 如果我只提供最后的 MyList 类型,PyCharm 会在我传递 Struct()list 时报错。

我可以改用:

_field: InitVar[Union[tuple, list, MyList]] = None
field: MyList = field(init=False)

def __post_init__(self, _field):
    self.field = MyList(_field)

但这非常难看,尤其是在跨 3 个字段重复时。此外,我必须构造一个类似 Struct(_field=field) 的结构,而不是 Struct(field=field)

2018年4月,“tm”在PyCharm的公告上评论了这个问题:https://blog.jetbrains.com/pycharm/2018/04/python-37-introducing-data-class/#comment-323957

【问题讨论】:

  • This discussion on github 涉及转换器的问题,在我看来,这似乎是您所要求的,以及为什么它们不是数据类的一部分。您当前使用 InitVar 的实现是您的方案的预期解决方案。
  • 我的转换器用例之一是将用户输入(通过 YAML 文件)转换为我的配置结构的类型安全枚举。也许我会使用包装构造函数并转换输入的函数来完成此操作。至于我将 ndarray 转换为 MyArray 的其他用例,我将不得不查看我的代码以找到我喜欢的解决方案。
  • 我明白了。那么,另一种选择是切换到支持转换器的attrs。并且不相关,但是自从您提出以来,yaml.load 对用户输入是不安全的。除非您可以信任您的用户,否则您应该使用yaml.safe_load
  • 哦,是的,我使用 ruamel.yaml,我相信所有加载程序(包括往返)都是安全的,除了“不安全”。
  • 不要使用数据类

标签: python python-dataclasses


【解决方案1】:

您将向属性分配值与生成要分配给属性的值的代码混为一谈。我会使用一个单独的类方法来将两段代码分开。

from dataclasses import dataclass


class MyList(list):
    pass


@dataclass
class Struct:
    field: MyList

    @classmethod
    def from_iterable(cls, x):
        return cls(MyList(x))


s1 = Struct(MyList([1,2,3]))
s2 = Struct.from_iterable((4,5,6))

现在,您将现有值MyList 传递给Struct.__init__。元组、列表以及 MyList 可以接受的任何其他内容都将传递给 Struct.from_iterable,这将负责构造 MyList 实例以传递给 Struct

【讨论】:

  • 这是 Rust 采用的方法,如果我从头开始设计 Python 程序,我可能会尝试这种方法。
【解决方案2】:

您是否尝试过 Pydantic BaseModel 而不是数据类?

使用以下代码,我的 Pycharm 不会抱怨:

from pydantic import BaseModel


class MyList(list):
    pass


class PydanticStruct(BaseModel):
    field: MyList

    def __post_init__(self):
        self.field = MyList(self.field)


a = PydanticStruct(field=['a', 'b'])

【讨论】:

  • 我不再积极开发新的 Python 代码或这个项目,但如果我再次从事 Python 工作,我会研究 Pydantic。
【解决方案3】:

dataclasses 在直截了当的数据容器中效果最好,有意识地省略了转换等高级实用程序(有关此功能和类似功能的完整说明,请参阅here)。实现这一点是一项相当大的工作,因为它还应该包含 pycharm 插件,该插件会通知现在支持的转换距离。

更好的方法是使用已经这样做的第三方之一,最受欢迎的是pydantic,可能是因为它有最简单的migration for dataclasses


本机pydantic 解决方案可能如下所示,其中转换代码是MyList 的一部分。以这种方式处理它会使__post_init__ 变得不必要,从而导致更清晰的模型定义:

import pydantic


class MyList(list):
    @classmethod
    def __get_validators__(cls):
        """Validators handle data validation, as well as data conversion.

        This function yields validator functions, with the last-yielded
        result being the final value of a pydantic field annotated with
        this class's type.
        Since we inherit from 'list', our constructor already supports
        building 'MyList' instances from iterables - if we didn't, we 
        would need to write that code by hand and yield it instead.
        """
        yield cls


class Struct(pydantic.BaseModel):
    field: MyList  # accepts any iterable as input


print(Struct(field=(1, 2, 3)))
# prints: field=[1, 2, 3]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-10-27
    • 1970-01-01
    • 2023-04-02
    • 2020-10-20
    • 2011-10-10
    • 2016-04-02
    • 2010-11-13
    • 1970-01-01
    相关资源
    最近更新 更多