第一章 Python数据模型

Guido 对语言设计美学的理解令人惊叹。我认识不少优秀的语言设计师,他们可以构建出理论上看起来漂亮的语言,但没人会使用。Guido知道如何在理论上做出一定妥协,设计出来的语言让使用者如沐春风,这真是不可多得。

                ------Jim Hugunin, creator of Jython, cocreator of AspectJ, and architect of the .Net DLR

Python 的最佳品质之一是它的一致性。在使用 Python 一段时间后,你就会开始理解python语言,并能正确的猜测出全新的语言特征的作用。

但是,如果在接触 Python 前学习了另一种面向对象语言,你可能会发现使用 len(collection) 而不是 collection.len()的用法很奇怪。这种明显怪现象只是冰山一角,但是如果正确理解python语言后,你会发现这就是我们称之为 Pythonic 的关键之处。这座冰山被称为 Python 数据模型,我们平时自己创建对象就要使用数据模型的API,确保使用最地道的语言功能。

数据模型其实是对python框架的描述。数据模型规范了语言自身各个组成部分的接口,例如序列、函数、迭代器、协程、类、上下文管理器等。

在使用框架时,我们会花费大量时间实现方法来交给框架调用。当我们用 Python 数据模型构建新的类时也是一样的情况。Python 解释器调用特殊方法来执行基本的对象操作,这些操作通常由特殊语法触发。特殊的方法名称以两个下划线开头和两个下划线结尾。例如,__getitem__是支持语法 obj[key]的 特殊方法。为了获得 my_collection[key]的值,解释器调用 my_collection.__getitem__(key)。

这些特殊方法名能让你自己的对象实现和支持以下的语法架构,并与之交互。

  • 集合
  • 属性访问
  • 迭代(包括使用async for的异步迭代)
  • 运算符重载
  • 函数和方法的调用
  • 字符串的展示和格式化
  • 使用关键字await的异步编程
  • 对象的创建和销毁
  • 管理器上下文(包括使用async with的异步上下文管理器)

MAGIC AND DUNDER

术语魔术方法是特殊方法的别名,但是我们如何表示像__getitem__这样的特殊方法?我从作家兼老师史蒂夫霍尔顿那里学会了说“dunder-getitem”。“Dunder”是“前后双下划线”的缩写。这就是为什么特殊方法也被称为dunder方法的原因。The Python Language Reference 的“Lexical Analysis” 一章警告说,“在任何上下文中,不遵循官方文档使用 __*__ 命名的特殊方法,都会带来麻烦“

本章更新的内容

本章与第一版相比几乎没有变化,因为它是对 Python 数据模型的介绍并且Python 数据模型非常稳定。最显着的变化是:

  • 支持异步编程和其他新特性的特殊方法,已经添加到表‘Overview of Special Methods’中
  • 图1-2展示了包括“Collection API”中特殊方法的使用,包括 Python 3.6 中引入的 collections.abc.Collection 抽象基类

在整个第二版中,我都采用了 Python 3.6 中引入的 f-string 语法,它比旧的字符串格式符号(str.format() 方法和 % 运算符)更具可读性,而且通常更方便。

TIP

仍然使用 my_fmt.format() 的一个原因是有的时候 my_fmt 的定义是在代码中不经常进行格式化操作的位置。例如,当 my_fmt 有多行并且在常量中定义时,或者当它需要读取配置文件或数据库。这些是真实需求,但不会经常发生。

一摞Python风格的卡牌

示例 1-1 很简单,但它展示了仅实现两个特殊方法 __getitem__ 和 __len__ 就具有的强大功能。

示例 1-1。扑克牌序列

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

首先要注意的是使用 collections.namedtuple 构造一个简单的类来表示单张卡牌。 我们使用 namedtuple 来构建卡牌对象的类,这些类通常只有几个属性,而且没有自定义方法,例如数据库记录。在示例中,我们使用它为扑克牌中的卡牌提供一个友好的表示,如控制台会话所示:

>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')

但是这个例子的重点是 FrenchDeck 类。它短小又精悍。首先,与任何标准 Python 集合一样,他跟任何标准python集合类型一样,可以用len函数查看一叠牌中卡牌的数量。

>>> deck = FrenchDeck()
>>> len(deck)
52

由于 __getitem__ 方法,从牌组中读取特定卡牌(例如第一张或最后一张)很容易: 

