卖兜搞IT

2024彻底搞懂Python的Dataclass(3)

本文较长,会分为几期,目标是给大家讲解Python的dataclass该怎么用,以及它是如何工作的,而不只是和大家一起创建一个dataclass

本文基于Python的版本是3.11,不同的版本的python,dataclass可能会有不同的差异,这是因为dataclass是一个正在不断演进的python特性,变化较大。

本文为连载,如果没有看过前面的内容,建议先跳转下面的链接进行“补课”

2024彻底搞懂Python的Dataclass(1)

2024彻底搞懂Python的Dataclass(2)

比较和排序

想要dataclass支持排序是非常简单的,只需要加上order=True

from dataclasses import dataclass

@dataclass(frozen=True, order=True)
class Position:
    x: float = 0
    y: float = 0

>>> p1 = Position(1, 2)
>>> p2 = Position(1, 3)
>>> p1 == p2, p1 > p2, p1 < p2, p1>=p2, p1<=p2
(False, False, True, False, True)
>>> sorted([p1, p2])
[Position(x=1, y=2), Position(x=1, y=3)]

dataclass默认的比较是根据tuple (x, y)的大小比较,实现简单(dataclass的比较函数我们是可以重置的,也就是根据具体需求去重新自己实现,但是这里我们就不演示了),但是即使如此,传统class要实现这个功能还是需要很多额外代码的,这里我们直接把实现贴出来(这里用了total_ordering简化了步骤,否则我们要实现的内置函数会更多,大于,大于等于,小于,小于等于)。

from functools import total_ordering

@total_ordering
class Position:
    def __init__(self, x: float = 0, y: float = 0,):
        self._x = x
        self._y = y
    @property
    def x(self):
        return self._x
    @property
    def y(self):
        return self._y
    def __str__(self):
        return f"{self.__class__.__qualname__}(x={self.x}, y={self.y})"
    def __repr__(self):
        return f"{self.__class__.__qualname__}(x={self.x}, y={self.y})"

            def __eq__(self, other):
        if self.__class__ == other.__class__:
            return (self.x, self.y) == (other.x, other.y)
        return NotImplemented
    def __hash__(self):
        return hash((self.x, self.y))
    def __lt__(self, other):
        if self.__class__ == other.__class__:
            return (self.x, self.y) < (other.x, other.y)
        return NotImplemented

测试

>>> p1 = Position(1, 2)
>>> p2 = Position(1, 3)
>>> p1 == p2, p1 > p2, p1 < p2, p1>=p2, p1<=p2
(False, False, True, False, True)
>>> sorted([p1, p2])
[Position(x=1, y=2), Position(x=1, y=3)]
>>>

序列化成字典或者元组

dataclass的实例可以非常方便的序列化成字典或者元组。

from dataclasses import dataclass, asdict, astuple

@dataclass(frozen=True, order=True)
class Position:
    x: float = 0
    y: float = 0

转化成字典或者元组

>>> p = Position(1, 2)
>>> asdict(p)
{'x': 1, 'y': 2}
>>> astuple(p)
(1, 2)
>>>

而传统的class要实现这个功能则需要我们自己写代码,比如

from functools import total_ordering

@total_ordering
class Position:
    def __init__(self, x: float = 0, y: float = 0,):
        self._x = x
        self._y = y
    @property
    def x(self):
        return self._x
    @property
    def y(self):
        return self._y
    def __str__(self):
        return f"{self.__class__.__qualname__}(x={self.x}, y={self.y})"
    def __repr__(self):
        return f"{self.__class__.__qualname__}(x={self.x}, y={self.y})"

            def __eq__(self, other):
        if self.__class__ == other.__class__:
            return (self.x, self.y) == (other.x, other.y)
        return NotImplemented
    def __hash__(self):
        return hash((self.x, self.y))
    def __lt__(self, other):
        if self.__class__ == other.__class__:
            return (self.x, self.y) < (other.x, other.y)
        return NotImplemented
    def asdict(self):
        return {
            'x': self.x,
            'y': self.y
        }
    def astuple(self):
        return (self.x, self.y)

测试

>>> p = Position(1, 2)
>>> p
Position(x=1, y=2)
>>> p.asdict()
{'x': 1, 'y': 2}
>>> p.astuple()
(1, 2)

只允许关键字参数

在dataclass里通过添加参数kw_only=True,可以限制实例初始化的时候只允许关键字参数(keyword only argument),而不能使用位置参数(positional argument).

from dataclasses import dataclass, asdict, astuple

@dataclass(frozen=True, order=True,kw_only=True)
class Position:
    x: float = 0
    y: float = 0

测试

>>> p = Position(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Position.__init__() takes 1 positional argument but 3 were given
>>> p = Position(x=1, y=2)
>>> p
Position(x=1, y=2)
>>>

当然,也可以让部分参数是keyword only argument (比如,只让y是keyword only argument)

from dataclasses import dataclass, KW_ONLY

@dataclass(frozen=True, order=True)
class Position:
    x: float = 0
    _: KW_ONLY
    y: float = 0

测试

>>> p = Position(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Position.__init__() takes from 1 to 2 positional arguments but 3 were given
>>> p = Position(1, y=2)
>>> p
Position(x=1, y=2)
>>>

~~~~~~✍️未完待续✍️~~~~~~