Python 数据模型

Python 风格的关键完全体现在 Python 的数据模型上,数据模型所描述的 API ,为使用最地道的语言特性来构建开发者自己的对象提供了工具。

当 Python 解析器遇到特殊句法时,会使用特殊方法去激活一些基本的对象操作。特殊方法以双下划线开头,以双下划线结尾。如:obj[key] 的背后就是 __getitem__ 方法。魔术方法是特殊方法的昵称,特殊方法也叫双下方法。

一. 一摞 Python 风格的纸牌

使用 __getitem____len__ 创建一摞有序的纸牌:

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])
 
class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    # ♠, ♡, ♣, ♢,
    suits = ['\u2660', '\u2661', '\u2663', '\u2662']
 
    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, index):
        return self._cards[index]

上面的例子,使用 collections.namedtuple 构建了一个简单的类来表示一张纸牌,namedtuple 用以构建只有少数属性但没有方法的类。

演示 1

我们自定义的 FrenchDeck 类可以像任何 python 标准集合类型一样使用 len() 函数,查看一叠牌有多少张:

>> d = FrenchDeck()
>> len(d)
52

演示 2

也可以像列表一样,使用位置索引, d[i] 将调用 __getitem__ 方法:

>> d[0]
Card(rank='2', suit='♠')
>> d[1]
Card(rank='3', suit='♠')
>> d[-1]
Card(rank='A', suit='♢')

演示 3

也可以使用标准库模块提供的 random.choice 方法,从序列中随机选取一个元素。下面,我们如随机取出一张纸牌:

>> import random
>> random.choice(d)
Card(rank='J', suit='♡')
>> random.choice(d)
Card(rank='J', suit='♠')

现在我们已经体会到通过 python 特殊方法,来使用 Python 数据模型的 2 个好处:

  • 作为类的用户,无需去记住标准操作的各种名词,如获取长度是 .size ,还是 .length,还是别的什么...
  • 可以更加方便地利用python的标准库,如 random.choice 函数。

演示 4

因为 __getitem__ 方法把 [] 操作交给了 self.cards 列表,所以我们的 FrenchDeck 实例自动支持切片:

>> d[:4]
[Card(rank='2', suit='♠'),
 Card(rank='3', suit='♠'),
 Card(rank='4', suit='♠'),
 Card(rank='5', suit='♠')]
>> d[-4:]
[Card(rank='J', suit='♢'),
 Card(rank='Q', suit='♢'),
 Card(rank='K', suit='♢'),
 Card(rank='A', suit='♢')]

演示 5

仅仅实现了 __getitem__ 方法,这一摞牌即变得可迭代:

for card in d:
    print(card.suit + card.rank, end=',')
    if card.rank == 'A':
        print()

运行结果:

一摞 Python 风格的纸牌

也可以直接调用内置的 reversed 函数,反向迭代 FrenchDeck 实例:

for card in reversed(d):
    print(card.suit + card.rank, end=',')
    if card.rank == '2':
        print()

运行结果:

反向迭代纸牌

演示 6

迭代通常是隐式的,比如一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序做一次迭代搜索。

因此,in 运算符可以用在我们的 FrenchDeck 实例上,因为它是可迭代的:

>> Card(rank='7', suit='♡') in d
True
>> Card(rank='20', suit='♠') in d
False

演示 7

FrenchDeck 还可以使用 Python 标准库中的 sorted 函数,实现排序:

首先定义一个排序依据的函数:

# 定义花色由大到小的顺序为:♠ ♡ ♢ ♣
SUIT_VALUES = {'\u2660': 3, '\u2661': 2, '\u2662': 1, '\u2663': 0}