>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')

我们应该创建一种方法来随机抽取卡牌吗?不需要。 Python 已经内置一个从序列中获取随机项的函数:random.choice。我们可以在直接在Deck实例上使用: 

>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>> choice(deck)
Card(rank='2', suit='clubs')

我们刚刚看到了使用特殊方法来利用 Python 数据模型的两个优点:

  • 用户使用类时不必记住标准操作的各种各样的名称。 (“如何获取元素的数量?是 .size()、.length() 还是什么函数?”)
  • 可以更加方便的利用Python 丰富的标准库并避免重新发明轮子,例如 random.choice 函数。 

但是好戏还在后面。

因为我们的 __getitem__ 委托给 self._cards 的  []  运算符,我们的deck对象自动支持切片。以下是我们如何从全新的扑克牌中查看前三张牌,然后从索引12 开始找到所有的A,每次跳过 13 张牌:

>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

虽然只实现了 __getitem__ 特殊方法,我们的套牌也是可迭代的: 

>>> for card in deck:  # doctest: +ELLIPSIS
...   print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...

我们也可以反向迭代deck: 

>>> for card in reversed(deck):  # doctest: +ELLIPSIS
...   print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...

doctest 中的省略

我尽可能从 doctest 中提取本书中的 Python 控制台列表以确保准确性。当输出太长时,省略的部分用省略号 (...) 标记,就像前面代码的最后一行一样。在这种情况下,我使用 # doctest: +ELLIPSIS 指令使 doctest 通过。如果您在交互式控制台中尝试这些示例,则可以完全省略 doctest 注释。

迭代通常是隐式的。如果一个集合没有实现 __contains__ 方法,则 in 运算符会执行顺序扫描进行迭代。in可以在 FrenchDeck 类上使用就是很好的例子,因为类是可迭代的。看看这个:

>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False

那么排序呢?一个常见的扑克牌等级系统是按大小(A 最高),然后按黑桃(最高)、红桃、方块和梅花(最低)的顺序排列。这是一个根据该规则对扑克牌进行排序的函数,梅花 2 返回 0,黑桃 A 返回 51: 

suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

def spades_high(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]

使用 spades_high,我们现在可以按排名递增的顺序对扑克牌排序: 

