《流畅的Python》Data model(数据/对象模型)
第一章 Data model
⚠️整本书都是讲解Data model,第一章是一个概述。
⚠️不适合初学者。因为special method和meta programming技巧只是Python代码的一部分。不是全部。
目标:学习第一章内容。之后是否深入还是先学习一些其他的知识待定。
纸牌的例子
>>> a = dict(a=1,b=2) >>> a {'a': 1, 'b': 2} >>> a['a'] == type(a).__getitem__(a, 'a') True
Data model中的special methods name让你的对象执行/实现,支持,交互basic language构架,例如:
- Iteration
- Collections 集合类
- Attribute access
- Operator overloading 重载操作符
- 函数和方法的调用
- Object创建和销毁
- 字符串格式化
- Managed contexts被管理的上下文代码(就是with语法块)
import collections # 扑克牌类,每张牌有2个属性:数字/字母rank,花色suit Card = collections.namedtuple('Card', ['rank', 'suit']) class FrenchDeck(object): # 2个类属性,储存牌的rank和suit。 ranks = [str(n) for n in range(2, 11)] + list('JQKA') #13种rank suits = 'spades diamonds clubs hearts'.split() #4种suit # 实例化一个对象,实例变量_cards(数据属性)是一个list,储存了52张Card(类型是namedtule) def __init__(self): self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] # self._cards = [] # for suit in self.suits: # for rank in self.ranks: # self._cards.append(Card(rank, suit)) # 因为内置函数len()的实参类型支持是内置的或序列或集合类型。(也包括collections中的类型) # 所以对自定义的类FrenckDeck,想查看实例对象的元素数量,需要重写__len__ def __len__(self): print('The length of deck is %s' % len(self._cards)) return len(self._cards) #52 # 重写__getitem__ # 因为实例对象deck,使用特殊语法[],解释器就会去找__getitem__这个特殊方法。 # 因为deck[参数],转化为__getitem__(self, 参数),最后调用self._cart[参数], 所以可以支持切片。 def __getitem__(self, position): return self._cards[position] deck = FrenchDeck() print(len(deck)) print(deck[:2])
输出:
The length of deck is 52 52 [Card(rank='2', suit='spades'), Card(rank='3', suit='spades')]
因为__getitem__返回的是一个tuple子类的对象,它是iterable的所以可以使用for:
>>> for card in deck: ... print(card) ... Card(rank='2', suit='spades') Card(rank='3', suit='spades') ...
解释:
for value in variable: pass语法的variable是一个iterator对象,它是iter(object)返回的。因此object有2类可能:
- 是支持迭代协议(有
__iter__()
方法)的集合对象。 - 或是支持序列协议(有
__getitem__()
方法,且数字参数从0
开始。本例的deck实例对象有方法__getitem__()
问题:为每张扑克牌设定一个大小值。然后按照扑克大小排序?
def spades_high(self,card): suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) # 用ranks的索引表示一张卡牌的rank的大小。从0~12 rank_value = FrenchDeck.ranks.index(card.rank) print(rank_value) return rank_value * len(suit_values) + suit_values[card.suit]
rank_value变量的取值范围0~12
len(suit_values): 得到4
所以:rank_value * len(suit_values)的值:0 4 8 12 16 20 24 28 32 36 40 44 48
得到的值,根据不同的牌的花色分别再加0,1,2,3。
所以:
2 3 4 5 6 7 8 9 10 J Q k A
club: 0 4 8 12 16 20 24 28 32 36 40 44 48
diamons: 1 5 9 13 17 21 25 29 33 37 41 45 49
hearts: 2 6 10 14 18 22 26 30 34 38 42 46 50
spades: 3 7 11 15 19 23 27 31 35 39 43 47 51
#根据扑克大小排序: for card in sorted(deck, key=deck.spades_high): print(card)
sorted(iterable, *, key=None)
使用sorted()返回一个按照大小排序的deck列表:
[Card(rank='2', suit='clubs'), Card(rank='2', suit='diamonds'), ...略]
in运算符
执行一次迭代搜索
>>> Card('Q', 'hearts') in deck True #True
小结:
- FrenchDeck 是继承object类。(implicitly)? 什么是隐式(即使不写FrenchDeck(object),也会继承object。这是Python3的特征)
- 功能的实现,不是来自继承。而是使用data model和合成的方法来实现的。
- 通过special methods,FrenchDeck的行为就像一个Python自带的序列数据类型。比如本例子的迭代和切片操作。
- French类还可以使用标准库中的模块的方法。
- 感谢合成composition,__len__, __getitem__的实现能够代理/传递所有的工作给一个list object: self._cards。
How Special Methods Are Used?
首先,Special Methods是由解释器调用的。
- 无需写my_object.__len__()这个格式❌,
- 而是写len(my_object) ✅
如果实例对象是user-defined class的实例,解释器会调用你在类定义中实现的__len__这个实例方法。
但是,像list, str, bytearray等等内建类型。解释器为了提高运算速度,Cpython会抄近路,直接调用C语言写的对应的结构体内的值。
特殊方法的调用:不直接体现在代码上:implicit!
例如for语句会调用iter()内置函数返回一个iterator用于迭代。如果参数对象是可迭代的,这个函数会是调用object.__iter__()。
除了元编程,一般很少直接使用special method。只有__init__方法用的较多:在子类中调用superclass。
尽量使用内置函数来代替special method:
- 内置函数是一种扩展,除了调用对应的特殊方法,还有提供其他服务。
- 对应内置类型数据,运行速度更快。可以看本书14.12节。
1.2.1 Emulating Numeric Types 模拟数值类型
自定义的对象,不能使用运算符,但通过特殊方法,我们能够让自定义对象使用运算符,进行运算。
本节展示了几个特殊对象,它们不会被直接的调用,而是被解释器调用。
例子:这是一个二维向量类:
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 __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)
解释器发现+号运输符会去调用__add__这个特殊方法:
>>> import linshi >>> from linshi import Vector >>> v1 = Vector(2, 4) >>> v2 = Vector(2, 1) >>> v1 + v2 Vector(4, 5)
解释器发现*号运输符会去调用__mul__这个特殊方法:
1.2.2 String Representation
特殊方法__repr__被内建函数repr调用,它返回一个用字符串的形式表现的对象,以方便观察。这就是String Representation。
上文举例的Vector类,如果没有定义__repr__方法,在控制台输出一个实例对象时,会输出<模块.类 object at 内存地址编号>的格式:
>>> from linshi import Vector >>> v1 = Vector(2, 4) >>> v1 <linshi.Vector object at 0x102a0bd60>
只会时👆这个表现形式。
repr就是Representation的前4个字母。repr()就是把一个自定义类的实例对象用自定义的方式来表现。
⚠️__repr__()主要用于调试。
⚠️在__repr__()的实现中,会使用了%r来获取对象的属性。
__repr__是__str__的替代。区别是__str__/str()没有返回值,需要配合print。可以用简单的指向操作:
__repr__ = __str__
1.2.3Boolean Value of a Custom Type
bool(object)会调用object.__boo__()方法
Overview of Special Methods
Why len Is Not a Method
len()为什么是特殊方法?作者在论坛上https://www.python.org/doc/humor/#the-zen-of-python回答了这个问题。
本章1.2节也提到了。
when x is an instance of a built-in type. No method is called for the built-in objects of CPython: the length is simply read from a field in a C struct. Getting the number of items in a collection is a common operation and must work efficiently for such basic and diverse types as str, list, memoryview, and so on.
当x是一个内建类型(如list, str)的实例,那么len(x)并不会调用任何方法,即调用__len__,而是直接从一个C语言的结构体内读取对象的长度。
因为从集合中得到元素的数量是一个经常使用的功能,所以通过这种方法让len()变得非常高效,尤其是计算大的集合的时候,速度非常快。
另一方面,len()作为特殊方法,可以用于自定义数据类型。在保持内置类型的效率和保证语言的一致性上找到了平衡点。
小结
通过特殊方法,自定义数据类型就可以充分利用内置类型的方法。这是一种简化。
杂谈:
数据模型,
也被称为对象模型。wiki定义对象模型是计算机编程的对象的属性。两者等同。
Magic Methods,
等同于special methods。Ruby社区也有这个概念。目的都是利用这个概念来丰富metaobject protocol。但本质不是魔术,只是为了让用户能够使用核心开发者使用的工具。
Metaobjects
元对象协议。
The metaobject part refers to the objects that are the building blocks of the language itself.
元对象涉及到构建语言本身的对象。
协议可以看成是interface接口。所以元对象协议完全等同于对象模型:一个用于构建核心语言的API。
本章是一个概述data model。后面的章节是:
- Data Structures
- Functions as Objects
- Object-oriented idioms
- Control Flow
- 元编程