當前位置: 首頁 ? 深入 Python 3 ?
難度級別: ?????
? My spelling is Wobbly. It’s good spelling but it Wobbles, and the letters get in the wrong places. ?
— Winnie-the-Pooh
出于傳遞所有理解的原因,我一直對語言非常著迷。我指的不是編程語言。好吧,是編程語言,但同時也是自然語言。使用英語。英語是一種七拼八湊的語言,它從德語、法語、西班牙語和拉丁語(等等)語言中借用了大量詞匯。事實上,“借用”是不恰當的詞匯,“掠奪”更加符合。或者也許叫“同化“——就像博格人(譯注:根據維基百科資料,Borg 是《星際旅行》虛構宇宙中的一個種族,該譯法未經原作者映證)。是的,我喜歡這樣。
我們就是博格人。你們的語言和詞源特性將會被添加到我們自己的當中。抵抗是徒勞的。
在本章中,將開始學習復數名詞。以及返回其它函數的函數、高級正則表達式和生成器。但首先,讓我們聊聊如何生成復數名詞。(如果還沒有閱讀《正則表達式》一章,現在也許是個好時機讀一讀。本章將假定您理解了正則表達式的基礎,并迅速進入更高級的用法。)
如果在講英語的國家長大,或在正規的學校學習過英語,您可能對下面的基本規則很熟悉 :
(我知道,還有許多例外情況。Man 變成 men 而 woman 變成 women,但是 human 變成 humans。Mouse 變成 mice ; louse 變成 lice,但 house 變成 houses。Knife 變成 knives ;wife 變成 wives,但是 lowlife 變成 lowlifes。而且甚至我還沒有開始提到那些原型和復數形式相同的單詞,就像 sheep、 deer 和 haiku。)
其它語言,當然是完全不同的。
讓我們設計一個 Python 類庫用來自動進行英語名詞的復數形式轉換。我們將以這四條規則為起點,但要記住的不可避免地還要增加更多規則。
?
因此,您正在看著單詞,至少是英語單詞,也就是說您正在看著字符的字符串。規則說你必須找到不同的字符組合,然后進行不同的處理。這聽起來是正則表達式的工作!
import re
def plural(noun):
if re.search('[sxz]$', noun): ①
return re.sub('$', 'es', noun) ②
elif re.search('[^aeioudgkprt]h$', noun):
return re.sub('$', 'es', noun)
elif re.search('[^aeiou]y$', noun):
return re.sub('y$', 'ies', noun)
else:
return noun + 's'
[sxz] 的意思是: “s、 x 或 z”,但只匹配其中之一。對 $ 應該很熟悉了,它匹配字符串的結尾。經過組合,該正則表達式將測試 noun 是否以 s、 x 或 z 結尾。re.sub() 函數執行基于正則表達式的字符串替換。讓我們看看正則表達式替換的細節。
>>> import re
>>> re.search('[abc]', 'Mark') ①
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark') ②
'Mork'
>>> re.sub('[abc]', 'o', 'rock') ③
'rook'
>>> re.sub('[abc]', 'o', 'caps') ④
'oops'
Mark 包含 a、 b 或 c 嗎?是的,它包含 a 。a、 b 或 c,并將其替換為 o。Mark 變成了 Mork。rock 轉換為 rook 。caps 轉換為 oaps,但實際上并是這樣。re.sub 替換 所有的 匹配項,而不僅僅是第一個匹配項。因此該正則表達式將 caps 轉換為 oops,因為無論是 c 還是 a 均被轉換為 o 。接下來,回到 plural() 函數……
def plural(noun):
if re.search('[sxz]$', noun):
return re.sub('$', 'es', noun) ①
elif re.search('[^aeioudgkprt]h$', noun): ②
return re.sub('$', 'es', noun)
elif re.search('[^aeiou]y$', noun): ③
return re.sub('y$', 'ies', noun)
else:
return noun + 's'
$ 匹配)替換為字符串 es 。換句話來說,向字符串尾部添加一個 es 。可以通過字符串鏈接來完成同樣的變化,例如 noun + 'es',但我對每條規則都選用正則表達式,其原因將在本章稍后更加清晰。^ 有特別的含義:非。[^abc] 的意思是:“ 除了 a、 b 或 c 之外的任何字符”。因此 [^aeioudgkprt] 的意思是除了 a、 e、 i、 o、 u、 d、 g、 k、 p、r 或 t 之外的任何字符。然后該字符必須緊隨一個 h,其后是字符串的結尾。所匹配的是以 H 結尾且 H 發音的單詞。a、 e、 i、 o 或 u。所匹配的是以 Y 結尾,且 Y 發音聽起來像 I 的單詞。讓我們看看“否定”正則表達式的更多細節。
>>> import re
>>> re.search('[^aeiou]y$', 'vacancy') ①
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy') ②
>>>
>>> re.search('[^aeiou]y$', 'day')
>>>
>>> re.search('[^aeiou]y$', 'pita') ③
>>>
vacancy 匹配該正則表達式,因為它以 cy 結尾,且 c 并非 a、 e、 i、 o 或 u。boy 不匹配,因為它以 oy 結尾,可以明確地說 y 之前的字符不能是 o 。day 不匹配,因為它以 ay 結尾。pita 不匹配,因為它不以 y 結尾。
>>> re.sub('y$', 'ies', 'vacancy') ①
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy') ②
'vacancies'
vacancy 轉換為 vacancies ,將 agency 轉換為 agencies,這正是想要的結果。注意,它也會將 boy 轉換為 boies,但這永遠也不會在函數中發生,因為我們首先進行了 re.search 以找出永遠不應進行該 re.sub 操作的單詞。y 之前的字符。然后在替換字符串中,用到了新的語法: \1,它表示“嘿,記住的第一個分組呢?把它放到這里。”在此例中, 記住了 y 之前的 c ,在進行替換時,將用 c 替代 c,用 ies 替代 y 。(如果有超過一個的記憶分組,可以使用 \2 和 \3 等等。)正則表達式替換功能非常強大,而 \1 語法則使之愈加強大。但是,將整個操作組合成一條正則表達式也更難閱讀,而且也沒有直接映射到剛才所描述的復數規則。剛才所闡述的規則,像 “如果單詞以 S 、X 或 Z 結尾,則添加 ES 。”如果查看該函數,有兩行代碼都在表述“如果以 S 、X 或 Z 結尾,那么添加 ES 。”它沒有之前那種模式更直接。
?
現在要增加一些抽象層次的內容。我們開始時定義了一系列規則:如果這樣,那樣做;否則前往下一條規則。現在讓我們對部分程序進行臨時的復雜化,以簡化另一部分。
import re
def match_sxz(noun):
return re.search('[sxz]$', noun)
def apply_sxz(noun):
return re.sub('$', 'es', noun)
def match_h(noun):
return re.search('[^aeioudgkprt]h$', noun)
def apply_h(noun):
return re.sub('$', 'es', noun)
def match_y(noun): ①
return re.search('[^aeiou]y$', noun)
def apply_y(noun): ②
return re.sub('y$', 'ies', noun)
def match_default(noun):
return True
def apply_default(noun):
return noun + 's'
rules = ((match_sxz, apply_sxz), ③
(match_h, apply_h),
(match_y, apply_y),
(match_default, apply_default)
)
def plural(noun):
for matches_rule, apply_rule in rules: ④
if matches_rule(noun):
return apply_rule(noun)
re.search() 函數調用結果。re.sub() 函數以應用恰當的復數變化規則。rules 數據結構——一個函數對的序列,而不是一個函數(plural())實現多個條規則。plural() 函數可以減少到幾行代碼。使用 for 循環,可以一次性從 rules 這個數據結構中取出匹配規則和應用規則這兩樣東西(一條匹配對應一條應用)。在 for 循環的第一次迭代過程中, matches_rule 將獲取 match_sxz,而 apply_rule 將獲取 apply_sxz。在第二次迭代中(假定可以進行到這一步), matches_rule 將會賦值為 match_h,而 apply_rule 將會賦值為 apply_h 。該函數確保最終能夠返回某個值,因為終極匹配規則 (match_default) 只返回 True,意思是對應的應用規則 (apply_default) 將總是被應用。該技術能夠成功運作的原因是 Python 中一切都是對象,包括了函數。數據結構 rules 包含了函數——不是函數的名稱,而是實際的函數對象。在 for 循環中被賦值后,matches_rule 和 apply_rule 是可實際調用的函數。在第一次 for 循環的迭代過程中,這相當于調用 matches_sxz(noun),如果返回一個匹配值,將調用 apply_sxz(noun) 。
如果這種附加抽象層令你迷惑,可以試著展開函數以了解其等價形式。整個 for 循環等價于下列代碼:
def plural(noun):
if match_sxz(noun):
return apply_sxz(noun)
if match_h(noun):
return apply_h(noun)
if match_y(noun):
return apply_y(noun)
if match_default(noun):
return apply_default(noun)
這段代碼的好處是 plural() 函數被簡化了。它處理一系列其它地方定義的規則,并以通用的方式對它們進行迭代。
這些規則可在任何地方以任何方式定義。plural() 函數并不關心。
現在,新增的抽象層是否值得呢?嗯,還沒有。讓我們考慮下要向函數中新增一條規則時該如何操作。在第一例中,將需要新增一條 if 語句到 plural() 函數中。在第二例中,將需要新增兩個函數, match_foo() 和 apply_foo(),然后更新 rules 序列以指定新的匹配和應用函數按照其它規則按順序調用。
但是對于下一節來說,這只是一個跳板而已。讓我們繼續……
?
其實并不是真的有必要為每個匹配和應用規則定義各自的命名函數。它們從未直接被調用,而只是被添加到 rules 序列并從該處被調用。此外,每個函數遵循兩種模式的其中之一。所有的匹配函數調用 re.search(),而所有的應用函數調用 re.sub()。讓我們將模式排除在考慮因素之外,使新規則定義更加簡單。
import re
def build_match_and_apply_functions(pattern, search, replace):
def matches_rule(word): ①
return re.search(pattern, word)
def apply_rule(word): ②
return re.sub(search, replace, word)
return (matches_rule, apply_rule) ③
build_match_and_apply_functions() 函數用于動態創建其它函數。它接受 pattern、 search 和 replace 三個參數,并定義了 matches_rule() 函數,該函數通過傳給 build_match_and_apply_functions() 函數的 pattern 及傳遞給所創建的 matchs_rules() 函數的 word 調用 re.search() 函數,哇。build_match_and_apply_functions() 函數的 search 和 replace 參數、以及傳遞給要創建 apply_rule() 函數的 word 調用 re.sub()。在動態函數中使用外部參數值的技術稱為 閉合【closures】。基本上,常量的創建工作都在創建應用函數過程中完成:它接受一個參數 (word),但實際操作還加上了另外兩個值(search 和 replace),該兩個值都在定義應用函數時進行設置。build_match_and_apply_functions() 函數返回一個包含兩個值的元組:即剛才所創建的兩個函數。在這些函數中定義的常量( match_rule() 函數中的 pattern 函數,apply_rule() 函數中的 search 和 replace )與這些函數呆在一起,即便是在從 build_match_and_apply_functions() 中返回后也一樣。這真是非常酷的一件事情。但如果此方式導致了難以置信的混亂(應該是這樣,它確實有點奇怪),在看看如何使用之后可能會清晰一些。
patterns = \ ①
(
('[sxz]$', '$', 'es'),
('[^aeioudgkprt]h$', '$', 'es'),
('(qu|[^aeiou])y$', 'y$', 'ies'),
('$', '$', 's') ②
)
rules = [build_match_and_apply_functions(pattern, search, replace) ③
for (pattern, search, replace) in patterns]
re.search() 中用于判斷該規則是否匹配的正則表達式。各組中的第二和第三個字符串是在 re.sub() 中將實際用于使用規則將名詞轉換為復數形式的搜索和替換表達式。match_default() 函數僅返回 True,意思是如果更多的指定規則無一匹配,代碼將簡單地向給定詞匯的尾部添加一個 s。本例則進行了一些功能等同的操作。最后的正則表達式詢問單詞是否有一個結尾($ 匹配字符串的結尾)。當然,每個字符串都有一個結尾,甚至是空字符串也有,因此該規則將始終被匹配。因此,它實現了 match_default() 函數同樣的目的,始終返回 True:它確保了如果沒有更多的指定規則用于匹配,代碼將向給定單詞的尾部增加一個 s 。build_match_and_apply_functions() 函數。也就是說,它接受每組三重字符串為參數,并將該三個字符串作為實參調用 build_match_and_apply_functions() 函數。 build_match_and_apply_functions() 函數返回一個包含兩個函數的元組。也就是說該 規則 最后的結尾與前例在功能上是等價的:一個元組列表,每個元組都是一對函數。第一個函數是調用 re.search() 的匹配函數;而第二個函數調用 re.sub() 的應用函數。此版本腳本的最前面是主入口點—— plural() 函數。
def plural(noun):
for matches_rule, apply_rule in rules: ①
if matches_rule(noun):
return apply_rule(noun)
plural() 函數基本沒有發生變化。它是完全通用的,它以規則函數列表為參數,并按照順序調用它們。它并不關系規則是如何定義的。在前例中,它們被定義為各自命名的函數。現在它們通過將 build_match_and_apply_functions() 函數的輸出映射為源字符串的列表來動態創建。這沒有任何關系; plural() 函數將以同樣方式運作。?
目前,已經排除了重復代碼,增加了足夠的抽象性,因此復數形式規則可以字符串列表的形式進行定義。下一個邏輯步驟是將這些字符串放入一個單獨的文件中,因此可獨立于使用它們的代碼來進行維護。
首先,讓我們創建一份包含所需規則的文本文件。沒有花哨的數據結構,只有空白符分隔的三列字符串。將其命名為 plural4-rules.txt.
[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s
下面看看如何使用該規則文件。
import re
def build_match_and_apply_functions(pattern, search, replace): ①
def matches_rule(word):
return re.search(pattern, word)
def apply_rule(word):
return re.sub(search, replace, word)
return (matches_rule, apply_rule)
rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file: ②
for line in pattern_file: ③
pattern, search, replace = line.split(None, 3) ④
rules.append(build_match_and_apply_functions( ⑤
pattern, search, replace))
build_match_and_apply_functions() 函數沒有發生變化。仍然使用了閉合技術:通過外部函數中定義的變量來動態創建兩個函數。open() 函數打開文件并返回一個文件對象。此例中,將要打開的文件包含了名詞復數形式的模式字符串。with 語句創建了叫做 context【上下文】的東西:當 with 塊結束時,Python 將自動關閉文件,即便是在 with 塊中引發了例外也會這樣。在 《文件》 一章中將學到關于 with 塊和文件對象的更多內容。for line in <fileobject> 代碼從打開的文件中讀取數據,并將文本賦值給 line 變量。在 《文件》 一章中將學到更多關于讀取文件的內容。split() 。split() 方法的第一個參數是 None,表示“對任何空白字符進行分隔(制表符或空白,沒有區別)”。第二個參數是 3,意思是“針對空白分隔三次,丟棄該行剩下的部分。”像 [sxz]$ $ es 這樣的行將被分割為列表 ['[sxz]$', '$', 'es'],意思是 pattern 獲得值 '[sxz]$', search 獲得值 '$',而 replace 獲得值 'es'。對于短短的一行代碼來說確實威力夠大的。pattern 、 search 和 replace 傳入 build_match_and_apply_functions() 函數,它將返回一個函數的元組。將該元組添加到 rules 列表,最終 rules 將儲存 plural() 函數所預期的匹配和應用函數列表。此處的改進是將復數形式規則獨立地放到了一份外部文件中,因此可獨立于使用它的代碼單獨對規則進行維護。代碼是代碼,數據是數據,生活更美好。
?
如果有個通用 plural() 函數解析規則文件不就更棒了嗎?獲取規則,檢查匹配,應用相應的轉換,進入下一條規則。這是 plural() 函數所必須完成的事,也是 plural() 函數必須做的事。
def rules(rules_filename):
with open(rules_filename, encoding='utf-8') as pattern_file:
for line in pattern_file:
pattern, search, replace = line.split(None, 3)
yield build_match_and_apply_functions(pattern, search, replace)
def plural(noun, rules_filename='plural5-rules.txt'):
for matches_rule, apply_rule in rules(rules_filename):
if matches_rule(noun):
return apply_rule(noun)
raise ValueError('no matching rule for {0}'.format(noun))
這段代碼到底是如何運作的?讓我們先看一個交互式例子。
>>> def make_counter(x):
... print('entering make_counter')
... while True:
... yield x ①
... print('incrementing x')
... x = x + 1
...
>>> counter = make_counter(2) ②
>>> counter ③
<generator object at 0x001C9C10>
>>> next(counter) ④
entering make_counter
2
>>> next(counter) ⑤
incrementing x
3
>>> next(counter) ⑥
incrementing x
4
make_counter 中出現的 yield 命令的意思是這不是一個普通的函數。它是一次生成一個值的特殊類型函數。可以將其視為可恢復函數。調用該函數將返回一個可用于生成連續 x 值的 生成器【Generator】。make_counter 生成器的實例,僅需像調用其它函數那樣對它進行調用。注意該調用并不實際執行函數代碼。可以這么說,是因為 make_counter() 函數的第一行調用了 print(),但實際并未打印任何內容。make_counter() 函數返回了一個生成器對象。next() 函數以一個生成器對象為參數,并返回其下一個值。對 counter 生成器第一次調用 next() ,它針對第一條 yield 語句執行 make_counter() 中的代碼,然后返回所產生的值。在此情況下,該代碼輸出將為 2,因其僅通過調用 make_counter(2) 對生成器進行初始創建。next() 將確切地從上次調用的位置開始繼續,直到下一條 yield 語句。所有的變量、局部數據等內容在 yield 時被保存,在 next() 時被恢復。下一行代碼等待被執行以調用 print() 以打印出 incrementing x 。之后,執行語句 x = x + 1。然后它繼續通過 while 再次循環,而它再次遇上的第一條語句是 yield x,該語句將保存所有一切狀態,并返回當前 x 的值(當前為 3)。next(counter) 時,又進行了同樣的工作,但這次 x 為 4。由于 make_counter 設置了一個無限循環,理論上可以永遠執行該過程,它將不斷遞增 x 并輸出數值。還是讓我們看一個更加實用的生成器用法。
def fib(max):
a, b = 0, 1 ①
while a < max:
yield a ②
a, b = b, a + b ③
1 開始,初始時上升緩慢,但越來越快。啟動該序列需要兩個變量:從 0 開始的 a,和從 1 開始的 b 。a + b) 并將其賦值給 b 以供稍后使用。注意該步驟是并行發生的;如果 a 為 3 且 b 為 5,那么 a, b = b, a + b 將會把 a 設置 5 (b 之前的值),將 b 設置為 8 ( a 和 b 之前值的和)。因此,現在有了一個連續輸出斐波那契數值的函數。當然,還可以使用遞歸來完成該功能,但這個方式更易于閱讀。同樣,它也與 for 循環合作良好。
>>> from fibonacci import fib >>> for n in fib(1000): ① ... print(n, end=' ') ② 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 >>> list(fib(1000)) ③ [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
for 循環中直接使用像 fib() 這樣的生成器。for 循環將會自動調用 next() 函數,從 fib() 生成器獲取數值并賦值給 for 循環索引變量。(n)for 循環, n 從 fib() 的 yield 語句獲取一個新值,所需做的僅僅是輸出它。一旦 fib() 的數字用盡(a 大于 max,即本例中的 1000), for 循環將會自動退出。list() 函數,它將遍歷整個生成器(就像前例中的 for 循環)并返回所有數值的列表。讓我們回到 plural5.py 看看該版本的 plural() 函數是如何運作的。
def rules(rules_filename):
with open(rules_filename, encoding='utf-8') as pattern_file:
for line in pattern_file:
pattern, search, replace = line.split(None, 3) ①
yield build_match_and_apply_functions(pattern, search, replace) ②
def plural(noun, rules_filename='plural5-rules.txt'):
for matches_rule, apply_rule in rules(rules_filename): ③
if matches_rule(noun):
return apply_rule(noun)
raise ValueError('no matching rule for {0}'.format(noun))
line.split(None, 3) 獲取三個“列”的值并將它們賦值給三個局部變量。build_match_and_apply_functions() 動態創建的兩個函數,這與之前的例子是一樣的。換而言之, rules() 是按照需求連續生成匹配和應用函數的生成器。rules() 是生成器,可直接在 for 循環中使用它。對 for 循環的第一次遍歷,可以調用 rules() 函數打開模式文件,讀取第一行,從該行的模式動態創建一個匹配函數和應用函數,然后生成動態創建的函數。對 for 循環的第二次遍歷,將會精確地回到 rules() 中上次離開的位置(在 for line in pattern_file 循環的中間)。要進行的第一項工作是讀取文件(仍處于打開狀態)的下一行,基于該行的模式動態創建另一匹配和應用函數,然后生成兩個函數。通過第四步獲得了什么呢?啟動時間。在第四步中引入 plural4 模塊時,它讀取了整個模式文件,并創建了一份所有可能規則的列表,甚至在考慮調用 plural() 函數之前。有了生成器,可以輕松地處理所有工作:可以讀取規則,創建函數并試用它們,如果該規則可用甚至可以不讀取文件剩下的部分或創建更多的函數。
失去了什么?性能!每次調用 plural() 函數,rules() 生成器將從頭開始——這意味著重新打開模式文件,并從頭開始讀取,每次一行。
要是能夠兩全其美多好啊:最低的啟動成本(無需對 import 執行任何代碼),同時 最佳的性能(無需一次次地創建同一函數)。哦,還需將規則保存在單獨的文件中(因為代碼和數據要涇渭分明),還有就是永遠不必兩次讀取同一行。
要實現該目標,必須建立自己的生成器。在進行此工作之前,必須對 Python 的類進行學習。
?
? 2001–9 Mark Pilgrim