数据类是什么,它们与普通类有何不同?

448

PEP 557 将数据类引入了Python标准库。它表示通过应用下面展示的@dataclass装饰器,它将生成“包括但不限于__init__()”。

from dataclasses import dataclass

@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
它还说数据类是“带有默认值的可变命名元组”,但我不明白这是什么意思,也不知道数据类与普通类有什么不同。
数据类是什么,什么时候最适合使用它们?

12
考虑到PEP的广泛内容,您还想了解什么?namedtuple是不可变的,其属性不能有默认值,而数据类是可变的,并且可以具有默认值。 - jonrsharpe
109
@jonrsharpe 我觉得在Stackoverflow上应该有一个关于这个主题的帖子是很合理的。Stackoverflow旨在成为问答格式的百科全书,不是吗?答案从来不是“只看另一个网站”。这里不应该有负评。 - Luke Davis
62
有五个帖子讲述如何将项目添加到列表中。一个关于@dataclass的问题不会导致网站崩溃。 - eric
10
namedtuples可以有默认值。请看这里:https://dev59.com/aWgu5IYBdhLWcg3wZmQm - MJB
dataclasses非常方便,它应该成为Python中的一个关键字(或者是一个没有装饰的类的一部分)。. . . https://youtube.com/live/0lCIkY32AdI?feature=share - undefined
4个回答

476
数据类只是普通的类,其主要用于存储状态,而不是包含大量逻辑。每当你创建一个主要由属性组成的类时,你就创建了一个数据类。 dataclasses 模块的作用是使创建数据类更加容易。它为你处理了很多样板代码。
当你的数据类需要是可哈希的时,这特别有用;因为这需要一个 __hash__ 方法和一个 __eq__ 方法。如果你添加了一个自定义的 __repr__ 方法以便于调试,那么代码会变得相当冗长:
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    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 total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand
    
    def __repr__(self) -> str:
        return (
            'InventoryItem('
            f'name={self.name!r}, unit_price={self.unit_price!r}, '
            f'quantity_on_hand={self.quantity_on_hand!r})'
        )

    def __hash__(self) -> int:
        return hash((self.name, self.unit_price, self.quantity_on_hand))

    def __eq__(self, other) -> bool:
        if not isinstance(other, InventoryItem):
            return NotImplemented
        return (
            (self.name, self.unit_price, self.quantity_on_hand) == 
            (other.name, other.unit_price, other.quantity_on_hand))

使用dataclasses,您可以将其简化为:

from dataclasses import dataclass

@dataclass(unsafe_hash=True)
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

(基于{{link1:PEP 示例}}的示例)。

同一个类装饰器也可以生成比较方法(__lt____gt__等)并处理不可变性。

namedtuple 类也是数据类,但默认情况下是不可变的(同时也是序列)。 dataclasses 在这方面更加灵活,可以轻松结构化,以便能够 扮演与namedtuple 类相同的角色

这个 PEP 受到了 {{link3:attrs 项目}} 的启发,它可以做得更多(包括 slots、验证器、转换器、元数据等)。

如果你想看一些例子,我最近在我的Advent of Code解决方案中使用了dataclasses,请查看day 7day 8day 11day 20的解决方案。

如果你想在Python版本小于3.7中使用dataclasses模块,则可以安装后移模块(需要3.6)或使用上述提到的attrs项目。


7
在第一个示例中,你是否有意将类成员与同名的实例成员隐藏起来?请帮助理解这个习惯用语。 - noname7619
13
@VladimirLenin:Python中没有类属性,只有类型注释。请查看PEP 526,特别是Class and instance variable annotations section部分。 - Martijn Pieters
3
@Bananach: @dataclass 大致生成相同的 __init__ 方法,带有默认值的 quantity_on_hand 关键字参数。当您创建实例时,它将始终设置 quantity_on_hand 实例属性。因此,我的第一个非 dataclass 示例使用相同的模式来模拟 dataclass 生成的代码会执行的操作。 - Martijn Pieters
1
@Bananach:所以在第一个例子中,我们可以省略设置实例属性并且不遮蔽类属性,在这种意义上它是多余的设置,但是数据类确实会设置它。 - Martijn Pieters
1
@user2853437,你的用例实际上并不受dataclasses支持;也许你最好使用dataclasses的大型亲戚attrs。该项目支持逐字段转换器,使您可以规范化字段值。如果您想坚持使用dataclasses,则确实应在__post_init__方法中进行规范化。 - Martijn Pieters
显示剩余6条评论

246

概述

这个问题已经回答了。但是,这个答案添加了一些实际的例子,以帮助基本理解数据类。

Python数据类到底是什么,在什么情况下最好使用它们?

  1. 代码生成器:生成样板代码;您可以选择在常规类中实现特殊方法,也可以让数据类自动实现这些方法。
  2. 数据容器:存储数据的结构(例如元组和字典),通常具有带点的属性访问,例如classes, namedtuple和其他

"具有默认值的可变命名元组"

这里是后一个短语的含义:

  • 可变的:默认情况下,数据类属性可以被重新赋值。您可以选择使它们不可变(见下面的示例)。
  • 命名元组:您可以像命名元组或普通类一样拥有带点的属性访问。
  • 默认值:您可以将默认值分配给属性。

与常规类相比,您主要节省输入样板代码。


特性

这是数据类功能的概述(简而言之?请参见下一节中的摘要表格)。

您将获得什么

以下是您从数据类中默认获取的功能。

属性 + 表示 + 比较

import dataclasses


@dataclasses.dataclass
#@dataclasses.dataclass()                                       # alternative
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

