目录

《Fluent Python》1:Python对象模型

前言

Python的一致性(consistency)是很好的,这意味着如果你可以依据所熟悉的特性来类比新特性。

对象模型(Data Model)是构建Python大厦的框架,指包括了sequences,iterators,函数,类等在内的Python组织方式和操作方式。当你在实现自己的对象时,熟悉对象模型可以帮你简化开发,提升效率。

Pythonic?

通常在一个语言后面加ic表示符合这种语言的习惯的做法,可以理解为入乡随俗——如果你不这么做,不一定是错的,但是会让熟悉这门语言的人感到不舒服。比如我们说英式英语,在口音纯正的英国人听来就觉得很奇怪,发音不标准,正如很多歪果仁说中文一样,虽然没错,但总感觉音调之类不对劲。那么Pythonic就好像是你说Python这门语言所应该知道的一些惯例。(暂时没想到更好的表述)

Python的类通常有一些特殊方法,以双下划线开头结尾,它们由某些操作符触发,由语言本身隐式调用,可以类比C++的重载,比如a[x]会触发a.__getitem__()方法,当然也可以显式调用,不过为什么不用更方便和优雅的隐式调用呢?再比如a+b会调用a.__add__()等,这些就是所谓Pythonic的一部分。

例子:一个卡牌类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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]

正如上文所说,接下来我们就可以用下标来访问FrenchDeck类的成员,而不需要自己想一个名字,对使用者造成不必要的困扰(我应该用size()还是length()去获取它的长度呢?)

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

更大的优势在于,一旦你遵循Python对象模型标准去设计你的类,就可以享受标准库提供的基于这些API的强大的众多API,比如只要实现了__getitem__(),就可以直接享受random.choise的红利而不需要自己实现。

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

另外,实现了__len()___getitem__(),FrenchDeck就是可迭代的了,意味着我们可以在for中使用它,以及reserved(deck)(反序循环),sorted(deck)(不过需要自己提供一个排序函数)。

__方法__如何被调用

正如前文所说,特殊方法是由Python解释器负责调用的,并不需要你手动调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

重点讲几个方法,第一个__rper__(),这个是在交互式终端和调试器中,Python用来展示你的类的对象的方式,比如下面就会输出v1+v2的表示。

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

类似地,__str__()方法是当这个类的对象被用于构造字符串类型时用的,另外会隐式地被print函数所调用,注意这两个函数还是有区别的。

__bool__()方法。默认地,用户自定义类的对象为true,但如果实现了__len__()__bool__()方法,解释器就会通过这些方法去判断。优先级是__bool__() > __len__()。当你显示地调用bool(x)时,解释器会隐式调用__bool__()方法。

特殊方法总结

分两类,操作符和非操作符类的。

/images/image-20220517235949060.png

/images/image-20220518000001627.png

另,一个细节是当你调用len(x),其实并不会invoke*(这个单词经常用,感觉挺正宗的,意思是调用、触发)*__len__()方法,这是出于效率的考虑。Python之禅讲到:Practically beats puricy,len方法为了效率,在CPython的实现中是直接从表示当前对象的结构体中读取的,为了实用性牺牲了纯粹性。