Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 557 – 数据类

作者:
Eric V. Smith <eric at trueblade.com>
状态:
最终
类型:
标准跟踪
创建:
2017-06-02
Python 版本:
3.7
历史记录:
2017-09-08, 2017-11-25, 2017-11-30, 2017-12-01, 2017-12-02, 2018-01-06, 2018-03-04
决议:
Python-Dev 消息

目录

审阅者注意事项

本 PEP 和初始实现是在一个单独的仓库中草拟的:https://github.com/ericvsmith/dataclasses。在公开论坛发表评论之前,请至少阅读本 PEP 末尾列出的 讨论

摘要

本 PEP 描述了对标准库的添加,称为数据类。虽然它们使用完全不同的机制,但数据类可以被认为是“带有默认值的、可变的 namedtuple”。因为数据类使用正常的类定义语法,所以你可以自由地使用继承、元类、文档字符串、用户定义的方法、类工厂和其他 Python 类特性。

提供了一个类装饰器,它检查类定义中的变量,以查找具有类型标注的变量,如 PEP 526,“变量标注语法”中所定义的。在本文件中,这些变量被称为字段。使用这些字段,装饰器会向类添加生成的函数定义,以支持实例初始化、repr、比较方法,以及可选的其他方法,如 规范 部分所述。这样的类称为数据类,但实际上类本身并没有什么特别之处:装饰器向类添加生成的函数,并返回与它所接收的类相同的类。

举例来说

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

@dataclass 装饰器将向 InventoryItem 类添加等效于以下方法的方法

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand
def __repr__(self):
    return f'InventoryItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity_on_hand={self.quantity_on_hand!r})'
def __eq__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __ne__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __lt__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __le__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __gt__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __ge__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented

数据类可以让你免于编写和维护这些方法。

理由

已经有过许多尝试来定义类,这些类的主要作用是存储通过属性查找访问的值。一些例子包括

  • 标准库中的 collections.namedtuple。
  • 标准库中的 typing.NamedTuple。
  • 流行的 attrs [1] 项目。
  • George Sakkis 的 recordType 技巧 [2],一个受 collections.namedtuple 启发而来的可变数据类型。
  • 许多在线示例技巧 [3]、包 [4] 和问题 [5]。David Beazley 在 2013 年 PyCon 的元类演讲中使用了一种形式的数据类作为示例 [6]

那么,为什么需要这个 PEP 呢?

随着 PEP 526 的加入,Python 有了一种简洁的方法来指定类成员的类型。本 PEP 利用了这种语法,提供了一种简单、不显眼的方式来描述数据类。除了以下两个例外,数据类不会检查标注中指定的类型。

数据类不使用任何基类或元类。这些类的用户可以自由地使用继承和元类,而不会受到数据类的任何干扰。被装饰的类确实是“普通”的 Python 类。数据类装饰器不应该干扰类的任何使用方式。

数据类的一个主要设计目标是支持静态类型检查器。使用 PEP 526 语法就是一个例子,但 fields() 函数和 @dataclass 装饰器的设计也是如此。由于其高度动态的特性,上述一些库难以与静态类型检查器一起使用。

数据类不是,也不打算成为上述所有库的替代机制。但包含在标准库中将使许多更简单的用例能够利用数据类。上面列出的许多库都拥有不同的功能集,当然会继续存在并发展壮大。

哪些情况下不适合使用数据类?

  • 需要与元组或字典的 API 兼容性。
  • 需要超出 PEP 484 和 526 所提供的类型验证,或者需要值验证或转换。

规范

本 PEP 中描述的所有函数都将位于一个名为 dataclasses 的模块中。

提供了一个函数 dataclass,通常用作类装饰器,用于后处理类并添加生成的函数,如下所述。

dataclass 装饰器检查类以查找 field。一个 field 被定义为在 __annotations__ 中识别的任何变量。也就是说,具有类型标注的变量。除了以下两个例外,数据类机制不会检查标注中指定的类型。

请注意,__annotations__ 保证是一个有序的映射,按照类声明顺序排列。所有生成函数中的字段顺序与其在类中出现的顺序一致。

dataclass 装饰器将向类添加各种“双下划线”方法,如下所述。如果类中已经存在任何添加的方法,则会引发 TypeError。装饰器返回与调用它的类相同的类:不会创建新的类。

dataclass 装饰器通常在没有参数和括号的情况下使用。但是,它也支持以下逻辑签名

def dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

