1. 数据模型
python最好的品质之一是一致性,当你熟悉了python之后遇到一个新的模块你总是可以快速的理解它,这便是得益于其一致性,任何对象都平等一致没有"魔法".
如果你用惯了典型的面向对象语言如java这种,初看python的代码会很不习惯.比如希望知道一个列表的长度,符合面向对象语言的查看方式是collection.len()
而在python中很奇怪确是len(collection)
.更奇怪的是无论是列表,字典,集合还是什么,取长度都是len(object)
这是一种设计思想上的差别,python中万物都是对象,但python却不是纯粹的面向对象语言.所谓的pythonic
的关键也在于此.这种设计思想完全体现在python的数据模型上,而python数据模型的通用API也为用户自己构建符合python语言特性的对象提供了工具.
python的数据模型与其说是模型不如说是语言框架描述,它规范了一套语言自身的交互接口,只要符合这些接口,对象就可以与语言框架与其他符合接口的对象相互交互.正是因为python的一致性,使用python语言不会让你觉得自由,但会让你觉得轻松.因此常有人将python编程比喻为搭乐高积木,衔接用的接口已经都设计好了,玩家要做的只是发挥想象力专注于实现自己的创意.
1.1. "魔术方法"
那么这些用于实现语言框架接口又是什么样呢?
这些接口被戏称为"魔术方法",他们的特征是方法名前后都有如__
的两个下划线,这些方法能让你自己的对象实现如下的语言框架:
- 迭代
- 集合类
- 属性访问
- 运算符重载
- 函数和方法的调用
- 对象的创建和销毁
- 字符串表示形式和格式化
- 上下文管理
- 协程
1.2. 实际感受下魔术方法
下面是一个例子用来展示如何使用__getitme__
和__len__
这两个魔术方法,帮助我们构建一个有序的扑克牌类的过程(例子来自<流畅的python>第一章示例1.1)
PS:为了便于理解这个例子所有变量用中文.实际编程的时候用中文并不是好习惯,尤其是参与开源项目的时候
from collections import namedtuple Card = namedtuple('扑克牌', ['大小', '花色']) class 牌堆: ranks = [str(n) for n in range(2, 11)] + list('JQKA') suits = '梅花 方片 红桃 黑桃'.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常用于构建只有少数属性但是没有方法的对象,比如数据库条目.利用namedtuple,我们可以很轻松地得到一个纸牌对象:
beer_card = Card("7","方片")
beer_card
扑克牌(大小='7', 花色='方片')
当然我们这个例子主要还是关注FrenchDeck
这个类,它既短小又精悍.首先,它跟任何标准Python集合类型一样,可以用len()
函数来查看一叠牌有多少张:
deck = 牌堆() len(deck)
52
deck[0]
扑克牌(大小='2', 花色='梅花')
要随机抽取一张牌,只要使用python标准库的random.choice
即可
from random import choice choice(deck)
扑克牌(大小='7', 花色='红桃')
现在已经可以体会到通过实现魔术方法来利用Python数据模型的两个好处
- 作为你的类的用户,他们不必去记住标准操作的各式名称("怎么得到元素的总数?是
.size()
还是.length()
还是别的什么?") - 由于接口统一,可以更加方便地利用Python的标准库,比如
random.choice
函数,从而不用重新发明轮子,即便是使用第三方库,只要大家都统一使用相同的接口也可以相互调用.
因为__getitem__
方法把[]
操作交给了self._cards
列表,所以我们的deck类自动支持切片slicing
操作
beer_card = Card("7","方片")
0
beer_card = Card("7","方片")
1
beer_card = Card("7","方片")
2
beer_card = Card("7","方片")
3
同时因为实现了__getitem__
方法,这一摞牌就变成可迭代的了
beer_card = Card("7","方片")
4
beer_card = Card("7","方片")
5
迭代通常是隐式的,譬如说一个集合类型没有实现__contains__
方法,那么in
运算符就会按顺序做一次迭代搜索.于是,in
运算符可以用在我们的FrenchDeck
类上,因为它是可迭代的
1.2.1. 排序
我们按照常规,用点数来判定扑克牌的大小,2 最小、A 最大;同时还要加上对花色的判定,黑桃最大、红桃次之、方块再次.梅花最小.下面就是按照这个规则来给扑克牌排序的函数,梅花2的大小是0,黑桃A 是51:
beer_card = Card("7","方片")
6
beer_card = Card("7","方片")
7
beer_card = Card("7","方片")
8
1.2.2. 为牌堆添加洗牌功能
目前的牌堆无法洗牌,这是因为我们虽然用__getitem__
方法将获取牌的位置行为委托给了self._cards
,但这实际上只是实现了不可变序列
协议,关于这些协议的问题,会在后面讲到.要让牌堆支持洗牌,还需要给它定义一个__setitem__
方法.
beer_card = Card("7","方片")
9
beer_card
0
beer_card
1
beer_card
2
beer_card
3
beer_card
4
1.3. 如何使用魔术方法
首先明确一点,魔术方法的存在是为了被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__
方法中调用超类的构造器.
通过内置的函数(例如len、iter、str等等)来使用魔术方法是最好的选择.这些内置函数不仅会调用魔术方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快.
PS:不要自己想当然地随意添加魔术方法,比如__foo__
之类的,因为虽然现在这个名字没有被Python内部使用,以后就不一定了
目前的魔术方法都可以在官网的第3节中找到详细说明.这边不一一复述.
1.4. 为什么len不是普通方法?
回到最初的问题,为什么不是collection.len()
而是len(collection)
?
len
之所以不是一个普通方法,是为了让Python自带的数据结构可以"走后门",让解释器可以针对内置数据类型提供更好的优化.同时多亏了它是魔术方法,我们也可以把len
用于自定义数据类型.纯粹未必是最好的,python的数据模型实现兼顾通用性,效率和一致性.也印证了"Python之禅"中的一句话:"不能让特例特殊到开始破坏既定规则."
还没有评论,来说两句吧...