19. 拾珍
本书的一大目标一直是尽可能少的介绍Python语言 .
如果做某种事情有两种方法 , 我会选择一种 , 并避免提及另一种 .
或者有时候 , 我会把另一种方法作为练习进行介绍 . 本章我会带领大家回顾那些遗漏的地方 .
Python提供了不少并不是完全必需的功能 ( 不用它们也能写出好代码 ) ,
但有时候 , 使用这些功能可以写出更简洁 , 更可读或更高效的代码 , 甚至有时候三者兼得 .
19.1 条件表达式
我们在 5.4 节中见过条件语句 .
条件语句通常用来从两个值中选择一个 . 例如 :
if x > 0 : y = math. log( x)
else : y = float ( 'nan' )
import mathdef main ( x) : if x > 0 : y = math. log( x) else : y = float ( 'nan' ) return yif __name__ == '__main__' : res = main( 0 ) print ( res, type ( res) )
这条语句检查x是否为正数 .
如果为整数 , 则计算math . log ; 如果为负数 , math . log会抛出ValueError异常 .
为了避免程序停止 , 我们直接生成一个 'NaN' , 一个特殊的浮点数 , 代表 '不是数' ( Not A Number ) . 我们可以使用 '条件表达式' 来更简洁地写出这条语句 :
y = math. log( x) if x > 0 else float ( 'nan' )
这条语句几乎可以用英语直接读出来 'y gets log-x if x greater than 0; otherwise is gets NaN'
( y的值在大于 0 时时math . log ( x ) , 否则是NaA ) .
递归函数有时候可以使用条件表达式重写 .
例如 , 下面是factorial的一个递归版本 :
def factorial ( n) : if n == 0 : return 1 else : return n * factorial( n - 1 )
我们可以将其重写为 :
def factorial ( n) : return 1 if n == 0 else n * factorial( n - 1 )
条件表达式的另一个用途是处理可选参数 .
例如 , 下面是GoodKangaroo的init方法 ( 参见练习 17 - 2 ) :
def __init__ ( self, name, contents= None ) : self. name = nameif contents == None : contents = [ ] self. pouch_contents = contents
我们可以将其重写为 :
def __init__ ( self, name, contents= None ) : self. name = nameself. pouch_contents = [ ] if contents == None else contents
一般来说 , 如果条件语句的两个条件分支都包含简单的返回或对同一变量进行赋值的表达式 ,
那么这个语句可以转化为条件表达式 .
19.2 列表理解
在 10.7 节中我们已经见过映射和过滤模式 . 例如 , 下面的函数接收一个字符串列表 ,
将每个元素通过字符串方法capitalize进行映射 , 并返回一个新的字符串列表 :
def capitalize_all ( t) : res = [ ] for s in t: res. append( s. capitalize( ) ) return res
我们可以用列表理解 ( list comprehension ) 把这个函数写得更紧凑 :
def capitalize_all ( t) : return [ s. capitalize( ) for s in t]
上面的方法括号操作符说明我们要构建一个新的列表 .
方括号之内的表达式指定了列表的元素 , 而for子句则表示我们要遍历的序列 . 列表理解的语法有一点粗糙的地方 , 因为里面的循环变量 , 即本例中的s , 在表达式出现在定义之前 . 列表理解也可以用于过滤操作 .
例如 , 下面的函数选择列表t中的大写元素 , 并返回一个新列表 :
def only_upper ( t) : res = [ ] for s in t: if s. isupper( ) : res. append( s) return res
我们可以用列表理解将其重写为 :
def only_upper ( t) return [ s for s in t if s. isupper( ) ]
对于检查表达式来说 , 列表理解更紧凑 , 更易于阅读 ,
并且它们通常都比实现相同功能的循环更快 , 有时候甚至快很多 .
因此 , 如果你因为我没有早些提到它而恼怒 , 我表示十分理解 . 但是我们辩解一下 , 列表理解更难以调试 , 因为你没法再循环内添加打印语句 .
我建议你只在计算简单到一次就能弄对的时候才使用它 .
对初学者来说 , 这意味着从来不用 .
19.3 生成器表达式
生成器表达式 ( generator expression ) 和列表理解类似 , 但是它使用圆括号 , 而不是使用方括号 :
>> > g = ( x ** 2 for x in range ( 5 ) )
>> > g
>> > < generator objrct < genexpr> at 0x7f4c5a786c0 >
结果是一个生成器对象 , 它知道该如何便遍历值的序列 .
但它又和列表理解不同 , 它不会一次把结果计算出来 , 而是等待请求 .
内置函数next会从生成器中获取下一个值 :
>> > next ( g)
0
>> > next ( g)
1
当到达序列的结尾后 , next会抛出一个StopIteration异常 .
可以使用for循环来遍历所有值 :
>> > for val in g:
. . . print ( val)
4
9
16
生成器对象会跟踪记录访问序列的位置 , 所以for循环会从上一个next所在的位置继续 .
一旦生成器遍历结束 , 再访问它就会抛出StopException :
>> > next ( g)
StopException
生成器表达式经常和sum , max , 和min之类的函数配合使用 :
>> > sum ( x ** 2 for x in range ( 5 ) )
30
19.4 any和all
Python提供了一个内置函数any , 它接收一个由布尔值组成的序列 , 并在其中任何 ( 一个 ) 值是True时返回True .
它可以用于列表 :
>> > any ( [ False , False , True ] )
>> > True
但它更常用于生成器表达式 :
>> > any ( letter == 't' for letter in 'monty' )
True
上面这个例子用处不大 , 因为它做的事情和in表达式一样 .
但是我们可以用any来重写 9.3 节中的搜索函数 .
例如 , 我们可以将aviods函数重新为 :
def avoids ( word, forbidden) : return not any ( letter in forbidden for letter in word)
这个函数读起来几乎和英语一致 :
'word avoids forbidden if there are not any forbidden letters in word'
( 我们说一个word避免被禁止 , 是指word中没有任何被禁的字母 ) .
Python还提供了另一个内置函数all , 它在序列中所有元素都是True时返回True .
作为练习 , 请使用all重写 9.3 节中的users_all函数 .
def user_all ( word, required) : for letter in required: if letter not in word: return False return True print ( user_all( 'hello' , 'h' ) )
def user_all ( word, required) : return all ( letter in word for letter in required) print ( user_all( 'hello' , 'h' ) )
19.5 集合
我曾在 13.6 节中使用字典来寻找在文档中出现但不属于一个单词列表的单词 .
我写的函数接收一个字典参数d1 , 其中包含文档总所有的单词作为键 ; 以及另一个参数d2 , 包含单词列表 .
它返回一个字典 , 包含d1中所有不在d2之中的键 :
def substracr ( d1, d2) : res = dict ( ) for key in d1: if key not in d2: res[ key] = None return res
在这些字典中 , 值都是None , 因为我们从来不用它们 .
因此 , 我们实际上浪费了一些存储空间 .
Python还提供了另一个内置类型 , 称为集合 ( set ) , 它表现得和没有值而使用键集合的字典类似 .
向一个集合添加元素很快 , 检查集合成员也很快 . 集合还提供方法和操作符来进行常见的集合操作 . 例如 , 几何减法可以使用方法difference或者操作符 '-' 来实现 .
因此我们可以将substract函数重写为 :
def substract ( d1, d2) : return set ( d1) - set ( d2)
结果是一个集合而不是字典 , 但是对于遍历之类的操作 , 表现是一样的 .
本书中的一些练习可以使用集合来更加简洁且高效地实现 .
例如 , 练习 10 - 7 中的has_duplicates函数 , 下面是使用字典来实现的一个解答 :
def has_duplicates ( t) : d = { } for x in t: if x in d: return True d[ x] = True return False
一个元素第一次出现的时候 , 把它加入到字典中 . 如果相同的元素再次出现时 , 函数就返回True .
使用集合 , 我们可以这样写同一个函数 :
def has_duplicates ( t) : return len ( set ( t) < len ( t) )
一个元素在一个结合中只能出现异常 , 所以如果t中间的某个元素出现超过一次 , 那么变成集合后其长度比t小 .
如果没有任何重复元素 , 那么集合的长度应当和t相同 . 我们也可以使用集合来解决第 9 章中的一些练习 .
例如 , 下面是users_only函数使用循环来实现的版本 :
def users_only ( word, available) : for letter in word: if letter not in available: return False return True
users_only检查word中所有的字符是不是在available中出现 .
我们可以这样重写 :
def users_only ( word, available) : return set ( word) <= set ( available)
操作符 < = 检查一个集合是否是零一个集合的子集 , 包括两个集合相等的情况 .
这正好符合word中所有字符都出现在Available中 .
19.6 计数器
计数器 ( counter ) 和集合类似 , 不同之处在于 , 如果一个元素出现超过一次 , 计数器会记录它出现了多少次 .
如果你熟悉多重集 ( multiset ) 这个数学概念 , 就会发现计数器是多重集的一个自然的表达方式 . 计数器定义在标准模块collections中 , 所有需要导入它在使用 .
可以用字符串 , 列表或者其他任何之处迭代访问的类型对象来初始化计数器 :
>> > from collections import Counter
>> > count = Counter( 'parrot' )
>> > count
Counter( { 'r' : 2 , 't' : 1 , 'o' : 1 , 'p' : 1 , 'a' : 1 } )
计数器有很多地方和字典相似 . 它们将每个键映射到其出现次数 . 和字典一样 , 键必须是可散列的 .
但和字典不同的是 , 在访问计数器中不存在的元素时 , 它并不会抛出异常 , 相反 , 它会返回 0 :
>> > count[ 'd' ]
0
我们可以使用计数器来重写练习 10 - 6 中的is_anagram函数 :
def is_anagram ( word1, word2) : return Counter( word1) == Counter( word2)
如果两个单词互为回文 , 则它们会包含相同的字母 , 且各个字母计数相同 , 所有他们对应的计数器对象也会相等 .
计数器提供方法和操作符来进行类似集合的操作 , 包括集合加法 , 减法 , 并集和交集 .
计数器还提供一个非常常用的方法most_common , 它返回一个 '值-频率对' 的列表 ,
按照最常见到最少见来排序 :
>> > count = Counter( 'parrot' )
>> > for val, freq in count. most_common( 3 ) :
. . . print ( val, freq) r 2
p 1
a 1
19.7 defaultdict
collections模块提供了defaultdict , 它和字典相似 , 不同的是 , 如果你访问一个不存在的键 ,
它会自动创建一个新值 . 创建一个defaultdict对象时 , 需要提供一个用于创建新值的函数 .
用来创建对象的函数有时被称为 '工厂(factory)' 函数 .
用于创建列表 , 集合以及其他类型对象的内置函数 , 都可以用作工厂函数 :
>> > from collections import defaultdict
>> > d = defaultdict( list )
请注意 , 参数是list ( 一个类对象 ) , 而不是list ( ) ( 一个新的列表 ) .
你提供的函数直到访问不存在的键时 , 才会被调用的 :
>> > t = d[ 'new key' ]
>> > t
[ ]
新列表t也会加到字典中 . 所有 , 如果我们修改t , 改动也会在d中体现 :
>> > t. append( 'new value' )
>> > d
defaultdict( < class 'lsit' > , { 'new key' : [ 'new value' ] } )
如果创建一个由列表组成的字典 , 使用defaultdict往往能够帮你写出更简洁的代码 .
在练习 12 - 2 的解答中 , 我创建了一个字典 , 将排序的字母字符串映射到可以由那些字母拼写的单词列表 .
例如 , 'opst' 映射到列表 [ 'opst' , 'pots' , 'spot' , 'stop' , 'tops' ] .
可以从↓下载该解答 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / anagram_sets . py
下面是原始的代码 :
def all_anagrams ( filename) : d = { } for line in open ( filename) : word = line. strip( ) . lorwe( ) t = signature( word) if t not in d: d[ t] = [ word] else : d[ t] . append( word) return ddef signature ( s) : t = list ( s) t. sort( ) t = '' . join( t) return t
这个函数 ( all_anagrams ) 可以用setdefault简化 , 你可能在练习 11 - 2 中也用过 :
def all_anagrams1 ( filename) : d = { } for line in open ( filename) : word = line. strip( ) . lower( ) t = signature( word) d. setdefault( t, [ ] ) . append( word) return d
但这个解决方案有一个缺点 , 它不管是否需要 , 每次都会新建一个列表 .
( 因为列表被作为参数传到进去了 , 传递给setdefault方法 , 所有每次调用都要创建 . )
我们可以使用defaultdict来避免这个问题 , 并进一步简化代码 :
def all_anagrams ( filename) : d = defaultdict( list ) for line in open ( filename) : t = signature( word) d[ t] . append( word) return d
在练习 18 - 3 的解答中 , 函数has_straightflush中使用了setdefault .
可以从↓下载它 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / PokerHandSoln . py
但这个解决方法的缺点是 , 不管是否必需 , 每次循环迭代都会创建一个新的Hand对象 .
作为练习 , 请使用dedaultdict重写该函数 .
from collections import defaultdictdef has_straightflush ( self) : d = defaultdict( PokerHand) for c in self. cards: d[ c. suit] . add_card( c) for hand in d. values( ) : if len ( hand. cards) < 5 : continue hand. make_histograms( ) if hand. has_straight( ) : return True return False
19.8 命名元组
很多简单的对象其实都可以看作是几个相关值的集合 .
例如 , 第 15 章中定义的Point对象 , 包含两个数字 , 即x和y .
定义一个这样的类时 , 通常会从init方法和str方法开始 .
class Point : def __init__ ( self, x= 0 , y= 0 ) : self. x = xself. y = ydef __str__ ( self) : return '(%g, %g)' % ( self. x, self. y)
这里用来更多代码来传达很少的信息 .
Python提供了一个更简单的方式来表达用一个意思 :
from collections import namedetuple
Point = namedtuple( 'Point' , [ 'x' , 'y' ] )
第一个参数是你想要创建的类名 .
第二个参数是Point对象应当包含的属性的列表 , 以字符串表示 .
namedtuple的返回值是一个类对象 :
>> > Point
< class '__main__.Point' >
这里Point类会自动提供__init__和__str__这样的方法 , 所以你不需要写它们 .
要创建一个Point对象 , 可以把Point类当作函数来用 :
>> > p = Point( 1 , 2 )
>> > p
Point( x= 1 , y= 2 )
init方法使用你提供的名字把实参赋值给属性 .
str方法会打印出Point对象及其属性的字符串表示 . 可以使用名称来访问命名元组的元素 :
>> > p. x, p. y
( 1 , 2 )
也可以直接把它当作元组来处理 :
>> > p[ 0 ] , p[ 1 ]
( 1 , 2 )
>> > x, y = p
>> > x, y
( 1 , 2 )
命名元组还提供了快速定义简单类的方法 , 但其缺点是简单的类并不会总保持简单 .
可能之后你需要给命名元组条件方法 . 如果定义一个新类 , 继承当前的命名元组 :
class Pointier ( Point) :
或者也可以直接切换成传统的类定义 .
19.9 收集关键词参数
在 12.4 节中 , 我们见过如何编写函数将其参数收集成一个元组 :
def printall ( * args) : print ( args)
可以使用任意个数的按位实参 ( 也就是说 , 不带名称的实参 ) 来调用这个函数 :
>> > printall( 1 , 2.0 , '3' )
( 1 , 2.0 , '3' )
但是 * 号操作符并不会收集关键字实参 :
>> > printall( 1 , 2.0 , third= '3' )
. . .
TypeError: printall( ) got an unecpected keyword argument 'third'
要收集关键词实参 , 可以使用 * * 操作符 :
def printall ( * args, ** kwargs) : print ( args, kwargs)
这里收集关键字形参可以任意命名 , 但kwargs是一个常见的选择 .
收集的结果是一个将关键词映射到值的字典 :
>> > printall( 1 , 2.0 , third= '3' )
( 1 , 2.0 ) { 'third' : '' }
如果有一个关键词到值的字典 , 就可以使用分散操作符 * * 来调用函数 :
>> > d = dict ( x= 1 , y= 2 )
>> > Point( ** d)
Point( x= 1 , y= 2 )
没有用分散操作符的话 , 函数会把d当作一个单独的按位实参 , 所有它会把d赋值给x , 并因为没有提供y的赋值而报错 :
>> > d = dict ( x= 1 , y= 2 )
>> > Point( d)
Traceback ( most recent call last) :
File "<stdin>" , line 1 , in < module>
TypeEroor: __new__( ) missing 1 required positional argument: 'y'
当处理参数很多的函数时 , 创建和传递字典来指定常用的选项是非常有用的 .
19.10 术语表
条件表达式 ( conditional expression ) : 一个根据条件返回一个或两个值的表达式 . 列表理解 ( list comprehension ) : 一个以方框包含一个for循环 , 生成新列表的表达式 . 生成器表达式 ( generator ecpression ) : 一个以括号包含一个for循环 , 返回一个生成器对象的表达式 . 多重集 ( multiset ) : 一个用来表达从一个集合的元素到它们出现次数的映射的数学概念 . 工厂函数 ( factory ) : 一个用来创建对象 , 并常常当作参数使用的函数 .
19.11 练习
1. 练习1
下面的函数可以递归地计算二项式系数 :
def binomial_coeff ( n, k) : """计算(n, k)的二项式系数,n: 实验次数k: 成功次数返回: int""" if k == 0 : return 1 if n == 0 : return 0 res = binomial_coeff( n- 1 , k) + binomial_coeff( n- 1 , k- 1 ) return res
使用内嵌条件表达式来重写该函数 .
注意 : 这个函数频率不高 , 因为它会不停得重复计算相同的值 .
可以通过使用备忘 ( memoizing , 参见 11.6 节 ) 来提高它的效率 .
但你可能会发现 , 使用条件表达式之后 , 添加备忘会变得比较困难 .
def binomial_coeff ( n, k) : """ 计算(n, k)的二项式系数, n: 实验次数 k: 成功次数 返回: int """ return 1 if k == 0 else 0 if n == 0 else binomial_coeff( n- 1 , k) + binomial_coeff( n- 1 , k- 1 )
memo_k = { } def binomial_coeff0 ( n, k) : if k == 0 : return 1 if n == 0 : return 0 res = binomial_coeff0( n - 1 , k) + binomial_coeff0( n - 1 , k - 1 ) return resdef binomial_coeff1 ( n, k) : key = n, kres = 1 if k == 0 else 0 if n == 0 else \binomial_coeff1( n - 1 , k) + binomial_coeff1( n - 1 , k - 1 ) memo_k[ key] = resreturn resdef binomial_coeff2 ( n, k) : return 1 if k == 0 else 0 if n == 0 else \binomial_coeff2( n - 1 , k) + binomial_coeff2( n - 1 , k - 1 ) def main ( n, k) : res0 = binomial_coeff0( n, k) print ( res0) res1 = binomial_coeff1( n, k) print ( res1) res2 = binomial_coeff2( n, k) print ( res2) if __name__ == '__main__' : main( 7 , 2 )