跳转至

第一部分:Python 数据结构

约 1421 个字 82 行代码 2 张图片 预计阅读时间 6 分钟

一、Python 数据模型

Python 解释器通过调用特殊方法来执行对象基本操作,特殊方法的名称两端都有双下划线,比如我们在使用 [] 来访问元素的时候,其实背后提供支持的是 __getitem__ 方法,运算符重载的实现就是通过重写这些特殊方法。

Code
class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x!r}, {self.y!r})"

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __rmul__(self, scalar):
        return self * scalar

下面是 Python 特殊方法的列表:

Python 特殊方法

首先是不包含运算符的特殊方法:

  • 字符串(字节)表示方式:__repr____str____format____bytes____fspath__
  • 数值转换:__bool____complex____int____float____hash____index__
  • 模拟容器:__len____getitem____setitem____delitem____contains__
  • 迭代:__iter____aiter____next____anext____reversed__
  • 可调用对象:__call____await__
  • 上下文管理:__enter____exit____aexit____aenter__
  • 实例创建和销毁:__new____init____del__
  • 属性管理:__getattr____getattribute____setattr____delattr____dir__
  • 属性描述符:__get____set____delete____set_name__
  • 抽象基类:__instancecheck____subclasscheck__
  • 类元编程:__prepare____init_subclass____class_getitem____mro_entries__

下面是包含运算符的特殊方法:

  • 一元数值运算:__neg__-)、__pos__+)、__abs__abs());
  • 比较运算:__lt__<)、__le__<=)、__eq__==)、__ne__!=)、__gt__>)、__ge__>=);
  • 算术运算:__add__+)、__sub__-)、__mul__*)、__truediv__/)、__floordiv__//)、__mod__%)、__matmul__@)、__divmod__divmod())、__round__round())、__pow__**);
  • 反向算术运算符(交换操作数):__radd____rsub____rmul____rtruediv____rfloordiv____rmod____rmatmul____rdivmod____rround____rpow__
  • 增量赋值算术运算符:__iadd__+=)、__isub__-=)、__imul__*=)、__itruediv__/=)、__ifloordiv__//=)、__imod__%=)、__imatmul__@=)、__ipow__**=);
  • 位运算:__and__&)、__or__|)、__xor__^)、__lshift__<<)、__rshift__>>)、__invert__~);
  • 反向位运算符(交换操作数):__rand____ror____rxor____rlshift____rrshift__
  • 增强赋值位运算符:__iand__&=)、__ior__|=)、__ixor__^=)、__ilshift__<<=)、__irshift__>>=);

对于 __add__ 方法以及 __radd__ 方法,如果第一个操作数对应的特殊方法不可用,则 Python 会尝试调用第二个操作数的 __radd__ 方法。

Python 对象基本都需要一个可用的字符串表示方法,这在调试和交互时非常有用。__repr__ 方法供内置函数 repr() 使用,获取对象的字符串表示形式,交互式控制台和调试器在表达式求值结果上就调用 repr() 函数。我们要求 __repr__ 方法返回的字符串应当没有歧义。__str__ 方法供内置函数 str() 使用,在背后供 print() 函数使用,有时候 __repr__ 方法返回的字符串足够好,就无需定义 __str__ 方法,因为继承自 object__str__ 方法最终会调用 __repr__

默认情况下,用户定义的实例都是真值,除非实现了 __bool__ 方法或者 __len__ 方法。简单来说,bool(x) 会调用 x.__bool__(),并且以 __bool__ 方法的返回值为准,如果没有实现 __bool__ 方法,则尝试调用 __len__ 方法,如果 __len__ 方法返回 0,则 bool(x) 返回 False,否则返回 True。我们要求 __bool__ 方法必须返回布尔值。

下面这张图展示了 Python 的基本容器类型的接口,图中所有的都是抽象基类:

Python 基本容器类型接口

顶部三个抽象基类都只有一个特殊方法,每一个容器类型都需要实现如下事项:

  • Iterable 要支持 for、拆包和其他迭代方式;
  • Sized 要支持内置函数 len()
  • Container 要支持 in 运算符;

Python 不强制要求具体类继承这些抽象基类中的任何一个,只需要实现对应的特殊方法,就说明对应的类满足对应接口。

