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__
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容