Python代碼進(jìn)階小技巧

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á)式常與summaxmin等函數(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ù) d1d2 ,分別包含文檔中的單詞(作為鍵使用)和單詞列表。它返回不在 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-7has_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ù)字 xy 。當(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ù),通常作為形參傳入。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容