如果 dataclass 仅用作一个简单的装饰器,没有参数,则它会像具有此签名中记录的默认值一样运行。也就是说,以下三个使用 @dataclass 的方式是等效的

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
    ...

dataclass 的参数是

  • init: 如果为 True(默认值),则会生成一个 __init__ 方法。
  • repr: 如果为 True(默认值),则会生成一个 __repr__ 方法。生成的 repr 字符串将包含类名以及每个字段的名称和 repr,按照它们在类中定义的顺序排列。标记为从 repr 中排除的字段不会被包含在内。例如:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)

    如果类已经定义了 __repr__,则此参数会被忽略。

  • eq: 如果为 True(默认值),则会生成一个 __eq__ 方法。该方法将类进行比较,就像它是一个包含其字段的元组一样,按照顺序排列。比较中的两个实例必须是相同类型。

    如果类已经定义了 __eq__,则此参数会被忽略。

  • order: 如果为 True(默认值为 False),则会生成 __lt____le____gt____ge__ 方法。这些方法将类进行比较,就像它是一个包含其字段的元组一样,按照顺序排列。比较中的两个实例必须是相同类型。如果 order 为 True 且 eq 为 False,则会引发 ValueError

    如果类已经定义了 __lt____le____gt____ge__ 中的任何一个,则会引发 ValueError

  • unsafe_hash: 如果为 False(默认值),则会根据 eqfrozen 的设置方式生成 __hash__ 方法。

    如果 eqfrozen 都为真,数据类将为你生成一个 __hash__ 方法。如果 eq 为真,而 frozen 为假,__hash__ 将被设置为 None,标记它为不可散列(因为它确实如此)。如果 eq 为假,__hash__ 将保持不变,这意味着将使用超类的 __hash__ 方法(如果超类是 object,这意味着它将回退到基于 id 的散列)。

    虽然不推荐,但你可以使用 unsafe_hash=True 强制数据类创建一个 __hash__ 方法。如果你的类在逻辑上是不可变的,但仍然可以被修改,则可能出现这种情况。这是一个专门的用例,应该谨慎考虑。

    如果一个类已经有一个明确定义的 __hash__,则添加 __hash__ 时的行为将被修改。当以下情况发生时,明确定义的 __hash__ 被定义:

    • __eq__ 在类中定义,并且 __hash__ 被定义为除 None 之外的任何值。
    • __eq__ 在类中定义,并且任何非 None __hash__ 被定义。
    • __eq__ 未在类上定义,并且任何 __hash__ 被定义。

    如果 unsafe_hash 为真,并且存在一个明确定义的 __hash__,则会引发 ValueError

    如果 unsafe_hash 为假,并且存在一个明确定义的 __hash__,则不会添加任何 __hash__

    有关更多信息,请参阅 Python 文档 [7]

  • frozen:如果为真(默认值为假),则对字段的赋值将生成异常。这模拟了只读冻结实例。如果类中定义了 __getattr____setattr__,则会引发 ValueError。请参阅下面的讨论。

field 可以选择性地使用正常的 Python 语法指定默认值

@dataclass
class C:
    a: int       # 'a' has no default value
    b: int = 0   # assign a default value for 'b'

在此示例中,ab 都将包含在添加的 __init__ 方法中,该方法将被定义为

def __init__(self, a: int, b: int = 0):

如果一个没有默认值的字段位于一个有默认值的字段之后,将引发 TypeError。无论这种情况发生在单个类中,还是作为类继承的结果,都是如此。

对于常见和简单的用例,不需要其他功能。但是,有一些数据类功能需要额外的每个字段信息。为了满足对额外信息的这种需求,你可以用对提供的 field() 函数的调用来替换默认字段值。field() 的签名为

def field(*, default=MISSING, default_factory=MISSING, repr=True,
          hash=None, init=True, compare=True, metadata=None)

MISSING 值是一个哨兵对象,用于检测是否提供了 defaultdefault_factory 参数。使用这个哨兵是因为 Nonedefault 的有效值。