这些默认值是通过自动将以下关键字设置为True来提供的:

@dataclasses.dataclass(init=True, repr=True, eq=True)

你可以启用哪些功能

如果将适当的关键字设置为True,则可以使用其他功能。

顺序

@dataclasses.dataclass(order=True)
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

现在已经实现了排序方法(重载运算符:< > <= >=),类似于functools.total_ordering,具有更强的相等性测试。

可散列的,可变的

@dataclasses.dataclass(unsafe_hash=True)                        # override base `__hash__`
class Color:
    ...

尽管对象可能是可变的(可能是不希望的),但哈希已经实现。

可哈希,不可变

@dataclasses.dataclass(frozen=True)                             # `eq=True` (default) to be immutable 
class Color:
    ...

现在已经实现了哈希,禁止更改对象或分配属性。

总体而言,如果unsafe_hash=Truefrozen=True,则对象是可哈希的。

另请参阅具有更多详细信息的原始hashing logic table

您无法获得的内容

要获得以下功能,必须手动实现特殊方法:

解包

@dataclasses.dataclass
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

    def __iter__(self):
        yield from dataclasses.astuple(self)

优化

@dataclasses.dataclass
class SlottedColor:
    __slots__ = ["r", "b", "g"]
    r : int
    g : int
    b : int

对象大小现在已被缩小:

>>> imp sys
>>> sys.getsizeof(Color)
1056
>>> sys.getsizeof(SlottedColor)
888

在某些情况下,__slots__ 还可以提高创建实例和访问属性的速度。此外,slots 不允许默认赋值;否则,会引发一个 ValueError
关于slots的更多信息,请参见 blog post

总结表

+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
|       Feature        |       Keyword        |                      Example                       |           Implement in a Class          |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
| Attributes           |  init                |  Color().r -> 0                                    |  __init__                               |
| Representation       |  repr                |  Color() -> Color(r=0, g=0, b=0)                   |  __repr__                               |
| Comparision*         |  eq                  |  Color() == Color(0, 0, 0) -> True                 |  __eq__                                 |
|                      |                      |                                                    |                                         |
| Order                |  order               |  sorted([Color(0, 50, 0), Color()]) -> ...         |  __lt__, __le__, __gt__, __ge__         |
| Hashable             |  unsafe_hash/frozen  |  {Color(), {Color()}} -> {Color(r=0, g=0, b=0)}    |  __hash__                               |
| Immutable            |  frozen + eq         |  Color().r = 10 -> TypeError                       |  __setattr__, __delattr__               |
|                      |                      |                                                    |                                         |
| Unpacking+           |  -                   |  r, g, b = Color()                                 |   __iter__                              |
| Optimization+        |  -                   |  sys.getsizeof(SlottedColor) -> 888                |  __slots__                              |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+

+这些方法不是自动生成的,需要在数据类中手动实现。

* __ne__ 不需要,因此 未实现


额外功能

初始化后

@dataclasses.dataclass
class RGBA:
    r : int = 0
    g : int = 0
    b : int = 0
    a : float = 1.0

    def __post_init__(self):
        self.a : int =  int(self.a * 255)


RGBA(127, 0, 255, 0.5)
# RGBA(r=127, g=0, b=255, a=127)

继承

@dataclasses.dataclass
class RGBA(Color):
    a : int = 0

转换

将数据类递归地转换为元组或字典:

>>> dataclasses.astuple(Color(128, 0, 255))
(128, 0, 255)
>>> dataclasses.asdict(Color(128, 0, 255))
{'r': 128, 'g': 0, 'b': 255}

限制


参考文献

  • R. Hettinger在Dataclasses: The code generator to end all code generators上的演讲
  • T. Hunner在Easier Classes: Python Classes Without All the Cruft上的演讲
  • Python关于哈希细节的文档
  • Real Python在The Ultimate Guide to Data Classes in Python 3.7上的指南
  • A. Shaw在A brief tour of Python 3.7 data classes上的博客文章
  • E. Smith的dataclassesGitHub仓库

6
如果可能的话,我会点两个赞。非常好的回答 @pylang。向您致敬,先生/女士 ;) - 10SecTom
4
这个回答比被接受的那个好得多。太棒了! - Corel
13
我喜欢这些微博长度的扩展回复。它们格式良好,分成易于理解的标题和部分,包括代码片段和参考资料部分。 - Josh Peak
2
有没有想过为什么不支持鸭子类型/类型推断,例如@dataclasses.dataclass class RGB(r=255,g=0,b=0)?对于基本的结构体类型,这种简写对我来说非常重要。 - WestCoastProjects
1
在关闭所有功能的情况下使用 @dataclass 有意义吗?你最终会得到什么? - 303

8

根据 PEP 规范

提供了一个类装饰器,用于检查类定义中的类型注释变量,这些变量在 PEP 526 "变量注释语法" 中定义。在本文档中,这些变量称为字段。使用这些字段,装饰器向类添加生成的方法定义,以支持实例初始化、repr、比较方法和其他可选方法,如规范章节所述。这样的类被称为数据类,但其实这个类并没有什么特别之处:装饰器向类添加生成的方法,然后返回相同的类。

@dataclass 生成器向类添加方法,否则您必须自己定义,例如 __repr____init____lt____gt__


5
考虑这个简单的类Foo
from dataclasses import dataclass
@dataclass
class Foo:    
    def bar():
        pass  

这是dir()内置函数比较。左侧是没有@dataclass装饰器的Foo,右侧是使用了@dataclass装饰器的。

enter image description here

使用inspect模块进行比较后,这里是另一个差异。

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接