《Fluent Python》- 01 Python数据模型
数据模型其实是对Python框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列,迭代器,函数,类和上下文管理器
一摞Python风格的纸牌
主要说明两个方法 __getitem__ 以及 __len__
Card = collections.namedtuple('Card', ['rank', 'suit']) # namedtuple,tuple的一种,不可变 # 名为Card,后面的rank,suit是其属性,简单来说就是一个不可变的对象包含['a', 'b']两个属性 # 可以通过 my_card = Card('rank', 'suit') 的方式简单的构造 class FrenchDeck: ranks = [str(n) for n in range(2, 11)] + list('JQKA') # 构造 '2 3 4...10 J Q K A' 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, item): # 为了实现 obj[item] 这个操作 return self._cards[item]
首先我们构造出这个FrenchDeck类,主要是包含有两个方法 __getitem__ 以及 __len__
当一个类实现了__getitem__方法时,便可以使用obj[index] 这种类似列表的方式去访问其元素,而 __len__ 方法是为了能让 len() 函数用作
简单来说,如果你在代码里用了obj[index] 会访问到 __getitem__ 方法,另一个同理
deck = FrenchDeck() print(len(deck)) # 52 print(deck[0]) # Card(rank='2', suit='spades') print(choice(deck)) # choice 从列表中随机访问 Card(rank='6', suit='diamonds')
符合预期值,其实一开始我们可能会有点不习惯,为什么要用len,而不是用 .length() 或者 .size() 这种方法来获取长度。Python采用这种方式其实也有其好处,假象一下,我们在Java中获取长度,有时可能不知道对方是采用了length() 方法还是size()方法,亦或者其他名称来获取长度,并没有统一的规范。
ranks列表的构造是采用了列表推导的方式构造了,这个下一节会说明(我刚看时看的也是很懵,不过通过结果倒推其实还是很容易理解的)
迭代通常是隐式的,譬如说一个集合类型没有实现__contains__方法,那么in运算符就会按顺序做一次迭代搜索。于是in运算符就可以用在我们的FrenchDeck类上:
print(Card('Q', 'hearts') in deck) # True print(Card('Q', 'beasts') in deck) # False
那么怎么排序呢?我们就按照2,3,4.....K,A 的顺序,再加上花色,黑桃最大,红桃次之,方块再次之,梅花最小。
suit_values = dict(spades = 3, hearts = 2, diamonds = 1, clubs = 0) def spades_high(card): rank_value = FrenchDeck.ranks.index(card.rank) return rank_value * len(suit_values) + suit_values[card.suit] # 这个用来排序的函数有些复杂 # sorted的里的key可以是函数,也可以是lambda,我们可以把函数抽象成一个lambda # 首先要明白这个函数是作用是什么,给定的参数是card换句话说,需要排序的列表里的参数得有card # 然后通过这个card做些许操作,得到一个值,通过这个值来比较 # FrenchDeck.ranks.index(card.rank) 这行其实是获取了FrenchDeck里的ranks,然后通过传入的card来判断,传入的card应该在哪个位置 # 举个例子,传入了Card(rank='J', suit='diamonds'),那么rank_value得到的答案就是'J'所在的下角标,也就是9 # 然后返回的是 9 * 4(因为有四种花色) + 这种花色的权值 # 这个排序的结果就是 (clubs, 2) ,(diamonds, 2) ... (spades, A) for card in sorted(deck, key=spades_high): print(card) ###结果 # Card(rank='2', suit='clubs') # Card(rank='2', suit='diamonds') # Card(rank='2', suit='hearts') # Card(rank='2', suit='spades') # ... # Card(rank='A', suit='diamonds') # Card(rank='A', suit='hearts') # Card(rank='A', suit='spades')
一些特殊方法
一些特殊方法是为了被Python解释器调用的,而自己并非需要调用这些。就像上面的那个长度,我们是通过len() 来获取,而非直接 .__len__()。
如果是Python的内置类型,比方说list,str等 CPython会抄近路,直接调用其ob_size属性,因为这个读取是比读函数快的。
很多时候这些特殊方法的调用是隐式的,比方说for i in x 这个语句,其实是用了iter(x) 方法,背后则是 x.__iter__()。这时你可能就会好奇了,我们的FrenchDeck其实并没有实现iter方法,却可以迭代,这是为什么。其实主要是__getitem__方法的功劳,关于迭代的具体流程之后还会说明。
还有不要想当然的去添加一些特殊方法,现在没有的,以后说不定会有
模拟数值类型
利用特殊方法,可以让自定义对象通过加号‘+’进行运算,简单来说,可以变相实现像C++那种重载运算符
class Vector: def __init__(self, x = 0, y = 0): self.x = x self.y = y def __repr__(self): # 类似于java里的 toString return 'Vector(%r, %r)' % (self.x, self.y) def __abs__(self): # 获取绝对值,可以通过abs() 来访问 return hypot(self.x, self.y) # 通俗来说,就是获取以a, b为边的直角三角形斜边,放这里的意思就是向量长 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)
关于__repr__方法再多说两句,__repr__几乎等价于Java中的toString()函数(友情提示你一下,Java里你即使实现了toString方法,也不能用String强转)。
__str__ 函数的用途是在调用 str(obj)的时候访问的,但是print(obj) 是不会调用__str__ 方法的。不论你有没有实现__repr__
但是反过来说,如果你没有实现__str__ 函数,但是有__repr__函数,那么解释器会帮你调用__repr__方法。
特殊方法一览
和运算符无关的特殊方法 | |
类别 | 方法名 |
字符串/字节序列表示形式 | __repr__, __str__, __format__, __bytes__ |
数值转换 | __abs__, __bool__, __complex__, __int__, __float__, __hash__, __index__ |
集合模拟 | __len__, __getitem__, __setitem__, __delitem__, __contains__ |
迭代枚举 | __iter__, __reversed__, __next__ |
可调用模拟 | __call__ |
上下文管理 | __enter__, __exit__ |
实例创建和销毁 | __new__, __init__, __del__ |
属性管理 | __getattr__, __setattr__, getattribute__, __setattribute__, __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__ divmod(), __pow__ **或pow(), __round__ round() |
反向算术运算符 | __radd__, __rsub__, __rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rdivmod__, __rpow__ |
增量赋值算术运算符 | __iadd__, __isub__, __imul__ , __itruediv__, __ifloordiv__, __imod__, __ipow__ |
位运算符 | __invert__ ~, __lshift__ <<, __rshift__ >>, __and__ &, __or__ |, __xor__ ^ |
反向位运算符 | __rlshift__, __rrshift__, __rand__, __rxor__, __ror__ |
增量赋值位运算符 | __ilshift__, __irshift__, __iand__, __ixor__, __ior__ |
杂谈(非正式向)
这个表是真的难操控,姑且就这样吧
老实说,我最开始看这一节花了很多时间,这第一节确实有些硬核,用到了很多之前见都没见过的方法或者方式。
有的时候正着不行就反着来,从结果出发倒推代码逻辑,然后去理解里面的用意能帮助我们不少。