field() 的参数为

  • default:如果提供,这将是该字段的默认值。这是必要的,因为 field 调用本身替换了默认值的正常位置。
  • default_factory:如果提供,它必须是一个零参数的可调用对象,当需要该字段的默认值时,将调用它。除了其他用途,这可以用于指定具有可变默认值的字段,如下所述。同时指定 defaultdefault_factory 是错误的。
  • init:如果为真(默认值),则该字段被包含为生成的 __init__ 方法的参数。
  • repr:如果为真(默认值),则该字段被包含在生成的 __repr__ 方法返回的字符串中。
  • compare:如果为真(默认值),则该字段被包含在生成的相等和比较方法(__eq____gt__ 等)中。
  • hash:这可以是布尔值或 None。如果为真,则该字段被包含在生成的 __hash__ 方法中。如果为 None(默认值),则使用 compare 的值:这通常是预期的行为。如果一个字段用于比较,则应将其考虑在散列中。将此值设置为除 None 之外的任何值是不鼓励的。

    设置 hash=Falsecompare=True 的一个可能原因是,如果一个字段的散列值计算代价昂贵,并且该字段需要用于相等性测试,而其他字段有助于类型的散列值的计算。即使一个字段被排除在散列之外,它仍然将用于比较。

  • metadata:这可以是映射或 None。None 被视为一个空字典。此值被包装在 types.MappingProxyType 中,使其成为只读的,并在 Field 对象上公开。数据类根本不使用它,它是作为第三方扩展机制提供的。多个第三方可以分别拥有自己的密钥,在元数据中用作命名空间。

如果字段的默认值由对 field() 的调用来指定,则该字段的类属性将被指定的 default 值替换。如果没有提供 default,则类属性将被删除。其目的是,在 dataclass 装饰器运行之后,类属性将全部包含字段的默认值,就像指定了默认值本身一样。例如,在

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20

类属性 C.z 将为 10,类属性 C.t 将为 20,而类属性 C.xC.y 将不会设置。

Field 对象

Field 对象描述了每个定义的字段。这些对象是在内部创建的,并且由模块级方法 fields() 返回(见下文)。用户永远不应该直接实例化一个 Field 对象。它的文档化属性为

  • name:字段的名称。
  • type:字段的类型。
  • defaultdefault_factoryinitreprhashcomparemetadata 与它们在 field() 声明中的含义和值相同。

可能存在其他属性,但它们是私有的,不得检查或依赖。

post-init 处理

生成的 __init__ 代码将调用一个名为 __post_init__ 的方法,如果它在类中定义。它将被调用为 self.__post_init__()。如果未生成 __init__ 方法,则不会自动调用 __post_init__

除了其他用途之外,这允许初始化依赖于一个或多个其他字段的字段值。例如

@dataclass
class C:
    a: float
    b: float
    c: float = field(init=False)

    def __post_init__(self):
        self.c = self.a + self.b

请参阅下面关于初始化专用变量的部分,了解将参数传递给 __post_init__() 的方法。另请参阅有关 replace() 如何处理 init=False 字段的警告。

类变量

dataclass 实际检查字段类型的另一个地方是确定一个字段是否为 PEP 526 中定义的类变量。它通过检查字段类型是否为 typing.ClassVar 来实现。如果一个字段是 ClassVar,那么它将被排除在字段的考虑范围之外,并被数据类机制忽略。有关更多讨论,请参见 [8]。这种 ClassVar 伪字段不会被模块级 fields() 函数返回。

只初始化变量

dataclass 检查类型注释的另一个地方是确定一个字段是否为仅初始化变量。它通过查看字段类型是否为 dataclasses.InitVar 类型来实现。如果一个字段是 InitVar,那么它将被视为一个称为仅初始化字段的伪字段。因为它不是真正的字段,所以它不会被模块级 fields() 函数返回。仅初始化字段被添加为生成的 __init__ 方法的参数,并传递给可选的 __post_init__ 方法。数据类不会以其他方式使用它们。

例如,假设一个字段将从数据库中初始化,如果在创建类时没有提供值

@dataclass
class C:
    i: int
    j: int = None
    database: InitVar[DatabaseType] = None

    def __post_init__(self, database):
        if self.j is None and database is not None:
            self.j = database.lookup('j')

c = C(10, database=my_database)

在这种情况下,fields() 将为 ij 返回 Field 对象,但不会为 database 返回。

冻结实例

无法创建真正不可变的 Python 对象。但是,通过将 frozen=True 传递给 @dataclass 装饰器,您可以模拟不可变性。在这种情况下,数据类将向该类添加 __setattr____delattr__ 方法。当调用这些方法时,它们将引发 FrozenInstanceError

使用 frozen=True 会产生很小的性能损失:__init__ 无法使用简单赋值来初始化字段,必须使用 object.__setattr__

继承

