第 1 章 Python 数据模型

第 1 章 Python 数据模型

Guido 对语言设计美学的深入理解让人震惊。我认识不少很不错的编程语言设计者,他们设计出来的东西确实很精彩,但是从来都不会有用户。Guido 知道如何在理论上做出一定妥协,设计出来的语言让使用者觉得如沐春风,这真是不可多得。1

——Jim Hugunin
Jython 的作者,AspectJ 的作者之一,.NET DLR 架构师

1摘自“Story of Jython”,这是 Jython Essentials(Samuele Pedroni 和 Noel Rappin 著,O'Reilly 出版社,2002 年)一书的序。

Python 最好的品质之一是一致性。当你使用 Python 工作一会儿后,就会开始理解 Python 语言,并能正确猜测出对你来说全新的语言特征。

然而,如果你带着来自其他面向对象语言的经验进入 Python 的世界,会对 len(collection) 而不是 collection.len() 写法觉得不适。当你进一步理解这种不适感背后的原因之后,会发现这个原因,和它所代表的庞大的设计思想,是形成我们通常说的“Python 风格”(Pythonic)的关键。这种设计思想完全体现在 Python 的数据模型上,而数据模型所描述的 API,为使用最地道的语言特性来构建你自己的对象提供了工具。

数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。

不管在哪种框架下写程序,都会花费大量时间去实现那些会被框架本身调用的方法, Python 也不例外。Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开头,以两个下划线结尾(例如 __getitem__)。比如 obj[key] 的背后就是 __getitem__ 方法,为了能求得 my_collection[key] 的值,解释器实际上会调用 my_collection.__getitem__(key)

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

  • 迭代

  • 集合类

  • 属性访问

  • 运算符重载

  • 函数和方法的调用

  • 对象的创建和销毁

  • 字符串表示形式和格式化

  • 管理上下文(即 with 块)

 magic 和 dunder

魔术方法(magic method)是特殊方法的昵称。有些 Python 开发者在提到 __getitem__ 这个特殊方法的时候,会用诸如“下划线-下划线- getitem”2 这种说法,但是显然这种说法会引起歧义,因为像 __x 这种命名在 Python 里还有其他含义,3 但是如果完整地说出“下划线-下划线- getitem -下划线-下划线”,又会很麻烦。于是我跟着 Steve Holden,一位技术书作者和老师,学会了“双下- getitem”(dunder-getitem)这种说法。于是乎,特殊方法也叫双下方法(dunder method)。4

2即 under-under-getitem 的直译。——译者注

3注 3:详见 9.7 节。

4我是从 Steve Holden 那里第一次听说 dunder 这个说法的。根据维基百科的解释,Mark Jackson 和 Time Hochberg 是最早在书写中开始使用这个词的人。那是 2002 年 9 月 26 日,他们两人在邮件列表里回复“__(双下划线)怎么念?”这个问题时提到了 dunder,最先回复的是 Jackson,11 分钟后 Hochberg 也回复了。

1.1 一摞Python风格的纸牌

接下来我会用一个非常简单的例子来展示如何实现 __getitem____len__ 这两个特殊方法,通过这个例子我们也能见识到特殊方法的强大。

示例 1-1 里的代码建立了一个纸牌类。

示例 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 构建了一个简单的类来表示一张纸牌。自 Python 2.6 开始,namedtuple 就加入到 Python 里,用以构建只有少数属性但是没有方法的对象,比如数据库条目。如下面这个控制台会话所示,利用 namedtuple,我们可以很轻松地得到一个纸牌对象:

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

当然,我们这个例子主要还是关注 FrenchDeck 这个类,它既短小又精悍。首先,它跟任何标准 Python 集合类型一样,可以用 len() 函数来查看一叠牌有多少张:

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

从一叠牌中抽取特定的一张纸牌,比如说第一张或最后一张,是很容易的:deck[0]deck[-1]。这都是由 __getitem__ 方法提供的:

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

我们需要单独写一个方法用来随机抽取一张纸牌吗?没必要,Python 已经内置了从一个序列中随机选出一个元素的函数 random.choice,我们直接把它用在这一摞纸牌实例上就好:

>>> 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 类自动支持切片(slicing)操作。下面列出了查看一摞牌最上面 3 张和只看牌面是 A 的牌的操作。其中第二种操作的具体方法是,先抽出索引是 12 的那张牌,然后每隔 13 张牌拿 1 张:

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

反向迭代也没关系:

>>> 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 中的省略

为了尽可能保证书中的 Python 控制台会话内容的正确性,这些内容都是直接从 doctest 里摘录的。在测试中,如果可能的输出过长的话,那么过长的内容就会被如上面例子的最后一行的省略号(...)所替代。此时就需要 #doctest: +ELLIPSIS 这个指令来保证 doctest 能够通过。要是你自己照着书中例子在控制台中敲代码,可以略过这一指令。

迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序做一次迭代搜索。于是,in 运算符可以用在我们的 FrenchDeck 类上,因为它是可迭代的:

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

那么排序呢?我们按照常规,用点数来判定扑克牌的大小,2 最小、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 ommitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

虽然 FrenchDeck 隐式地继承了 object 类,5 但功能却不是继承而来的。我们通过数据模型和一些合成来实现这些功能。通过实现 __len____getitem__ 这两个特殊方法,FrenchDeck 就跟一个 Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭代和切片)。同时这个类还可以用于标准库中诸如 random.choicereversedsorted 这些函数。另外,对合成的运用使得 __len____getitem__ 的具体实现可以代理给 self._cards 这个 Python 列表(即 list 对象)。