Collection 有三个很重要的接口:

  • Sequence 规范 list 和 str 等内置类型的接口;
  • Mapping 被 dict、collections.defaultdict 等实现;
  • Set 规范 set、frozenset 等内置类型的接口;

并且只有 Sequence 需要实现 __reversed__ 方法,因为需要按照各种方向排列内容。

二、序列

2.1 列表推导式与生成器表达式

2.2 序列与可迭代对象拆包

2.3 序列模式匹配

2.4 切片

2.5 +*list.sort()sorted()

三、字典和集合

3.x 可哈希

可哈希/Hashable 在 Python 的术语表中有定义,其含义如下:如果一个对象的哈希值在整个生命周期内不改变,并且这个对象可以与其他对象相比较,那么这个对象就是可哈希的,两个可哈希对象相等当且仅当其哈希值相等。可以见的,我们需要实现 __hash__ 方法和 __eq__ 方法。

数值类型和不可变的扁平类型 strbytes 都是可哈希的;如果容器类型是不可变的,并且其所含的对象都是可哈希的,那么容器自身是可哈希的。

一个对象的哈希值根据所使用的 Python 版本和设备架构有关,哈希计算过程中也有加盐操作,正确实现的对象,其哈希值在一个进程内保持不变。

五、数据类构建器

Python 提供了几种构建简单类的方式,这些类只是字段的容器,几乎没有额外功能,这些模式被称为数据类/Data Class,dataclasses 包就支持该模式。这一节介绍 collections.namedtupletyping.NamedTuple@dataclasses.dataclass

5.1 概述

例子
class Coordinate:

    def __init__(self, lat, lon):
        self.lat = lat
        self.lon = lon


>>> from coordinates import Coordinate
>>> moscow = Coordinate(55.76, 37.62)
>>> moscow
<coordinates.Coordinate object at 0x107142f10>
>>> location = Coordinate(55.76, 37.62)
>>> location == moscow
False
>>> (location.lat, location.lon) == (moscow.lat, moscow.lon)
True

可以看见,这个类的实现比较简单但是失败:

  • 没有 __repr__ 方法,从 object 继承的 __repr__ 方法返回的字符串没有信息量;
  • 没有 __eq__ 方法,从 object 继承的 __eq__ 方法比较的是对象的地址/ID,而不是 latlon 的值;
  • 想要对比两个坐标是否相等,需要手动对比 latlon 的值;
1
2
3
4
5
6
7
8
9
>>> from collections import namedtuple
>>> Coordinate = namedtuple('Coordinate', 'lat lon')
>>> issubclass(Coordinate, tuple)
True
>>> moscow = Coordinate(55.756, 37.617)
>>> moscow
Coordinate(lat=55.756, lon=37.617)
>>> moscow == Coordinate(lat=55.756, lon=37.617)
True
1
2
3
4
5
6
7
>>> import typing
>>> Coordinate = typing.NamedTuple('Coordinate',
...     [('lat', float), ('lon', float)])
>>> issubclass(Coordinate, tuple)
True
>>> typing.get_type_hints(Coordinate)
{'lat': <class 'float'>, 'lon': <class 'float'>}

typing.NamedTuple 可以在 class 语句最后使用,类型注释按照 PEP 526 标准编写,这样的代码更易读。值得注意的是,虽然 typing.NamedTuple 位于超类的位置上,但是实际上 typing.NamedTuple 使用元类构建用户类。typing.NamedTuple 生成的 __init__ 方法中,字段参数的顺序与在 class 中出现的顺序相同。

from typing import NamedTuple

class Coordinate(NamedTuple):
    lat: float
    lon: float

    def __str__(self):
        ns = 'N' if self.lat >= 0 else 'S'
        we = 'E' if self.lon >= 0 else 'W'
        return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'

>>> issubclass(Coordinate, tuple)
True
>>> issubclass(Coordinate, NamedTuple)
False
from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

    def __str__(self):
        ns = 'N' if self.lat >= 0 else 'S'
        we = 'E' if self.lon >= 0 else 'W'
        return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'

简单来说,三种数据类构造器的共同点和差异如下:

数据类构造器共同点和差异