>>> for card in sorted(deck, key=spades_high):  # doctest: +ELLIPSIS
...      print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (46 cards omitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

虽然 FrenchDeck 隐式继承自object类,但它的大部分功能都不是来源于继承,而是来自利用数据模型和组合。 通过实现特殊方法 __len__ 和 __getitem__,我们的 FrenchDeck 的行为就像是一个标准的 Python 序列,并允许从核心语言特性(例如,迭代和切片)和标准库中受益,正如使用 random.choice、reversed 和 sorted 的示例所示。由于使用了组合,__len__ 和 __getitem__ 的实现可以将所有工作委托给列表对象 self._cards。


如何洗牌

到目前为止,FrenchDeck 不能被洗牌,因为它是不可变的:每张扑克牌及其位置不能更改,除非违反封装并直接处理 _cards 属性。在第 13 章中,我们只需要添加一行 __setitem__ 方法就能解决这个问题。

如何使用特殊方法

关于特殊方法,首先要了解的是它们是由 Python 解释器调用的,而不是用户。用户不会直接写 my_object.__len__()。而是使用 len(my_object) ,如果 my_object 是用户定义类的实例,则 Python 会调用你自己实现的 __len__ 方法。

但是解释器在处理像 list、str、bytearray 这样的内置类型或像 NumPy 数组这样的扩展包时会走捷径。用 C 语言编写的 Python 可变大小的集合包括一个名为 PyVarObject 的struct,它有一个 ob_size 字段保存集合中的项的数量。因此,如果 my_object 是这些内置数据结构的实例,则 len(my_object) 会直接读取 ob_size 字段的值,这比调用方法要快得多。

通常,特殊方法调用是隐式的。例如,for i in x: 的语句实际上在背后调用了 iter(x) ,如果实现了x.__iter__(),就会调用 x.__iter__(),如果没有实现x.__iter__(),就会调用 x.__getitem__(),就像在 FrenchDeck 示例中所示。

通常,您的代码不应该经常直接调用特殊方法。除非做大量的元编程,我们只需要实现特殊方法,而不是显式调用它们。唯一经常被用户代码直接调用的特殊方法是 __init__ ,目的是在自己的 __init__ 实现中调用超类的构造器。

如果需要调用特殊方法,通常最好调用相关的内置函数(例如,len、iter、str 等)。这些内置函数调用相应的特殊方法,但通常提供额外的好处并且——对于内置类型——比方法调用更快。例如,参见第 17 章中的““Using iter with a Callable” 。

在接下来的部分中,我们将看到特殊方法的一些最重要的用途:

  • 模拟数字类型
  • 对象的字符串表示
  • 对象的布尔值
  • 实现集合

模拟数字类型

特殊方法可以使自定义对象通过加号‘+’(或者是别的运算符)进行运算。我们将在第 16 章中更详细地介绍这一点,但这里我们的目标是通过另一个简单的示例进一步说明特殊方法的使用。

我们将实现一个类来表示二维向量,就像数学和物理学中使用的欧几里得向量(见图 1-1)。


TIP

内置的complex类型可以用来表示二维向量,但是我们的类可以扩展来表示n维向量。我们将在第 17 章中介绍。

第一章 Python数据模型_第1张图片

 我们将通过编写一个模拟控制台会话来开始为这样的类设计 API,稍后我们可以将其用作 doctest。以下代码段测试图 1-1 中所示的向量加法:

>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

注意 + 运算符如何生成一个新的 Vector,并在控制台上以友好的格式显示。

abs 内置函数返回整数和浮点数的绝对值,以及复数的模,为了保持一致,我们的 API 也使用 abs 来计算向量的模:

>>> v = Vector(3, 4)
>>> abs(v)
5.0

我们还可以实现 * 运算符来执行标量乘法(即,将一个向量乘以一个数字以生成一个具有相同方向和相乘幅度的新向量):

>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0

示例 1-2 是 Vector 类的实现,通过使用特殊方法 __repr__、__abs__、__add__ 和 __mul__ 来实现刚才描述的操作。

示例 1-2 一个简单的二维向量类

"""
vector2d.py: a simplistic class demonstrating some special methods

It is simplistic for didactic reasons. It lacks proper error handling,
especially in the ``__add__`` and ``__mul__`` methods.

This example is greatly expanded later in the book.

Addition::

    >>> v1 = Vector(2, 4)
    >>> v2 = Vector(2, 1)
    >>> v1 + v2
    Vector(4, 5)

Absolute value::

    >>> v = Vector(3, 4)
    >>> abs(v)
    5.0

Scalar multiplication::

    >>> v * 3
    Vector(9, 12)
    >>> abs(v * 3)
    15.0

"""


import math

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)

除了熟悉的 __init__ 之外,我们还实现了五个特殊方法。请注意,它们都不是在类中直接调用的,也不是在 doctest 说明的类的典型用法中进行直接调用。如前所述,Python 解释器会进行大多数特殊方法调用。

示例 1-2 实现了两个运算符:+ 和 *用来演示__add__ 和 __mul__ 的基本用法。不管是 *还是 + 操作符 ,都会创建并返回一个新的 Vector 实例,并且不会修改任何一个操作数——self 或 other 仅仅在方法中被读取。这是中缀运算符的预期行为:创建新对象而不会修改操作数。我将在第 16 章对此进行更多说明。


WARNING

在实现时,示例 1-2 允许将向量乘以数字,但不能将数字乘以向量,这违反了标量乘法的交换性。我们将在第 16 章中使用特殊方法 __rmul__ 来解决这个问题。

在以下部分中,我们将讨论 Vector 中的其他特殊方法。

字符串表示

__repr__ 特殊方法由内置 repr 方法调用,来获得要检查的对象的字符串表示形式。如果没有自定义 __repr__,Python 的控制台会将 Vector 实例显示为 

交互式控制台和调试器在计算表达式的结果上调用 repr,就像带有 % 运算符的经典格式中的 %r 占位符一样,以及在 f-strings str.format 方法中使用的新的format string syntax中的 !r 转换字段。

请注意,我们的 __repr__ 中的 f 字符串使用 !r 来获取要显示的属性的标准表示。这是一个很好的做法,因为它能够体现 Vector(1, 2) 和 Vector('1', '2') 之间的关键区别-后者在本例的上下文中不起作用,因为构造函数的参数应该是数字类型,而不是 str。