5在 Python 2 中,对 object 的继承需要显式地写为 FrenchDeck(object);而在 Python 3 中,这个继承关系是默认的。

 如何洗牌

按照目前的设计,FrenchDeck 是不能洗牌的,因为这摞牌是不可变的(immutable):卡牌和它们的位置都是固定的,除非我们破坏这个类的封装性,直接对 _cards 进行操作。第 11 章会讲到,其实只需要一行代码来实现 __setitem__ 方法,洗牌功能就不是问题了。

1.2 如何使用特殊方法

首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调用其中由你实现的 __len__ 方法。

然而如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构体。直接读取这个值比调用一个方法要快很多。

很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。

通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现它们的次数。唯一的例外可能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自己的子类的 __init__ 方法中调用超类的构造器。

通过内置的函数(例如 leniterstr,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。14.12 节中有详细的例子。

不要自己想当然地随意添加特殊方法,比如 __foo__ 之类的,因为虽然现在这个名字没有被 Python 内部使用,以后就不一定了。

1.2.1 模拟数值类型

利用特殊方法,可以让自定义对象通过加号“+”(或是别的运算符)进行运算。第 13 章对此有详细的介绍,现在只是借用这个例子来展示特殊方法的使用。

我们来实现一个二维向量(vector)类,这里的向量就是欧几里得几何中常用的概念,常在数学和物理中使用的那个(见图 1-1)。

图 1-1:一个二维向量加法的例子,Vector(2,4) + Vextor(2,1) = Vector(4,5)

 Python 内置的 complex 类可以用来表示二维向量,但我们这个自定义的类可以扩展到 n 维向量,详见第 14 章。

为了给这个类设计 API,我们先写个模拟的控制台会话来做 doctest。下面这一段代码就是图 1-1 所示的向量加法:

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

注意其中的 + 运算符所得到的结果也是一个向量,而且结果能被控制台友好地打印出来。

abs 是一个内置函数,如果输入是整数或者浮点数,它返回的是输入值的绝对值;如果输入是复数(complex number),那么返回这个复数的模。为了保持一致性,我们的 API 在碰到 abs 函数的时候,也应该返回该向量的模:

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

我们还可以利用 * 运算符来实现向量的标量乘法(即向量与数的乘法,得到的结果向量的方向与原向量一致 6,模变大):

6如果向量与负数相乘,得到的结果向量的方向与原向量相反。——编者注

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

示例 1-2 包含了一个 Vector 类的实现,上面提到的操作在代码里是用这些特殊方法实现的:__repr____abs____add____mul__

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

from math import hypot

class Vector:

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

    def __repr__(self):
        return 'Vector(%r, %r)' % (self.x, self.y)

    def __abs__(self):
        return 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)

虽然代码里有 6 个特殊方法,但这些方法(除了 __init__)并不会在这个类自身的代码中使用。即便其他程序要使用这个类的这些方法,也不会直接调用它们,就像我们在上面的控制台对话中看到的。上文也提到过,一般只有 Python 的解释器会频繁地直接调用这些方法。接下来看看每个特殊方法的实现。

