在PEP 557中,数据类被引入python标准库。

它们使用@dataclass装饰器,它们应该是“可变的namedtuples with default”,但我不确定我是否理解这实际上意味着什么,以及它们与普通类有什么不同。

到底什么是python数据类,什么时候使用它们最好?


来自PEP规范:

A class decorator is provided which inspects a class definition for variables with type annotations as defined in PEP 526, "Syntax for Variable Annotations". In this document, such variables are called fields. Using these fields, the decorator adds generated method definitions to the class to support instance initialization, a repr, comparison methods, and optionally other methods as described in the Specification section. Such a class is called a Data Class, but there's really nothing special about the class: the decorator adds generated methods to the class and returns the same class it was given.

@dataclass生成器向类中添加方法,否则你必须像__repr__、__init__、__lt__和__gt__这样定义自己。

数据类只是用于存储状态的常规类,而不是包含大量逻辑。每次创建主要由属性组成的类时,都创建了一个数据类。

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))

使用数据类,您可以将其简化为:

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

相同的类装饰器还可以生成比较方法(__lt__, __gt__等)并处理不可变性。

命名元组类也是数据类,但默认情况下是不可变的(也是序列)。数据类在这方面要灵活得多,并且可以很容易地进行结构化,以便它们可以扮演与命名元组类相同的角色。

PEP的灵感来自attrs项目,它可以做更多的事情(包括插槽、验证器、转换器、元数据等)。

如果你想看一些例子,我最近在我的几个代码降临解决方案中使用了数据类,请参阅第7天、第8天、第11天和第20天的解决方案。

如果你想在Python版本< 3.7中使用数据类模块,那么你可以安装反向移植模块(需要3.6)或使用上面提到的attrs项目。

概述

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

到底什么是python数据类,什么时候使用它们最好?

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

"可变的namedtuples with default[s]"

下面是后一个短语的意思:

Mutable:默认情况下,数据类属性可以被重新分配。你可以选择将它们设置为不可变的(参见下面的示例)。 Namedtuple:您可以像Namedtuple或常规类一样使用点状的属性访问。 Default:可以为属性指定默认值。

与普通类相比,您主要节省了键入样板代码的时间。


特性

这是数据类特性的概述(TL;DR?请参阅下一节中的汇总表)。

你得到了什么

以下是默认情况下从数据类中获得的特性。

属性+表示+比较

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_order具有更强的相等性测试。

Hashable,可变的

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

尽管对象可能是可变的(可能不希望如此),但还是实现了哈希。

Hashable,不变的

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

现在实现了散列,不允许更改对象或为属性赋值。

总的来说,如果unsafe_hash=True或frozen=True,对象是可哈希的。

更多细节请参见原始哈希逻辑表。

你得不到什么

为了获得以下特性,必须手动实现特殊的方法:

拆包

@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__还可以提高创建实例和访问属性的速度。此外,插槽不允许默认赋值;否则,将引发ValueError。

在这篇博客文章中可以看到更多关于插槽的信息。


汇总表

+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
|       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__不需要,因此没有实现。


附加功能

Post-initialization

@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关于数据类的演讲:终结所有代码生成器的代码生成器 T. Hunner关于“更简单的类:没有所有麻烦的Python类”的演讲 Python关于哈希细节的文档 关于Python 3.7中数据类的终极指南的Real Python指南 A. Shaw关于Python 3.7数据类简要介绍的博客文章 E. Smith关于数据类的github存储库

考虑这个简单的类Foo

from dataclasses import dataclass
@dataclass
class Foo:    
    def bar():
        pass  

下面是dir()内置比较。左边是没有@dataclass装饰器的Foo,右边是有@dataclass装饰器的Foo。

这里是另一个区别,在使用inspect模块进行比较之后。