__repr__ 返回的字符串应该是明确的,并且如果可能,__repr__ 的返回结果需要匹配所表示对象再次创建时的源代码。这就是为什么我们的 Vector 表示看起来像调用类的构造函数(例如 Vector(3, 4))。

相反, __str__ 由内置的 str() 调用,并由 print 函数隐式使用。str()应该返回适合显示给终端用户的字符串。

有时 __repr__ 返回的字符串是用户友好的,这时就不需要 __str__ 方法,因为从object类继承的实现会调用 __repr__ 作为替代计划。示例 5-2 是本书中带有自定义 __str__ 的几个示例之一。


TIP

具有 toString 方法的语言经验的程序员倾向于实现 __str__ 而不是 __repr__。如果您只在 Python 中实现这些特殊方法中的一个,请选择实现 __repr__。

“What is the difference between __str__ and __repr__ in Python?” 是一个 Stack Overflow 问题,Pythonistas Alex Martelli 和 Martijn Pieters 在这个问题上给出了精彩的回答。

自定义类型的布尔值

尽管 Python 具有 bool 类型,但它接受布尔上下文中的任何对象,例如 if 或 while控制表达式,或者作为 and、or 和 not 的操作数。为了确定值 x 是真还是假,Python会调用 bool(x),它返回 True 或 False。

默认情况下,用户定义类的实例被认为是真,除非实现了 __bool__ 或 __len__ 特殊方法。基本上, bool(x) 调用 x.__bool__() 并使用结果。如果 没有实现__bool__ ,Python 会尝试调用 x.__len__(),如果返回值是0,则 bool 返回 False。否则 bool 返回 True。

我们的 __bool__ 实现在概念上很简单:如果向量的模为零,返回 False,否则返回 True。我们使用 bool(abs(self)) 将向量的模转换为布尔值,因为 __bool__ 预期返回一个布尔值。__bool__ 方法以外的方法很少需要显式调用 bool(),因为任何对象都可以在布尔上下文中使用。

请注意特殊方法 __bool__ 如何允许您的对象遵循 Python 标准库文档的“内置类型”一章中定义的真值测试规则。


NOTE

Vector.__bool__ 的效率更高实现是这样的:

    def __bool__(self):
        return bool(self.x or self.y)

这样可读性差一些,但是不需要调用 abs、__abs__、平方和平方根的过程。显式的将结果转换为 bool是必须的,因为 __bool__ 会返回一个布尔值,or按原样返回任一操作数的真值:如果x为真,则x or y 计算结果为 x,否则结果为 y,无论y是真还是假。


集合API

图1-2展示了python语言基本集合类型的接口,图中的所有类型都是抽象基类(abstract base classes)。本部分目标是展示 Python 最重要的集合接口的全景图以及它们是如何从特殊方法构建的。

第一章 Python数据模型_第2张图片

从图上可以看到,每个顶端的抽象类都有一个特殊方法。Collection ABC(在python 3.6引入)统一集合需要实现的三个接口:

  • Iterable:支持for,拆包和其他类型的迭代
  • Sized: 支持内置方法 len
  • Container: 支持操作符 in

Python不需要具体的类去继承上面的抽象类,例如:任何实现__len__的类都可以满足Sized接口。

三个非常重要的集合类型:

  • Sequence:规范化内置接口,例如str,list
  • Mapping: 通常由dict和collections.defaultdict实现
  • Set: 内置类型set和frozenset的接口

其中,只有序列(Sequence)是可以被反转的(Reversible),因为序列支持随意改变内容的顺序,而字典和集合不支持。


Note:

在python3.7之后,dict类型是‘有序的’(只有键插入的顺序被保留),但是不能随意重新排列字典中的键。

Set ABC的特殊方法实现了一些中缀操作符,例如:a & b返回集合a和b的交集,&的背后是特殊方法__and__。

接下来的两章将详细介绍标准库序列、映射和集合。

现在让我们来看一下 Python 数据模型中定义的特殊方法的主要类别。

特殊方法总览

Python 语言参考的“Data Model” chapter 列出了 80 多个特殊方法名称。其中一半以上都是为了支持算术、位和比较运算符。表1-1展示了特殊方法名称,不包括用于实现中缀运算符或核心数学函数(如 abs)的部分,其中包括了最新版本新增的特殊方法:

  • 在python3.5中引入的异步特殊方法:__anext__ 
  • 在python3.6中引入的类自定义钩子:__init_subclass__