1.2.2 字符串表示形式

Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector object at 0x10e100070>

交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;在老的使用 % 符号的字符串格式中,这个函数返回的结果用来代替 %r 所代表的对象;同样,str.format 函数所用到的新式字符串格式化语法也是利用了 repr,才把 !r 字段变成字符串。

 %str.format 这两种格式化字符串的手段在本书中都会使用。其实整个 Python 社区都在同时使用这两种方法。个人来讲,我越来越喜欢 str.format 了,但是 Python 程序员更喜欢简单的 %。因此,这两种形式并存的情况还会持续下去。

__repr__ 的实现中,我们用到了 %r 来获取对象各个属性的标准字符串表示形式——这是个好习惯,它暗示了一个关键:Vector(1, 2)Vector('1', '2') 是不一样的,后者在我们的定义中会报错,因为向量对象的构造函数只接受数值,不接受字符串 7

7实际上,Vector 的构造函数接受字符串。而且,对于使用字符串构造的 Vector,这 6 个特殊方法中,只有 __abs____bool__ 会报错。此外,1.2.4 节定义的 __bool__ 不会报错。——编者注

__repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象。因此这里使用了类似调用对象构造器的表达形式(比如 Vector(3, 4) 就是个例子)。

__repr____str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。

如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。

 “Difference between __str__ and __repr__ in Python”是 Stack Overflow 上的一个问题,Python 程序员 Alex Martelli 和 Martijn Pieters 的回答很精彩。

1.2.3 算术运算符

通过 __add____mul__,示例 1-2 为向量类带来了 +* 这两个算术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(selfother)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。第 13 章会谈到更多这方面的问题。

 示例 1-2 只实现了数字做乘数、向量做被乘数的运算,乘法的交换律则被忽略了。在第 13 章里,我们将利用 __rmul__ 解决这个问题。

1.2.4 自定义的布尔值

尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如 ifwhile 语句,或者 andornot 运算符)。为了判定一个值 x还是为,Python 会调用 bool(x),这个函数只能返回 True 或者 False

默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对 __bool__ 或者 __len__ 函数有自己的实现。bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则返回 True

我们对 __bool__ 的实现很简单,如果一个向量的模是 0,那么就返回 False,其他情况则返回 True。因为 __bool__ 函数的返回类型应该是布尔型,所以我们通过 bool(abs(self)) 把模值变成了布尔值。

在 Python 标准库的文档中,有一节叫作“Built-in Types”,其中规定了真值检验的标准。通过实现 __bool__,你定义的对象就可以与这个标准保持一致。

如果想让 Vector.__bool__ 更高效,可以采用这种实现:

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

它不那么易读,却能省掉从 abs__abs__ 到平方再到平方根这些中间步骤。通过 bool 把返回类型显式转换为布尔值是为了符合 __bool__ 对返回值的规定,因为 or 运算符可能会返回 x 或者 y 本身的值:若 x 的值等价于,则 or 返回 x 的值;否则返回 y 的值。

1.3 特殊方法一览

Python 语言参考手册中的“Data Model”一章列出了 83 个特殊方法的名字,其中 47 个用于实现算术运算、位运算和比较操作。

表 1-1 和表 1-2 列出了这些方法的概况。

 这些表并没有完全按照官方文档分组。

表1-1:跟运算符无关的特殊方法

类别

方法名

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

__repr____str____format____bytes__

数值转换

__abs____bool____complex____int____float____hash____index__

集合模拟

__len____getitem____setitem____delitem____contains__

迭代枚举

__iter____reversed____next__

可调用模拟

__call__

上下文管理

__enter____exit__

实例创建和销毁

__new____init____del__

属性管理

__getattr____getattribute____setattr____delattr____dir__

属性描述符

__get____set____delete__

跟类相关的服务

__prepare____instancecheck____subclasscheck__

表1-2:跟运算符相关的特殊方法

类别

方法名和对应的运算符

一元运算符

__neg__ -__pos__ +__abs__ abs()

众多比较运算符

__lt__ <__le__ <=__eq__ ==__ne__ !=__gt__ >__ge__ >=

算术运算符