当数据类由 @dataclass 装饰器创建时,它会按相反的 MRO(即从 object 开始)遍历该类所有基类,并为它找到的每个数据类,将该基类中的字段添加到一个有序的字段映射中。在添加完所有基类字段后,它会将自己的字段添加到该有序映射中。所有生成的代码都将使用此组合的、计算后的有序字段映射。由于字段按插入顺序排列,因此派生类会覆盖基类。示例

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

字段的最终列表按顺序是 xyzx 的最终类型为 int,如类 C 中所指定。

C 生成的 __init__ 方法将类似于

def __init__(self, x: int = 15, y: int = 0, z: int = 10):

默认工厂函数

如果一个字段指定了 default_factory,则在需要字段的默认值时,它将使用零个参数调用该函数。例如,要创建一个新列表实例,请使用

l: list = field(default_factory=list)

如果一个字段被排除在 __init__ 之外(使用 init=False)并且该字段还指定了 default_factory,则默认工厂函数将始终从生成的 __init__ 函数中调用。这是因为没有其他方法可以为该字段提供初始值。

可变默认值

Python 将默认成员变量值存储在类属性中。考虑以下示例,它不使用数据类

class C:
    x = []
    def add(self, element):
        self.x += element

o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x

请注意,类 C 的两个实例共享相同的类变量 x,这是预期的。

使用数据类,如果此代码有效

@dataclass
class D:
    x: List = []
    def add(self, element):
        self.x += element

它将生成类似于以下代码的代码

class D:
    x = []
    def __init__(self, x=x):
        self.x = x
    def add(self, element):
        self.x += element

assert D().x is D().x

这与使用类 C 的原始示例存在相同的问题。也就是说,类 D 的两个实例,在创建类实例时未为 x 指定值,将共享 x 的同一副本。因为数据类只使用普通的 Python 类创建,所以它们也会遇到这个问题。数据类没有通用方法可以检测到这种情况。相反,如果数据类检测到类型为 listdictset 的默认参数,它将引发 TypeError。这是一个部分解决方案,但它确实可以防止许多常见的错误。有关更多详细信息,请参见“拒绝的想法”部分中的 自动支持可变默认值

使用默认工厂函数是一种为字段创建可变类型的新实例作为默认值的方法

@dataclass
class D:
    x: list = field(default_factory=list)

assert D().x is not D().x