表1-1 特殊方法名称(操作符除外)

类别 方法名

字符串,字节序列表示形式

__repr____str____format____bytes____fspath__

数值转换

__abs____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__

表1-2中的特殊方法用来支持中缀和数学运算符。其中这里最新的名称是 __matmul__、__rmatmul__ 和 __imatmul__,它们是在 Python 3.5 中添加的,用于支持使用 @ 作为矩阵乘法的中缀运算符,

Operator category Symbols Method names

Unary numeric

- + abs()

__neg__ __pos__ __abs__

Rich comparison

< <= == != > >=

__lt__ __le__ __eq__ __ne__ __gt__ __ge__

Arithmetic

+ - * / // % @ divmod() round() ** pow()

__add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __matmul__ __divmod__ __round__ __pow__

Reversed arithmetic

(arithmetic operators with swapped operands)

__radd__ __rsub__ __rmul__ __rtruediv__ __rfloordiv__ __rmod__ __rmatmul__ __rdivmod__ __rpow__

Augmented assignment arithmetic

+= -= *= /= //= %= @= **=

__iadd__ __isub__ __imul__ __itruediv__ __ifloordiv__ __imod__ __imatmul__ __ipow__

Bitwise

& | ^ << >> ~

__and__ __or__ __xor__ __lshift__ __rshift__ __invert__

Reversed bitwise

(bitwise operators with swapped operands)

__rand__ __ror__ __rxor__ __rlshift__ __rrshift__

Augmented assignment bitwise

&= |= ^= <<= >>=

__iand__ __ior__ __ixor__ __ilshift__ __irshift__


NOTE

当第一个操作数上的相应特殊方法无法使用时,Python 会在第二个操作数上调用反向运算符特殊方法。增量赋值是将中缀运算符与变量赋值相结合的快捷方式,例如 a += b。 

第 16 章详细解释了反向运算符和增量赋值。

为什么 len 不是普通方法

我在 2013 年向核心开发人员 Raymond Hettinger 提出了这个问题,他回答的关键是引用了《Python 之禅》中的一句话:“实用胜过纯粹”。在“如何使用特殊方法”中,我描述了当 x 是内置类型的实例时 len(x) 如何运行得非常快。CPython 的内置对象不会调用任何方法:长度只是从 C 结构中的字段中读取。获取集合长度是一种常见的操作,并且必须有效地处理诸如 str、list、memoryview 等基本和多样化的类型。

换句话说, len 之所以不是一个普通方法,是为了让它作为 Python 数据模型的一部分得到了特殊处理,就像 abs 一样。 但是由于特殊方法__len__,我们可以在自定义对象上使用len。这是对高效内置对象的需求和语言的一致性之间做了平衡。同样来自《Python之禅》:“特殊情况并不足以打破规则。”


NOTE

如果您将 abs 和 len 视为一元运算符,那么您可能更倾向于原谅它们的功能外观----虽然看起来像面向对象语言中的函数,但实际上又不是函数。事实上,ABC 语言——Python 始祖,开创了它的许多特性——有一个相当于 len 的 # 运算符(你可以写成 #s)。当用作中缀运算符时,写为 x#s,此时它会计算 x 在 s 中的出现次数,在 Python 中,对于任何序列 s,对应的写法是s.count(x)。

章节总结

通过实现特殊方法,自定义对象可以像内置类型一样运行,从而实现社区认为 Pythonic 的富有表现力的编码风格。

Python 对象的一个​​基本要求是提供其自身的可用字符串表示,一个用于调试和日志记录,另一个用于向终端用户展示。这就是数据模型中存在特殊方法 __repr__ 和 __str__ 的原因。

由于运算符重载,Python 提供了丰富的数字类型选择,从内置到 decimal.Decimal 和 fractions.Fraction,都支持中缀算术运算符。NumPy 数据科学库支持带有矩阵和张量的中缀运算符。第 16 章将通过对 Vector 示例的增强来展示实现运算符(包括反向运算符和增强赋值)。

本书涵盖了 Python 数据模型的大部分剩余特殊方法的使用和实现。

你可能感兴趣的:(流畅的python第二版学习,python)