__add__ +__sub__ -__mul__ *__truediv__ /__floordiv__ //__mod__ %__divmod__ divmod()__pow__ **pow()__round__ round()

反向算术运算符

__radd____rsub____rmul____rtruediv____rfloordiv____rmod____rdivmod____rpow__

增量赋值算术运算符

__iadd____isub____imul____itruediv____ifloordiv____imod____ipow__

位运算符

__invert__ ~__lshift__ <<__rshift__ >>__and__ &__or__ |__xor__ ^

反向位运算符

__rlshift____rrshift____rand____rxor____ror__

增量赋值位运算符

__ilshift____irshift____iand____ixor____ior__

 当交换两个操作数的位置时,就会调用反向运算符(b * a 而不是 a * b)。增量赋值运算符则是一种把中缀运算符变成赋值运算的捷径(a = a * b 就变成了 a *= b)。第 13 章会对这两者作出详细解释。

1.4 为什么len不是普通方法

我在 2013 年问核心开发者 Raymond Hettinger 这个问题时,他用“Python 之禅”里的原话回答了我:“实用胜于纯粹。”在 1.2 节里我提到过,如果 x 是一个内置类型的实例,那么 len(x) 的速度会非常快。背后的原因是 CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。获取一个集合中元素的数量是一个很常见的操作,在 strlistmemoryview 等类型上,这个操作必须高效。

换句话说,len 之所以不是一个普通方法,是为了让 Python 自带的数据结构可以走后门,abs 也是同理。但是多亏了它是特殊方法,我们也可以把 len 用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”

 如果把 abslen 都看作一元运算符的话,你也许更能接受它们——虽然看起来像面向对象语言中的函数,但实际上又不是函数。有一门叫作 ABC 的语言是 Python 的直系祖先,它内置了一个 # 运算符,当你写出 #s 的时候,它的作用跟 len 一样。如果写成 x#s 这样的中缀运算符的话,那么它的作用是计算 sx 出现的次数。在 Python 里对应的写法是 s.count(x)。注意这里的 s 是一个序列类型。

1.5 本章小结

通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具 Python 风格的代码。

Python 对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过 __repr____str__ 来满足这个要求。前者方便我们调试和记录日志,后者则是给终端用户看的。这就是数据模型中存在特殊方法 __repr____str__ 的原因。

对序列数据类型的模拟是特殊方法用得最多的地方,这一点在 FrenchDeck 类的示例中有所展现。在第 2 章中,我们会着重介绍序列数据类型,然后在第 10 章中,我们会把 Vector 类扩展成一个多维的数据类型,通过这个练习你将有机会实现自定义的序列。

Python 通过运算符重载这一模式提供了丰富的数值类型,除了内置的那些之外,还有 decimal.Decimalfractions.Fraction。这些数据类型都支持中缀算术运算符。在第 13 章中,我们还会通过对 Vector 类的扩展来学习如何实现这些运算符,当然还会提到如何让运算符满足交换律和增强赋值。

Python 数据模型的特殊方法还有很多,本书会涵盖其中的绝大部分,探讨如何使用和实现它们。

1.6 延伸阅读

对本章内容和本书主题来说,Python 语言参考手册里的“Data Model”一章(<>)是最符合规范的知识来源。

Alex Martelli 的《Python 技术手册(第 2 版)》对数据模型的讲解很精彩。我写这本书的时候,《Python 技术手册》的最新版本是 2006 年出版的,书里用的还是 Python 2.5,但是 Python 关于数据模型的概念并没有太大的变化,而书中 Martelli 对属性访问机制的描述,应该是除了 CPython 中的 C 源码之外在这方面最权威的解释。Martelli 还是 Stack Overflow 上的高产贡献者,在他名下差不多有 5000 条答案,你也可以去他的 Stack Overflow 主页上看看。

David Beazley 著有两本基于 Python 3 的书,其中对数据模型进行了详尽的介绍。一本是《Python 参考手册(第 4 版)》8,另一本是与 Brian K. Jones 合著的《Python Cookbook(第 3 版)中文版》。

8该书已由人民邮电出版社出版,书号:978-7-115-24259-4。——编者注

由 Gregor Kiczales、Jim des Rivieres 和 Daniel G. Bobrow 合著的 The Art of the Metaobject Protocol(又称 AMOP,MIT 出版社,1991 年)一书解释了元对象协议(metaobject protocol,MOP)的概念,而 Python 数据模型便是对这一概念的一种阐释。

