1. 條件表達(dá)式
在兩個(gè)值之間進(jìn)行二選一的條件語句中,我們的通常寫法是:
if x > 0:
y = math.log(x)
else:
y = float('nan')
其實(shí)可以用條件表達(dá)式進(jìn)行簡(jiǎn)化:
y = math.log(x) if x > 0 else float('nan')
這條語句讀起來很像英語:y gets log-x if x is greater than 0; otherwise it gets NaN
(如果x大于0,y的值則是x的log;否則y的值為NaN)。
有時(shí)候也可以使用條件表達(dá)式改寫遞歸函數(shù)。例如,下面是階乘函數(shù)的遞歸版本:
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)
條件表達(dá)式的另一個(gè)用處是處理可選參數(shù)。例如,下面是一個(gè)類的 init 方法:
def __init__(self, name, contents=None):
self.name = name
if contents == None:
contents = []
self.pouch_contents = contents
我們可以像這樣重寫:
def __init__(self, name, contents=None):
self.name = name
self.pouch_contents = [] if contents == None else contents
一般來說,如果條件語句的兩個(gè)分支中均為簡(jiǎn)單的表達(dá)式,不是被返回就是被賦值給相同的變量,那么你可以用條件表達(dá)式替換調(diào)該條件語句。
2. 列表推導(dǎo)式
def capitalize_all(t):
res = []
for s in t:
res.append(s.capitalize())
return res
我們可以使用列表推導(dǎo)式簡(jiǎn)化該函數(shù):
def capitalize_all(t):
return [s.capitalize() for s in t]
列表推導(dǎo)式也可以用于篩選。例如,這個(gè)函數(shù)只選擇 t 中為大寫的元素,并返回一個(gè)新列表:
def only_upper(t):
res = []
for s in t:
if s.isupper():
res.append(s)
return res
我們可以使用列表推導(dǎo)式重寫這個(gè)函數(shù):
def only_upper(t):
return [s for s in t if s.isupper()]
列表推導(dǎo)式非常簡(jiǎn)潔、易讀,至少對(duì)簡(jiǎn)單的表達(dá)式是這樣的。而且通常比對(duì)應(yīng)的 for 循環(huán)要更快,有時(shí)要快很多。
3. 生成器表達(dá)式
生成器表達(dá)式與列表推導(dǎo)式類似,但是使用的是圓括號(hào),而不是方括號(hào):
>>> g = (x**2 for x in range(5))
>>> g
<generator object <genexpr> at 0x7f4c45a786c0>
結(jié)果是一個(gè)表達(dá)式對(duì)象,該對(duì)象知道如何遍歷一個(gè)值序列。但與列舉推導(dǎo)式不同的是,它不會(huì)一次性計(jì)算出所有的值;而是等待求值請(qǐng)求。內(nèi)建函數(shù) next 從生成器獲取下一個(gè)值:
>>> next(g)
0
>>> next(g)
1
抵達(dá)序列的末尾時(shí),next會(huì)拋出StopIteration異常。你還可以使用for循環(huán)遍歷這些值:
>>> for val in g:
... print(val)
4
9
16
生成器對(duì)象會(huì)記錄其在序列中的位置,因此for循環(huán)是從next結(jié)束的地方開始的。一旦生成器被消耗完,它會(huì)拋出StopException。
>>> next(g)
StopIteration
生成器表達(dá)式常與sum、max和min等函數(shù)一起使用:
>>> sum(x**2 for x in range(5))
30
4.any 和 all
Python提供了一個(gè)內(nèi)建函數(shù) any,它接受一個(gè)布爾值序列,如果其中有任意一個(gè)值為 True 則返回 True 。它也適用于列表:
>>>> any([False, False, True])
True
但是它通常用于生成器表達(dá)式:
>>> any(letter == 't' for letter in 'monty')
True
上面這個(gè)例子不是很有用,因?yàn)樗墓δ芎?in 操作符一樣。但是我們可以使用 any 重寫搜索一節(jié)中的部分搜索函數(shù)。例如,我們可以像這樣編寫 avoids 函數(shù):
def avoids(word, forbidden):
return not any(letter in forbidden for letter in word)
上面的函數(shù)讀取來和英語沒什么區(qū)別:“word avoids forbidden if there are not any forbidden letters in word.”(如果某個(gè)詞中沒有任何禁用字母,那么該詞就算避免了使用禁用詞。)將 any 與生成器表達(dá)式結(jié)合使用的效率較高,因?yàn)樗灰挥龅秸嬷稻蜁?huì)終止,所以不會(huì)對(duì)整個(gè)序列進(jìn)行計(jì)算。Python還提供了另一個(gè)內(nèi)建函數(shù) all,如果序列中的每個(gè)元素均為 True 才會(huì)返回 True 。我們做個(gè)練習(xí),使用 all 重寫搜索一節(jié)中 uses_all 函數(shù)。
5. 集合
在字典差集一節(jié)中,我使用字典對(duì)那些在文檔中但不在單詞列表里的單詞進(jìn)行了查找。我寫的那個(gè)函數(shù)接受參數(shù) d1 和 d2 ,分別包含文檔中的單詞(作為鍵使用)和單詞列表。它返回不在 d2 中但在 d1 里的鍵組成的字典。
def subtract(d1, d2):
res = dict()
for key in d1:
if key not in d2:
res[key] = None
return res
在上面的字典中,所有鍵的值均為 None ,因?yàn)槲覀儾粫?huì)使用這些值。后果就是會(huì)浪費(fèi)一些存儲(chǔ)空間。Python提供了另一個(gè)叫做集合的內(nèi)建類型,它的行為類似沒有值的字典鍵集合。往集合中添加元素是非??斓模怀蓡T關(guān)系檢測(cè)也很快。另外,集合還提供了計(jì)算常見集合操作的方法和操作符。例如,集合差集就有一個(gè)對(duì)應(yīng)的 difference 方法,或者操作符 -。因此,我們可以這樣重寫 subtract 函數(shù):
def subtract(d1, d2):
return set(d1) - set(d2)
結(jié)果是一個(gè)集合,而不是字典,但對(duì)于像迭代這樣的操作而言,二者是沒有區(qū)別的。如果使用集合來完成本書中的部分練習(xí)題,代碼會(huì)比較簡(jiǎn)潔、高效。例如,下面是習(xí)題10-7中 has_duplicates 函數(shù)的一種使用字典的實(shí)現(xiàn):
def has_duplicates(t):
d = {}
for x in t:
if x in d:
return True
d[x] = True
return False
當(dāng)某個(gè)元素首次出現(xiàn)時(shí),它被添加至字典中。如果同樣的元素再次出現(xiàn),函數(shù)則返回 True 。如果使用集合,我們可以像這樣重寫該函數(shù):
def has_duplicates(t):
return len(set(t)) < len(t)
一個(gè)元素在集合中只能出現(xiàn)一次,因此如果 t 中的某個(gè)元素出現(xiàn)次數(shù)超過一次,那么集合的大小就會(huì)小于 t 。如果沒有重復(fù)的元素,集合和 t 的大小則相同。我們還可以使用集合完成第九章:文字游戲中的部分練習(xí)題。例如,下面是使用循環(huán)實(shí)現(xiàn)的 uses_only 函數(shù):
def uses_only(word, available):
for letter in word:
if letter not in available:
return False
return True
uses_only 檢查 word 中的所有字符也在 available 中。我們可以像這樣重寫該函數(shù):
def uses_only(word, available):
return set(word) <= set(available)
操作符 <= 檢查某個(gè)集合是否是另一個(gè)集合的子集或本身,包括了二者相等的可能性。如果 word 中所有的字符都出現(xiàn)在 available 中,則返回 True 。接下來做個(gè)練習(xí),使用集合重寫 avoids 函數(shù)。
6.計(jì)數(shù)器
計(jì)數(shù)器(Counter)類似集合,區(qū)別在于如果某個(gè)元素出現(xiàn)次數(shù)超過一次,計(jì)數(shù)器就會(huì)記錄其出現(xiàn)次數(shù)。如果你熟悉數(shù)學(xué)中的 多重集 概念,計(jì)數(shù)器就是用來表示一個(gè)多重集的自然選擇。計(jì)數(shù)器定義在叫做 collections 的標(biāo)準(zhǔn)模塊中,因此你必須首先導(dǎo)入該模塊。你可以通過字符串、列表或任何支持迭代的數(shù)據(jù)結(jié)構(gòu)來初始化計(jì)數(shù)器:
>>> from collections import Counter
>>> count = Counter('parrot')
>>> count
Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1})
計(jì)數(shù)器的行為與字典有很多相似的地方:它們將每個(gè)鍵映射至其出現(xiàn)的次數(shù)。與字典一樣,鍵必須是可哈希的。與字典不同的是,如果你訪問一個(gè)沒有出現(xiàn)過的元素,計(jì)數(shù)器不會(huì)拋出異常,而只是返回 0 :
>>> count['d']
0
我們可以使用計(jì)數(shù)器重寫習(xí)題10-6 中的 is_anagram 函數(shù):
def is_anagram(word1, word2):
return Counter(word1) == Counter(word2)
如果兩個(gè)單詞是變位詞,那么它們會(huì)包含相同的字符,而且字符的計(jì)數(shù)也相同,因此它們的計(jì)數(shù)器也是等價(jià)的。計(jì)數(shù)器提供了執(zhí)行類似集合操作的方法和操作符,包括集合添加、差集、并集和交集。另外,還提供了一個(gè)通常非常有用的方法 most_common ,返回一個(gè)由值-頻率對(duì)組成的列表,按照頻率高低排序:
>>> count = Counter('parrot')
>>> for val, freq in count.most_common(3):
... print(val, freq)
r 2
p 1
a 1
7. defaultdict
collections 模塊中還提供了一個(gè) defaultdict ,它類似字典,但是如果你訪問一個(gè)不存在的鍵,它會(huì)臨時(shí)生成一個(gè)新值。在創(chuàng)建 defaultdict 時(shí),你提供一個(gè)用于創(chuàng)建新值的函數(shù)。這個(gè)用于創(chuàng)建對(duì)象的函數(shù)有時(shí)也被稱為 工廠 。用于創(chuàng)建列表、集合和其他類型的內(nèi)建函數(shù)也可以用作工廠:
>>> from collections import defaultdict
>>> d = defaultdict(list)
請(qǐng)注意,這里的實(shí)參是 list ,它是一個(gè)類對(duì)象,而不是 list() ,后者是一個(gè)新列表。你提供的函數(shù)只有在訪問不存在的鍵時(shí),才會(huì)被調(diào)用。
>>> t = d['new key']
>>> t
[]
新列表 t 也被添加至字典中。因此如果我們修改 t ,改動(dòng)也會(huì)出現(xiàn)在 d 中。
>>> t.append('new value')
>>> d
defaultdict(<class 'list'>, {'new key': ['new value']})
如果你要?jiǎng)?chuàng)建一個(gè)列表組成的字典,通常你可以使用 defaultdict 來簡(jiǎn)化代碼。在習(xí)題12-2的答案(可從 http://thinkpython2.com/code/anagram_sets.py 處獲取)中,我創(chuàng)建的字典將排好序的字符串映射至一個(gè)可以由這些字符串構(gòu)成的單詞列表。例如,'opst' 映射至列表 ['opts', 'post', 'pots', 'spot', 'stop', 'tops'] 。
下面是代碼:
def all_anagrams(filename):
d = {}
for line in open(filename):
word = line.strip().lower()
t = signature(word)
if t not in d:
d[t] = [word]
else:
d[t].append(word)
return d
這個(gè)函數(shù)可以使用 setdefault 進(jìn)行簡(jiǎn)化,你可能在習(xí)題11-2中也用到了:
def all_anagrams(filename):
d = {}
for line in open(filename):
word = line.strip().lower()
t = signature(word)
d.setdefault(t, []).append(word)
return d
這種方案有一個(gè)缺點(diǎn),即不管是否需要,每次都會(huì)創(chuàng)建一個(gè)新列表。如果只是創(chuàng)建列表,這問題你不大,但是如果工廠函數(shù)非常復(fù)雜,就可能會(huì)成為一個(gè)大問題。
我們可以使用 defaultdict 來避免這個(gè)問題,同時(shí)簡(jiǎn)化代碼:
def all_anagrams(filename):
d = defaultdict(list)
for line in open(filename):
word = line.strip().lower()
t = signature(word)
d[t].append(word)
return d
習(xí)題18-3的答案(可從 http://thinkpython2.com/code/PokerHandSoln.py 下載)中,has_straightflush 函數(shù)使用了 setdefault 。這個(gè)答案的缺點(diǎn)就是每次循環(huán)時(shí)都會(huì)創(chuàng)建一個(gè) Hand 對(duì)象,不管是否需要。我們做個(gè)練習(xí),使用 defaultdict 改寫這個(gè)函數(shù)。
8. 命名元組
許多簡(jiǎn)單對(duì)象基本上就是相關(guān)值的集合。例如,第十五章:類和對(duì)象中定義的 Point 對(duì)象包含兩個(gè)數(shù)字 x 和 y 。當(dāng)你像下面這樣定義類時(shí),你通常先開始定義 init 和 str 方法:
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
但是編寫了這么多代碼,卻只傳遞了很少的信息。Python提供了一個(gè)更簡(jiǎn)潔的實(shí)現(xiàn)方式:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
第一個(gè)實(shí)參是你希望創(chuàng)建的類的名稱。第二個(gè)實(shí)參是 Point 對(duì)象應(yīng)該具備的屬性列表,以字符串的形式指定。 namedtuple 的返回值是一個(gè)類對(duì)象:
>>> Point
<class '__main__.Point'>
這里的 Point 自動(dòng)提供了像 __init__ 和 __str__ 這樣的方法,你沒有必須再自己編寫。
如果想創(chuàng)建一個(gè) Point 對(duì)象,你可以將 Point 類當(dāng)作函數(shù)使用:
>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
init 方法將實(shí)參賦值給你提供的屬性。str 方法打印 Point 對(duì)象的字符串呈現(xiàn)及其屬性。
你可以通過名稱訪問命令元組的元素:
>>> p.x, p.y
(1, 2)
但是你也可以把命名元組當(dāng)作元組使用:
>>> p[0], p[1]
(1, 2)
>>> x, y = p
>>> x, y
(1, 2)
命名元組是定義簡(jiǎn)單類的一種便捷方式。缺點(diǎn)是這些簡(jiǎn)單類不會(huì)一成不變。之后你可能會(huì)發(fā)現(xiàn)想要給命名元組添加更多的方法。在這種情況下,你可以定義一個(gè)繼承自命名元組的新類:
class Pointier(Point):
# add more methods here
或者使用傳統(tǒng)的類定義方式。
9. 匯集關(guān)鍵字實(shí)參
在可變長(zhǎng)度參數(shù)元組一節(jié)中,我們學(xué)習(xí)了如何編寫一個(gè)將實(shí)參匯集到元組的函數(shù):
def printall(*args):
print(args)
你可以使用任意數(shù)量的位置實(shí)參(即不帶關(guān)鍵字的參數(shù))調(diào)用該函數(shù):
>>> printall(1, 2.0, '3')
(1, 2.0, '3')
不過 * 星號(hào)操作符無法匯集關(guān)鍵字參數(shù):
>>> printall(1, 2.0, third='3')
TypeError: printall() got an unexpected keyword argument 'third'
如果要匯集關(guān)鍵字參數(shù),你可以使用 ** 雙星號(hào)操作符:
def printall(*args, **kwargs):
print(args, kwargs)
你可以給關(guān)鍵字匯集形參取任意的名稱,但是 kwargs 是常用名。上面函數(shù)的結(jié)果是一個(gè)將關(guān)鍵字映射至值的字典:
>>> printall(1, 2.0, third='3')
(1, 2.0) {'third': '3'}
如果你有一個(gè)有關(guān)鍵字和值組成的字典,可以使用分散操作符(scatter operator) ** 調(diào)用函數(shù):
>>> d = dict(x=1, y=2)
>>> Point(**d)
Point(x=1, y=2)
如果沒有分散操作符,函數(shù)會(huì)將 d 視為一個(gè)位置實(shí)參,因此會(huì)將 d 賦值給 x 并報(bào)錯(cuò),因?yàn)闆]有給 y 賦值:
>>> d = dict(x=1, y=2)
>>> Point(d)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __new__() missing 1 required positional argument: 'y'
在處理有大量形參的函數(shù)時(shí),通常可以創(chuàng)建指定了常用選項(xiàng)的字典,并將其傳入函數(shù)。
10. 術(shù)語表
條件表達(dá)式(conditional expression):
根據(jù)條件在兩個(gè)值中二選一的表達(dá)式。
列表推導(dǎo)式(list comprehension):
位于方括號(hào)中帶 for 循環(huán)的表達(dá)式,最終生成一個(gè)新列表。
生成器表達(dá)式(generator expression):
位于圓括號(hào)中帶 for 循環(huán)的表達(dá)式,最終生成一個(gè)生成器對(duì)象。
多重集(multiset):
一個(gè)數(shù)學(xué)概念,表示一個(gè)集合的元素與各元素出現(xiàn)次數(shù)之間的映射。
工廠(factory):
用于創(chuàng)建對(duì)象的函數(shù),通常作為形參傳入。