def sort_rank(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * 10 + SUIT_VALUES[card.suit]

def sort_suit(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return SUIT_VALUES[card.suit] * 100 + rank_value

优先按 rank 的大小排序,rank 相同时则比较 suit 的值:

for card in sorted(d, key=sort_rank):
    print(card.suit + card.rank, end=',')
    if card.suit == '\u2660':
        print()

运行结果:

优先按 suit 的大小排序,suit 相同时则比较 rank 的值:

for card in sorted(d, key=sort_suit):
    print(card.suit + card.rank, end=',')
    if card.rank == 'A':
        print()

运行结果:

总结:虽然 FrenchDeck 隐式地继承了 object 类,但功能却不是继承而来的。通过实现 __len____getitem__ 这两个特殊方法,使 FrenchDeck 类就跟一个 Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性,如迭代和切片)。同时这个类还可用于标准库中诸如:random.choicereversedsorted 这些函数。

演示 8

按照目前的设计,FrenchDeck 还不支持洗牌,因为它是不可变的:

>> random.shuffle(d)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
...
TypeError: 'FrenchDeck' object does not support item assignment

shuffle 函数要调换集合中元素的位置,而 FrenchDeck 只实现了不可变的序列协议,可变的序列还必须提供 __setitem__ 方法:

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = ['\u2660', '\u2661', '\u2663', '\u2662']
    ...
    
    def __setitem__(self, index, card):
        self._cards[index] = card

洗牌:

>> random.shuffle(d)
>> 

没有任何的返回值,可见 random.shuffle 就地修改了可变序列 d 。为便于观察结果,我们定义输入的输出函数:

def print_cards(cards):
    for i, card in enumerate(cards):
        print(card.suit + card.rank, end=',')
        if i != 0 and (i+1) % 13 == 0:
            print()

运行结果:

print_cards(d)

每次洗牌,都是一个随机的序列:

print_cards(d)

二. 自定义一个二维向量类

首先明确一点,特殊方法的存在是为了被 Python 解析器调用的,例如:我们不会使用 obj.__len__() 这种写法,而是 len(obj)。在执行 len(obj) 时,如果 obj 是一个自定义类的对象,那么 Python 会自己去调用我们实现的 __len__ 方法。

对于 Python 内置的数据类型,比如列表、字符串、字节序列等,那么 CPython 会抄个近路,__len__ 实际上会返回 PyVarObject 里的 ob_size 属性,这是因为直接读取属性比调用一个方法要快得多。

很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句其实是调用 iter(x),而这个函数的背后是 x.__iter__() 方法。

通过内置函数如来使用特殊方法是最好的选择。这些内置函数不仅会调用这些方法,通常还提供额外的好处,对于内置类型来说,它们的速度更快。

下面,我们通过定义一个简单的二维向量类,再来体会一下 Python 特殊方法的美妙:

from math import hypot
 
class Vector:
    """自定义二维向量"""
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
 
    def __repr__(self):
        return f'Vector({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, scaler):
        return Vector(self.x * scaler, self.y * scaler)

使用 Vector 类,就像使用 Python 内置的数据类型一样简单:

>> v1 = Vector(2, 4)
>> v2 = Vector(2, 1)
>> v1 + v2
Vector(4,5)
>> v3 = Vector(3,4)
>> abs(v3)
5.0
>> bool(v3)
True
>> v3 * 100
Vector(300,400)

三. Python 特殊方法一览

跟运算符无关的特殊方法

类别 方法名
字符串/字节序列表示形式 __repr____str____format____bytes__
数值转换 __abs____bool____complex____int____float____hash____index__
集合模拟 __len____getitem____setitem____delitem____contains__
迭代枚举 __iter____reversed____next__
可调用模式 __call__
上下文管理 __enter____exit__
实例的创建和销毁 __nex____init____del__
属性管理 __getattr____getattribute____setattr____delattr____dir__
属性描述符 __get____set____delete__
跟类相关的服务 __prepare_____instancecheck____subclasscheck__

跟运算符相关的特殊方法

类别 方法名和对应的运算符
一元运算符 __neg__ -__pos__ +__abs__ abs()
比较运算符 __lt__ <__le__ <=__eq__ ==__ne__ !=__gt__ >__ge__ >=
算数运算符 __add__ +__sub__ -__mul__ *__truediv__ /__floordiv__ //__mod__ %__divmod__ divmode()__pow__ ** / pow()__round__ round()
反向算数运算符 __radd____rsub____rmul____rtruediv____rfloordiv____rmod____rdivmod__
增量赋值算术运算符 __iadd____isub____imul____itruediv____ifloordiv____imod____ipow__
位运算符 __invert__ ~__lshift__ <<__rshift__ >>__and__ &__or__ |__xor__ ^
反向位运算符 __rlshift____rrshift____rand____ror____rxor__
增量赋值位运算符 __ilshift____irshift____iand____ior____ixor__

你可能感兴趣的:(Python 数据模型)