1.1 一摞Python风格的纸牌
__getitem__和__len__
例1,一摞有序的纸牌
import collections # namedtuple是一个函数,它用来创建一个自定义的tuple对象,并且规定了tuple元素, # 并可以用属性而不是索引来引用tuple的某个元素。 # namedtuple用来构建只有少数属性但是没有方法的对象 Card = collections.namedtuple('Card',['rank','suit']) class FrenchDeck: ranks = [str(n) for n in range(2,11)] + list('JQKA') # 黑桃、方块、梅花、红桃 suits = 'spades diamonds clubs hearts'.split() def __init__(self): self._cards = [Card(rank,suit) for rank in self.ranks for suit in self.suits] def __len__(self): return len(self._cards) def __getitem__(self, position): return self._cards[position]
# 利用namedtuple会轻松得到一个纸牌对象 beer_card = Card('7','diamonds') print(beer_card) # 用len()函数来查看一叠牌有多少张 deck = FrenchDeck() print(len(deck)) # 利用__getitem__方法从一叠牌中抽取特定的一张纸牌,比如第一张或最后一张。 print(deck[0]) print(deck[-1]) # Python内置从一直序列中随机选出一个元素的函数random.choice。 from random import choice print("随机抽取一张牌3次:") print(choice(deck)) print(choice(deck)) print(choice(deck)) # 查看一摞牌最上面3张和只看牌面是A的牌的操作 # 其中第二种操作的具体方法是,先抽出索引12的那张牌,然后每向后数13张牌拿一张 print("最上面3张牌:") print(deck[:3]) print("牌面为A的牌:") print(deck[12::13])
输出结果:
Card(rank='7', suit='diamonds') 52 Card(rank='2', suit='spades') Card(rank='A', suit='hearts') 随机抽取一张牌3次: Card(rank='9', suit='clubs') Card(rank='J', suit='clubs') Card(rank='10', suit='diamonds') 最上面3张牌: [Card(rank='2', suit='spades'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='clubs')] 牌面为A的牌: [Card(rank='5', suit='spades'), Card(rank='8', suit='diamonds'), Card(rank='J', suit='clubs'), Card(rank='A', suit='hearts')]
为了能够清晰的看出每一步得到的结果,也不需要这么多的输出语句,(注意注释掉上面暗调的代码)可在控制台运行:
>>> from frenchdeck import FrenchDeck, Card >>> beer_card = Card('7','diamonds') >>> beer_card Card(rank='7', suit='diamonds') >>> deck = FrenchDeck() >>> len(deck) 52 >>> deck[0] Card(rank='2', suit='spades') >>> deck[-1] Card(rank='A', suit='hearts') >>> from random import choice >>> choice(deck) Card(rank='2', suit='hearts') >>> choice(deck) Card(rank='K', suit='hearts') >>> choice(deck) Card(rank='J', suit='spades') >>> deck[:3] [Card(rank='2', suit='spades'), Card(rank='2', suit='diamonds'), Card(rank='2', suit='clubs')] >>> deck[12::13] [Card(rank='5', suit='spades'), Card(rank='8', suit='diamonds'), Card(rank='J', suit='clubs'), Card(rank='A', suit='hearts')] >>>
给一摞扑克牌排序,按照2最小,A最大;黑桃最大,红桃次之,方块再次,梅花最小。
按照这个规则给扑克牌排序,梅花2大小是0,黑桃A是51
suit_values = dict(spades =3, hearts=2, diamonds=1, clubs=0) def spades_high(card): # 在[2,3,4,5,6,7,8,9,10,J,Q,K,A]中的位置 rank_value = FrenchDeck.ranks.index(card.rank) # suit_values[card.suit]加上花色的大小 return rank_value*len(suit_values)+suit_values[card.suit] deck = FrenchDeck() 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') ...(46 cards ommitted) Card(rank='A', suit='diamonds') Card(rank='A', suit='hearts') Card(rank='A', suit='spades')
一般注记:按照目前的设计,FenchDeck是不能洗牌的,因为这摞牌是不可变的:卡牌和它们的位置都是固定的,除非破坏这个类的封装性,直接对_cards进行操作。第11章会讲到。
1.2 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被Python解释器调用的,你自己并不需要调用它们。也就是说没有my_object.__len__()这种写法,而应该使用len(my_object)。
通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率远远低于你去实现它们的次数。唯一的例外可能是__init__方法。
1.2.1 模拟数值类型
我们来实现一个二维向量(vector)类,这里的向量就是欧几里得几何中常用的概念。
__repr__、__abs__、__add__和__mul__
例2,一个简单的二维向量类
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 __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)
为了给这个类设计API,我们先写个模拟的控制台会话来做doctest。
>>> from vector import Vector >>> v1 = Vector(2,4) >>> v2 = Vector(2,1) >>> v1 + v2 Vector(4, 5) >>> v = Vector(3,4) >>> abs(v) 5.0 >>> v * 3 Vector(9, 12) >>> abs(v * 3) 15.
>>> v * -3
Vector(-9, -12)
abs是一个内置函数,如果输入是整数或者浮点数,它返回的是输入值的绝对值;如果输入是复数,那么返回这个复数的摸。
*运算符实现向量的标量乘法(即向量与数的乘法,得到的结果向量的方向与原向量方向一致,模变大。如果向量与负数相乘,得到的结果向量的方向与原向量相反。)
1.2.2 字符串表示形式
Python有一个内置的函数叫repr,它能把对一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。
%和str.format是两种格式化字符串的手段。
__str__和__repr__的区别(https://stackoverflow.com/questions/1436703/difference-between-str-and-repr)
1.2.3 算术运算符
通过例2发现,__add__和__mul__两个方法的返回值都是新创建的向量对象,被操作的两个向量(self或other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。
1.2.4 自定义的布尔值
如果想让Vector.__bool__更高效,可以采用:
def __bool__(self): return bool(self.x or self.y)
它不那么易读,却能省掉从abs到__abs__到平方再到平方根这些中间步骤。通过bool把返回类型显示转换为布尔值是为了符合__bool__对返回值的规定,因为or运算符可能会返回x或者y本身的值:若x的值等价于真,则or返回x的值;否则返回y的值。
1.3 特殊方法一览
表1-1:跟运算符无关的特殊方法
类别 | 方法名 |
字符串字节序列表示形式 | _ repr _ , _ format _ , _ str _ , _ bytes _ |
数值转换 | _ abs _ , _ bool _ , _ complex _ , _ int _ ,_ float _ , _ hash _ , _ index _ |
集合模拟 | _ len _ , _ getitem _ , _ setitem _ , _ delitem _ ,_ contains _ |
迭代枚举 | iter _ , _ reversed _ , _ next _ |
可调用模拟 | _ call _ |
上下文管理 | _ enter _ , _ exit _ |
实例创建和销毁 | _ new _ , _ init _ , _ del _ |
属性管理 | _ getattr _ , _ getattribute _ , _ setattr _ , _ delattr _ , _ dir _ |
属性描述符 | _ get _ , _ set _ , _ delete _ |
跟踪相关的服务 | _ prepare _ , _ instancecheck _ , _ subclasscheck _ |
表1-2:跟运算符相关的特殊方法
类别 | 方法名和对应的运算符 |
一元运算符 | 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 _ , _ rpow _ |
增量赋值运算符 | _ iadd _ , _ isub _ , _ imul _ , _ itruediv _ , _ ifloordiv _ _ imod _ , _ idivmod _ , _ ipow _ |
位运算符 | _ invert _ ~, _ lshift _ <<, _ rshift _ >>, _ adn _ &, _ or _ |
反向位运算符 | _ rlshift _ , _ rrshift _ , _ rand _ , _ rxor _ , _ ror _ |
增量赋值位运算符 | _ ilshift _ , _ irshift _ , _ iand _ , _ ixor _ , _ ior _ |
提示:当交换两个操作数的位置时,就会调用反向运算符(b*a而不是a*b)