杂谈

数据模型还是对象模型

Python 文档里总是用“Python 数据模型”这种说法,而大多数作者提到这个概念的时候会说“Python 对象模型”。Alex Martelli 的《Python 技术手册(第 2 版)》和 David Beazley 的《Python 参考手册(第 4 版)》是这个领域中最好的两本书,但是他们也总说“Python 对象模型”。维基百科中对象模型的第一个定义是:计算机编程语言中对象的属性。这正好是“Python 数据模型”所要描述的概念。我在本书中一直都会用“数据模型”这个词,首先是因为在 Python 文档里对这个词有偏爱,另外一个原因是 Python 语言参考手册中与这里讨论的内容最相关的一章的标题就是“数据模型”。

魔术方法

在 Ruby 中也有类似“特殊方法”的概念,但是 Ruby 社区称之为“魔术方法”,而实际上 Python 社区里也有不少人用的是后者。而我恰恰认为“特殊方法”是“魔术方法”的对立面。Python 和 Ruby 都利用了这个概念来提供丰富的元对象协议,这不是魔术,而是让语言的用户和核心开发者拥有并使用同样的工具。

考虑一下 JavaScript,情况就正好反过来了。JavaScript 中的对象有不透明的魔术般的特性,而你无法在自定义的对象中模拟这些行为。比如在 JavaScript 1.8.5 中,用户的自定义对象不能有只读属性,然而不少 JavaScript 的内置对象却可以有。因此在 JavaScript 中,只读属性是“魔术”般的存在,对于普通的 JavaScript 用户而言,它就像超能力一样。2009 年推出的 ECMAScript 5.1 才让用户可以定义只读属性。JavaScript 中跟元对象协议有关的部分一直在进化,但由于历史原因,这方面它还是赶不上 Python 和 Ruby。

元对象

The Art of the Metaobject ProtocalAMOP)是我最喜欢的计算机图书的标题。客观来说,元对象协议这个词对我们学习Python 数据模型是有帮助的。元对象所指的是那些对建构语言本身来讲很重要的对象,以此为前提,协议也可以看作接口。也就是说,元对象协议是对象模型的同义词,它们的意思都是构建核心语言的 API。

一套丰富的元对象协议能让我们对语言进行扩展,让它支持新的编程范式。AMOP 的第一作者 Gregor Kiczales 后来成为面向方面编程的先驱,他写出了一个 Java 扩展叫 AspectJ,用来实现他对面向方面编程的理念。其实在 Python 这样的动态语言里,更容易实现面向方面编程。现在已经有几个 Python 框架在做这件事情了,其中最重要的是 zope.interfacehttp://docs.zope.org/zope.interface/)。第11 章的延伸阅读里会谈到它。

目录

  • 版权声明
  • O'Reilly Media, Inc. 介绍
  • 献词
  • 前言
  • 第一部分 序幕
  • 第 1 章 Python 数据模型
  • 第二部分 数据结构
  • 第 2 章 序列构成的数组
  • 第 3 章 字典和集合
  • 第 4 章 文本和字节序列
  • 第三部分 把函数视作对象
  • 第 5 章 一等函数
  • 第 6 章 使用一等函数实现设计模式
  • 第 7 章 函数装饰器和闭包
  • 第四部分 面向对象惯用法
  • 第 8 章 对象引用、可变性和垃圾回收
  • 第 9 章 符合 Python 风格的对象
  • 第 10 章 序列的修改、散列和切片
  • 第 11 章 接口:从协议到抽象基类
  • 第 12 章 继承的优缺点
  • 第 13 章 正确重载运算符
  • 第五部分 控制流程
  • 第 14 章 可迭代的对象、迭代器和生成器
  • 第 15 章 上下文管理器和 else 块
  • 第 16 章 协程
  • 第 17 章 使用future处理并发
  • 第 18 章 使用 asyncio 包处理并发
  • 第六部分 元编程
  • 第 19 章 动态属性和特性
  • 第 20 章 属性描述符
  • 第 21 章 类元编程
  • 结语
  • 附录 A 辅助脚本
  • Python 术语表
  • 作者简介
  • 关于封面