PEP 557 – 数据类
- 作者:
- Eric V. Smith <eric at trueblade.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2017年6月2日
- Python 版本:
- 3.7
- 发布历史:
- 2017年9月8日,2017年11月25日,2017年11月30日,2017年12月1日,2017年12月2日,2018年1月6日,2018年3月4日
- 决议:
- Python-Dev 消息
审阅者须知
本 PEP 和初始实现是在一个单独的仓库中起草的:https://github.com/ericvsmith/dataclasses。在公共论坛发表评论之前,请至少阅读本 PEP 末尾列出的讨论。
摘要
本 PEP 描述了标准库中称为数据类(Data Classes)的补充。尽管它们使用非常不同的机制,但数据类可以被认为是“带默认值的可变命名元组”。由于数据类使用普通的类定义语法,因此您可以自由地使用继承、元类、文档字符串、用户定义方法、类工厂和其他 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 装饰器将向类添加各种“dunder”方法,如下所述。如果任何已添加的方法已存在于类中,则会引发 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(默认值),则__hash__方法将根据eq和frozen的设置方式生成。如果
eq和frozen都为 true,数据类将为您生成一个__hash__方法。如果eq为 true 且frozen为 false,__hash__将设置为None,标记为不可哈希(确实如此)。如果eq为 false,__hash__将保持不变,这意味着将使用超类的__hash__方法(如果超类是object,这意味着它将回退到基于 id 的哈希)。尽管不建议这样做,但您可以使用
unsafe_hash=True强制数据类创建一个__hash__方法。这可能发生在您的类在逻辑上是不可变的但仍可变的情况下。这是一个特殊用例,应仔细考虑。如果一个类已经有一个明确定义的
__hash__,则在添加__hash__时的行为会改变。当以下情况时,定义了明确定义的__hash__:__eq__在类中定义,并且__hash__定义为除None之外的任何值。__eq__在类中定义,并且定义了任何非None的__hash__。- 类上未定义
__eq__,并且定义了任何__hash__。
如果
unsafe_hash为 true 且存在明确定义的__hash__,则会引发ValueError。如果
unsafe_hash为 false 且存在明确定义的__hash__,则不添加__hash__。有关更多信息,请参阅 Python 文档 [7]。
frozen:如果为 true(默认值为 False),则赋值给字段将引发异常。这模拟了只读冻结实例。如果类中定义了__getattr__或__setattr__,则会引发ValueError。请参阅下面的讨论。
field 可以选择使用正常的 Python 语法指定默认值
@dataclass
class C:
a: int # 'a' has no default value
b: int = 0 # assign a default value for 'b'
在此示例中,a 和 b 都将包含在添加的 __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 值是一个哨兵对象,用于检测是否提供了 default 和 default_factory 参数。使用此哨兵是因为 None 是 default 的有效值。
field() 的参数是
default:如果提供,这将是此字段的默认值。这是必需的,因为field调用本身取代了默认值的正常位置。default_factory:如果提供,它必须是一个零参数可调用对象,当需要此字段的默认值时将调用它。除了其他目的之外,这还可以用于指定具有可变默认值的字段,如下所述。同时指定default和default_factory是错误的。init:如果为 true(默认值),此字段将作为参数包含在生成的__init__方法中。repr:如果为 true(默认值),此字段将包含在生成的__repr__方法返回的字符串中。compare:如果为 True(默认值),此字段将包含在生成的相等和比较方法(__eq__、__gt__等)中。hash:这可以是布尔值或None。如果为 True,此字段将包含在生成的__hash__方法中。如果为None(默认值),则使用compare的值:这通常是预期的行为。如果一个字段用于比较,则应将其视为哈希的一部分。不鼓励将此值设置为除None之外的任何值。将
hash=False但compare=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.x 和 C.y 将不会设置。
Field 对象
Field 对象描述了每个定义的字段。这些对象在内部创建,并由模块级方法 fields() 返回(参见下文)。用户不应直接实例化 Field 对象。其文档属性为
name:字段的名称。type:字段的类型。default、default_factory、init、repr、hash、compare和metadata具有与它们在field()声明中相同的含义和值。
可能存在其他属性,但它们是私有的,不得检查或依赖。
后初始化处理
生成的 __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() 将返回 i 和 j 的 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
字段的最终列表按顺序为 x、y、z。x 的最终类型是 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 类创建,所以它们也共享这个问题。数据类没有通用的方法来检测这种情况。相反,如果数据类检测到类型为 list、dict 或 set 的默认参数,它将引发 TypeError。这是一个部分解决方案,但它确实可以防止许多常见的错误。有关此选项和其他选项的更多详细信息,请参阅“拒绝的提议”部分中的自动支持可变默认值。
使用默认工厂函数是一种为字段创建可变类型新实例作为默认值的方法
@dataclass
class D:
x: list = field(default_factory=list)
assert D().x is not D().x
模块级辅助函数
fields(class_or_instance):返回一个Field对象的元组,这些对象定义了此数据类的字段。接受数据类或数据类的实例。如果未传递数据类或其实例,则引发ValueError。不返回ClassVar或InitVar等伪字段。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,则type使用typing.Any。此函数并非严格必需,因为任何用于创建带有__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]。
后初始化参数
在添加 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__ 函数。
使用 InitVars,__post_init__ 函数现在可以接受参数。它们首先传递给 __init__,后者将它们传递给 __post_init__,用户代码可以在那里根据需要使用它们。
备用类方法构造函数和 InitVar 伪字段之间的唯一真正区别在于对象创建期间所需的非字段参数。使用 InitVars,使用 __init__ 和模块级 replace() 函数时,必须始终指定 InitVars。考虑创建实例需要 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)
如果 init=False 字段在 __post_init__ 运行后从源对象复制到目标对象,那么 s2 最终将是 Square(length=2.0, area=1.0),而不是正确的 Square(length=2.0, area=4.0)。
自动支持可变默认值
一个提议是自动复制默认值,以便如果字面量列表 [] 是默认值,每个实例将获得一个新的列表。这个决定有一些不希望的副作用,所以最终决定是不允许 3 种已知的内置可变类型:list、dict 和 set。有关此选项和其他选项的完整讨论,请参阅 [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