模块级辅助函数

  • fields(class_or_instance):返回一个 Field 对象元组,这些对象定义了此数据类的字段。接受数据类或数据类实例。如果未传递数据类或其实例,则引发 ValueError。不返回 ClassVarInitVar 的伪字段。
  • asdict(instance, *, dict_factory=dict):将数据类 instance 转换为字典(通过使用工厂函数 dict_factory)。每个数据类都转换为其字段的字典,以 name:value 对的形式。数据类、字典、列表和元组会递归地转换为字典。例如
    @dataclass
    class Point:
         x: int
         y: int
    
    @dataclass
    class C:
         l: List[Point]
    
    p = Point(10, 20)
    assert asdict(p) == {'x': 10, 'y': 20}
    
    c = C([Point(0, 0), Point(10, 4)])
    assert asdict(c) == {'l': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
    

    如果 instance 不是数据类实例,则引发 TypeError

  • astuple(*, tuple_factory=tuple):将数据类 instance 转换为元组(通过使用工厂函数 tuple_factory)。每个数据类都转换为其字段值的元组。数据类、字典、列表和元组会递归地转换为元组。

    从前面的示例继续

    assert astuple(p) == (10, 20)
    assert astuple(c) == ([(0, 0), (10, 4)],)
    

    如果 instance 不是数据类实例,则引发 TypeError

  • make_dataclass(cls_name, fields, *, bases=(), namespace=None):创建一个新的数据类,其名称为 cls_name,字段如 fields 中定义,基类如 bases 中给出,并使用 namespace 中给出的命名空间进行初始化。fields 是一个可迭代对象,其元素可以是 name(name, type)(name, type, Field)。如果只提供 name,则 typing.Any 将用作 type。此函数不是严格要求的,因为任何用于创建具有 __annotations__ 的新类的 Python 机制都可以随后应用 dataclass 函数将该类转换为数据类。提供此函数是为了方便。例如
    C = make_dataclass('C',
                       [('x', int),
                         'y',
                        ('z', int, field(default=5))],
                       namespace={'add_one': lambda self: self.x + 1})
    

    等同于

    @dataclass
    class C:
        x: int
        y: 'typing.Any'
        z: int = 5
    
        def add_one(self):
            return self.x + 1
    
  • replace(instance, **changes):创建一个与 instance 类型相同的新对象,用 changes 中的值替换字段。如果 instance 不是数据类,则引发 TypeError。如果 changes 中的值未指定字段,则引发 TypeError

    新返回的对象是通过调用数据类的 __init__ 方法创建的。这确保了如果存在 __post_init__,它也会被调用。

    如果存在任何没有默认值的仅初始化变量,则必须在调用 replace 时指定它们,以便它们可以传递给 __init____post_init__

    对于 changes 来说,包含任何定义为具有 init=False 的字段都是错误的。在这种情况下,将引发 ValueError

    请注意 init=False 字段在调用 replace() 时的工作方式。它们不会从源对象中复制,而是在 __post_init__() 中初始化,如果它们被初始化的话。预计 init=False 字段将很少且谨慎使用。如果使用它们,明智的做法可能是拥有备用类构造函数,或者可能是一个自定义的 replace()(或类似名称的)方法来处理实例复制。

  • is_dataclass(class_or_instance): 如果其参数是数据类或数据类的实例,则返回 True,否则返回 False。

    如果你需要知道一个类是否是一个数据类的实例(而不是数据类本身),那么添加一个对 not isinstance(obj, type) 的进一步检查。

    def is_dataclass_instance(obj):
        return is_dataclass(obj) and not isinstance(obj, type)
    

讨论

python-ideas 讨论

这个讨论从 python-ideas 开始 [9],并被转移到 GitHub 仓库 [10] 进行进一步的讨论。作为这个讨论的一部分,我们决定使用 PEP 526 语法来驱动字段的发现。

支持自动设置 __slots__?

至少对于初始版本,__slots__ 将不被支持。 __slots__ 需要在类创建时添加。数据类装饰器是在类创建后调用的,因此为了添加 __slots__,装饰器必须创建一个新的类,设置 __slots__ 并返回它。由于这种行为有点出人意料,数据类的初始版本将不支持自动设置 __slots__。有许多解决方法

  • 在类定义中手动添加 __slots__
  • 编写一个函数(可以作为装饰器使用),该函数使用 fields() 检查类并创建一个带有 __slots__ 设置的新类。

有关更多讨论,请参见 [11]

为什么不直接使用 namedtuple?

  • 任何命名元组都可以意外地与任何其他具有相同字段数量的命名元组进行比较。例如:Point3D(2017, 6, 2) == Date(2017, 6, 2)。在数据类中,这将返回 False。
  • 命名元组可以意外地与元组进行比较。例如,Point2D(1, 10) == (1, 10)。在数据类中,这将返回 False。
  • 实例始终是可迭代的,这可能使得添加字段变得困难。如果一个库定义了
    Time = namedtuple('Time', ['hour', 'minute'])
    def get_time():
        return Time(12, 0)
    

    然后如果用户使用此代码作为

    hour, minute = get_time()
    

    那么将无法向 Time 添加 second 字段,而不会破坏用户的代码。

  • 没有可变实例的选项。
  • 无法指定默认值。
  • 无法控制哪些字段用于 __init____repr__ 等。
  • 无法支持通过继承组合字段。

为什么不直接使用 typing.NamedTuple?

对于具有静态定义字段的类,它使用类型注释支持与数据类类似的语法。这会生成一个命名元组,因此它共享了 namedtuple 的优点和一些缺点。数据类与 typing.NamedTuple 不同,支持通过继承组合字段。

为什么不直接使用 attrs?

  • attrs 的速度比将其移入标准库的速度快。
  • attrs 支持此处未提议的额外功能:验证器、转换器、元数据等。数据类通过不实现这些功能来实现简单性。

有关更多讨论,请参见 [12]

post-init 参数

在添加 InitVar 之前的 PEP 的早期版本中,后初始化函数 __post_init__ 从未接受任何参数。

执行参数化初始化(不仅仅是使用数据类)的正常方法是提供一个备用类方法构造函数。例如

@dataclass
class C:
    x: int

    @classmethod
    def from_file(cls, filename):
        with open(filename) as fl:
            file_value = int(fl.read())
        return C(file_value)

c = C.from_file('file.txt')

由于 __post_init__ 函数是生成 __init__ 中调用的最后一个函数,因此拥有一个类方法构造函数(它也可以在构造对象后立即执行代码)在功能上等同于能够向 __post_init__ 函数传递参数。

使用 InitVar__post_init__ 函数现在可以接受参数。它们首先被传递给 __init__,然后由 __init__ 传递给 __post_init__,用户代码可以在其中根据需要使用它们。

备用类方法构造函数和 InitVar 伪字段之间唯一的真正区别在于在对象创建期间是否需要非字段参数。对于 InitVar,使用 __init__ 和模块级 replace() 函数,InitVar 必须始终指定。考虑这样一种情况,在其中需要一个 context 对象来创建实例,但它不会存储为字段。对于备用类方法构造函数,context 参数始终是可选的,因为你仍然可以通过 __init__ 创建对象(除非你抑制它的创建)。哪种方法更合适将取决于应用程序,但两种方法都受支持。

使用 InitVar 字段的另一个原因是类作者可以控制 __init__ 参数的顺序。这对于具有默认值的常规字段和 InitVar 字段尤为重要,因为所有具有默认值的字段都必须放在所有没有默认值的字段之后。之前的设计是所有初始化专用字段都放在常规字段之后。这意味着如果任何字段具有默认值,那么所有初始化专用字段也必须具有默认值。

asdict 和 astuple 函数名

模块级辅助函数 asdict()astuple() 的名称可以说不符合 PEP 8 规范,应该分别为 as_dict()as_tuple()。但是,经过讨论 [13],决定保持与 namedtuple._asdict()attr.asdict() 的一致性。

被拒绝的方案

在 replace() 中复制 init=False 字段

定义为 init=False 的字段,根据定义不会传递给 __init__,而是使用默认值初始化,或者在 __init__ 中调用默认工厂函数,或者在 __post_init__ 中通过代码初始化。

PEP 的先前版本规定,在 __init__ 返回之后,将从源对象复制 init=False 字段到新创建的对象,但这被认为与使用 __init____post_init__ 来初始化新对象不一致。例如,考虑以下情况

@dataclass
class Square:
    length: float
    area: float = field(init=False, default=0.0)

    def __post_init__(self):
        self.area = self.length * self.length

s1 = Square(1.0)
s2 = replace(s1, length=2.0)

如果在运行 __post_init__ 后,将 init=False 字段从源对象复制到目标对象,那么 s2 最终将变为 Square(length=2.0, area=1.0),而不是正确的 Square(length=2.0, area=4.0)

自动支持可变默认值

一项提议是自动复制默认值,以便如果一个文字列表 [] 是一个默认值,那么每个实例都会获得一个新列表。这个决定的副作用是不希望的,因此最终决定是不允许 3 种已知的内置可变类型:列表、字典和集合。有关此主题和其他选项的完整讨论,请参见 [14]

示例

自定义 __init__ 方法

有时生成的 __init__ 方法不够用。例如,假设你想要创建一个对象来存储 *args**kwargs

@dataclass(init=False)
class ArgHolder:
    args: List[Any]
    kwargs: Mapping[Any, Any]

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

a = ArgHolder(1, 2, three=3)

一个复杂的例子

此代码存在于一个闭源项目中

class Application:
    def __init__(self, name, requirements, constraints=None, path='', executable_links=None, executables_dir=()):
        self.name = name
        self.requirements = requirements
        self.constraints = {} if constraints is None else constraints
        self.path = path
        self.executable_links = [] if executable_links is None else executable_links
        self.executables_dir = executables_dir
        self.additional_items = []

    def __repr__(self):
        return f'Application({self.name!r},{self.requirements!r},{self.constraints!r},{self.path!r},{self.executable_links!r},{self.executables_dir!r},{self.additional_items!r})'

这可以用以下代码替换

@dataclass
class Application:
    name: str
    requirements: List[Requirement]
    constraints: Dict[str, str] = field(default_factory=dict)
    path: str = ''
    executable_links: List[str] = field(default_factory=list)
    executable_dir: Tuple[str] = ()
    additional_items: List[str] = field(init=False, default_factory=list)

数据类版本更具声明性,代码更少,支持 typing,并且包含其他生成的函数。

致谢

以下人员在开发此 PEP 和代码过程中提供了宝贵的意见:Ivan Levkivskyi、Guido van Rossum、Hynek Schlawack、Raymond Hettinger 和 Lisa Roach。感谢他们抽出时间和贡献专业知识。

必须特别提到 attrs 项目。它确实是本 PEP 的灵感来源,我尊重他们做出的设计决策。

参考资料


来源: https://github.com/python/peps/blob/main/peps/pep-0557.rst

最后修改: 2023-09-09 17:39:29 GMT