當前位置: 首頁‣ 深入 Python 3 ‣
難度級別: ♦♦♦♢♢
❝ 東是東,西是西,東西不相及 ❞
— 拉迪亞德·吉卜林
生成器是一類特殊 迭代器。 一個產生值的函數 yield 是一種產生一個迭代器卻不需要構建迭代器的精密小巧的方法。 我會告訴你我是什么意思。
記得 菲波拉稀生成器嗎? 這里是一個從無到有的迭代器:
class Fib:
'''生成菲波拉稀數列的迭代器'''
def __init__(self, max):
self.max = max
def __iter__(self):
self.a = 0
self.b = 1
return self
def __next__(self):
fib = self.a
if fib > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return fib
讓我們一行一行來分析。
class Fib:
類(class)?什么是類?
⁂
Python 是完全面向對象的:你可以定義自己的類,從你自己或系統自帶的類繼承,并生成實例。
在Python里定義一個類非常簡單。就像函數一樣, 沒有分開的接口定義。 只需定義類就開始編碼。 Python類以保留字 class 開始, 后面跟類名。 技術上來說,只需要這么多就夠了,因為一個類不是必須繼承其他類。
class PapayaWhip: ①
pass ②
PapayaWhip, 沒有從其他類繼承。 類名通常是大寫字母分隔, 如EachWordLikeThis, 但這只是個習慣,并非必須。
if 語句, for 循環, 或其他代碼塊。第一行非縮進代碼表示到了類外。
PapayaWhip 類沒有定義任何方法和屬性, 但依據句法,應該在定義中有東西,這就是 pass 語句。 這是Python 保留字,意思是“繼續,這里看不到任何東西”。 這是一個什么都不做的語句,是一個很好的占位符,如果你的函數和類什么都不想做(刪空函數或類)。
☞Python中的
pass就像Java 或 C中的空大括號對 ({}) 。
很多類繼承自其他類, 但這個類沒有。 很多類有方法,這個類也沒有。 Python 類不是必須有東西,除了一個名字。 特別是C++ 程序員發現 Python 類沒有顯式的構造和析構函數會覺得很古怪。 盡管不是必須, Python 類 可以 具有類似構造函數的東西: __init__() 方法。
__init__() 方法本示例展示 Fib 類使用 __init__ 方法。
class Fib:
'''生成菲波拉稀數列的迭代器''' ①
def __init__(self, max): ②
docstring, 與模塊和方法一樣。
__init__() 方法被立即調用。很容易將其——但技術上來說不正確——稱為該類的“構造函數” 。 很容易,因為它看起來很像 C++ 的構造函數(按約定,__init__() 是類中第一個被定義的方法),行為一致(是類的新實例中第一片被執行的代碼), 看起來完全一樣。 錯了, 因為__init__() 方法調用時,對象已經創建了,你已經有了一個合法類對象的引用。
每個方法的第一個參數,包括 __init__() 方法,永遠指向當前的類對象。 習慣上,該參數叫 self。 該參數和C++或Java中 this 角色一樣, 但 self 不是 Python的保留字, 僅僅是個命名習慣。 雖然如此,請不要取別的名字,只用 self; 這是一個很強的命名習慣。
在 __init__() 方法中, self 指向新創建的對象; 在其他類對象中, 它指向方法所屬的實例。盡管需在定義方法時顯式指定self ,調用方法時并 不 必須明確指定。 Python 會自動添加。
⁂
Python 中實例化類很直接。 實例化類時就像調用函數一樣簡單,將 __init__() 方法需要的參數傳入。 返回值就是新創建的對象。
>>> import fibonacci2
>>> fib = fibonacci2.Fib(100) ①
>>> fib ②
<fibonacci2.Fib object at 0x00DB8810>
>>> fib.__class__ ③
<class 'fibonacci2.Fib'>
>>> fib.__doc__ ④
'生成菲波拉稀數列的迭代器'
Fib 類的實例(在fibonacci2 模塊中定義) 將新創建的實例賦給變量fib。 你傳入一個參數 100, 這是Fib的__init__()方法作為max參數傳入的結束值。
Fib 的實例。
__class__, 它是該對象的類。 Java 程序員可能熟悉 Class 類, 包含方法如 getName() 和 getSuperclass() 獲取對象相關元數據。 Python里面, 這類元數據由屬性提供,但思想一致。
docstring ,就像函數或模塊中的一樣。 類的所有實例共享一份 docstring。
☞Python里面, 和調用函數一樣簡單的調用一個類來創建該類的新實例。 與C++ 或 Java不一樣,沒有顯式的
new操作符。
⁂
繼續下一行:
class Fib:
def __init__(self, max):
self.max = max ①
__init__() 方法的 max完全是兩回事。 self.max 是實例內 “全局” 的。 這意味著可以在其他方法中訪問它。
class Fib:
def __init__(self, max):
self.max = max ①
.
.
.
def __next__(self):
fib = self.a
if fib > self.max: ②
__init__() 方法中定義……
__next__() 方法中引用。
實例變量特定于某個類的實例。 例如, 如果你創建 Fib 的兩個具有不同最大值的實例, 每個實例會記住自己的值。
>>> import fibonacci2 >>> fib1 = fibonacci2.Fib(100) >>> fib2 = fibonacci2.Fib(200) >>> fib1.max 100 >>> fib2.max 200
⁂
現在 你已經準備學習如何創建一個迭代器了。 迭代器就是一個定義了 __iter__() 方法的類。
class Fib: ①
def __init__(self, max): ②
self.max = max
def __iter__(self): ③
self.a = 0
self.b = 1
return self
def __next__(self): ④
fib = self.a
if fib > self.max:
raise StopIteration ⑤
self.a, self.b = self.b, self.a + self.b
return fib ⑥
fib 應是一個類,而不是一個函數。
Fib(max) 會創建該類一個真實的實例,并以max做為參數調用__init__() 方法。 __init__() 方法以實例變量保存最大值,以便隨后的其他方法可以引用。
iter(fib)的時候,__iter__()就會被調用。(正如你等下會看到的, for 循環會自動調用它, 你也可以自己手動調用。) 在完成迭代器初始化后,(在本例中, 重置我們兩個計數器 self.a 和 self.b), __iter__() 方法能返回任何實現了 __next__() 方法的對象。 在本例(甚至大多數例子)中, __iter__() 僅簡單返回 self, 因為該類實現了自己的 __next__() 方法。
next()方法時,__next__() 會自動調用。 隨后會有更多理解。
__next__() 方法拋出 StopIteration 異常, 這是給調用者表示迭代用完了的信號。 和大多數異常不同, 這不是錯誤;它是正常情況,僅表示迭代器沒有值可產生了。 如果調用者是 for 循環, 它會注意到該 StopIteration 異常并優雅的退出。 (換句話說,它會吞掉該異常。) 這點神奇之處就是使用 for 的關鍵。
__next__() 方法簡單 return該值。 不要使用 yield ; 該語法上的小甜頭僅用于你使用生成器的時候。 這里你從無到有創建迭代器,使用 return 代替。
完全暈了? 太好了。 讓我們看如何調用該迭代器:
>>> from fibonacci2 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
為什么?完全一模一樣! 一字節一字節的與你調用 Fibonacci-as-a-generator (模塊第一個字母大寫)相同。但怎么做到的?
for 循環內有魔力。下面是究竟發生了什么:
for 循環調用 Fib(1000)。 這返回Fib 類的實例。 叫它 fib_inst。
for 循環調用 iter(fib_inst), 它返回迭代器。 叫它 fib_iter。 本例中, fib_iter == fib_inst, 因為 __iter__() 方法返回 self,但for 循環不知道(也不關心)那些。
for 循環調用 next(fib_iter), 它又調用 fib_iter對象的 __next__() 方法,產生下一個菲波拉稀計算并返回值。 for 拿到該值并賦給 n, 然后執行n值的 for 循環體。
for循環如何知道什么時候結束?很高興你問到。 當next(fib_iter) 拋出 StopIteration 異常時, for循環將吞下該異常并優雅退出。 (其他異常將傳過并如常拋出。) 在哪里你見過 StopIteration 異常? 當然在 __next__() 方法。
⁂
現在到曲終的時候了。我們重寫 復數規則生成器 為迭代器。
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8')
self.cache = []
def __iter__(self):
self.cache_index = 0
return self
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1]
if self.pattern_file.closed:
raise StopIteration
line = self.pattern_file.readline()
if not line:
self.pattern_file.close()
raise StopIteration
pattern, search, replace = line.split(None, 3)
funcs = build_match_and_apply_functions(
pattern, search, replace)
self.cache.append(funcs)
return funcs
rules = LazyRules()
因此這是一個實現了 __iter__() 和 __next__()的類。所以它可以 被用作迭代器。然后,你實例化它并將其賦給 rules 。這只發生一次,在import的時候。
讓我們一口一口來吃:
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8') ①
self.cache = [] ②
LazyRules 類時, 打開模式文件,但不讀取任何東西。 (隨后再進行)
__next__() 方法中) 。
我們繼續之前,讓我們近觀 rules_filename。它沒在 __iter__() 方法中定義。事實上,它沒在任何方法中定義。它定義于類級別。它是 類變量, 盡管訪問時和實例變量一樣 (self.rules_filename), LazyRules 類的所有實例共享該變量。
>>> import plural6 >>> r1 = plural6.LazyRules() >>> r2 = plural6.LazyRules() >>> r1.rules_filename ① 'plural6-rules.txt' >>> r2.rules_filename 'plural6-rules.txt' >>> r2.rules_filename = 'r2-override.txt' ② >>> r2.rules_filename 'r2-override.txt' >>> r1.rules_filename 'plural6-rules.txt' >>> r2.__class__.rules_filename ③ 'plural6-rules.txt' >>> r2.__class__.rules_filename = 'papayawhip.txt' ④ >>> r1.rules_filename 'papayawhip.txt' >>> r2.rules_filename ⑤ 'r2-overridetxt'
__class__ 屬性來訪問類屬性(于此相對的是單獨實例的屬性)。
現在回到我們的演示:
def __iter__(self): ①
self.cache_index = 0
return self ②
for 循環——調用 iter(rules)的時候,__iter__() 方法都會被調用。
__iter__() 方法都需要做的就是必須返回一個迭代器。 在本例中,返回 self,意味著該類定義了__next__() 方法,由它來關注整個迭代過程中的返回值。
def __next__(self): ①
.
.
.
pattern, search, replace = line.split(None, 3)
funcs = build_match_and_apply_functions( ②
pattern, search, replace)
self.cache.append(funcs) ③
return funcs
for 循環——調用 __next__() 方法, next(rules)都跟著被調用。 該方法僅在我們從后往前移動時比較好體會。所以我們就這么做。
build_match_and_apply_functions() 函數還沒修改;與它從前一樣。
self.cache。
從后往前移動……
def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise StopIteration ③
.
.
.
readline() 方法 (注意:是單數,不是復數 readlines()) 從一個打開的文件中精確讀取一行,即下一行。(文件對象同樣也是迭代器! 它自始至終是迭代器……)
readline() 可以讀, line 就不會是空字符串。 甚至文件包含一個空行, line 將會是一個字符的字符串 '\n' (回車換行符)。 如果 line 是真的空字符串, 就意味著文件已經沒有行可讀了。
StopIteration 異常。 記住,開門見山的說是因為我們需要為下一條規則找到一個匹配和應用功能。下一條規則從文件的下一行獲取…… 但已經沒有下一行了! 所以,我們沒有規則返回。 迭代器結束。 (♫ 派對結束 ♫)
由后往前直到 __next__()方法的開始……
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1] ①
if self.pattern_file.closed:
raise StopIteration ②
.
.
.
self.cache 將是一個我們匹配并應用單獨規則的功能列表。 (至少那個應該看起來熟悉!) self.cache_index 記錄我們下一步返回的緩存條目。 如果我們還沒有耗盡緩存 (舉例 如果 self.cache 的長度大于 self.cache_index),那么我們就會命中一條緩存! 哇! 我們可以從緩存中返回匹配和應用功能而不是從無到有創建。
放到一起,發生了什么事? 當:
LazyRules 類的一個單一實例, 叫 rules, 它打開模式文件但并沒有讀取。
plural() 函數來讓一個不同的單詞變復數。 plural() 函數中的for 循環會調用iter(rules),這會重置緩存索引但不會重置打開的文件對象。
for循環會從rules中索要一個值,該值會調用其__next__()方法。然而這一次, 緩存已經被裝入了一個匹配和應用功能對, 與模式文件中第一行模式一致。 由于對前一個單詞做復數變換時已經被創建和緩存,它們被從緩存中返回。 緩存索引遞增,打開的文件無需訪問。
for 循環再次運轉并從 rules請求一個值。 這會再次調用 __next__() 方法。 這一次, 緩存被用完了——它僅有一個條目,而我們被請求第二個——于是 __next__() 方法繼續。 從打開的文件中讀取下一行,從模式中創建匹配和應用功能,并緩存之。
readline() 命令的地方。現在,緩存已經有更多條目了,并且再次從頭開始來將一個新單詞變復數,在讀取模式文件下一行之前,緩存中的每一個條目都將被嘗試。
我們已經到達復數變換的極樂世界。
import 時發生的唯一的事就是實例化一個單一的類并打開一個文件(但并不讀取)。
☞這真的是極樂世界? 嗯,是或不是。 這里有一些
LazyRules示例需要細想的地方: 模式文件被打開(在__init__()中),并持續打開直到讀取最后一個規則。 當Python退出或最后一個LazyRules類的實例銷毀,Python 會最終關閉文件,但是那仍然可能會是一個很長的時間。如果該類是一個“長時間運行”的Python進程的一部分,Python可能從不退出,LazyRules對象就可能一直不會釋放。這種情況有解決辦法。 不要在
__init__()中打開文件并讓其在一行一行讀取規則時一直打開,你可以打開文件,讀取所有規則,并立即關閉文件。或你可以打開文件,讀取一條規則,用tell()方法保存文件位置,關閉文件,后面再次打開它,使用seek()方法 繼續從你離開的地方讀取。 或者你不需擔心這些就讓文件打開,如同本示例所做。 編程即是設計, 而設計牽扯到所有的權衡和限制。讓一個文件一直打開太長時間可能是問題;讓你代碼太復雜也可能是問題。哪一個是更大的問題,依賴于你的開發團隊,你的應用,和你的運行環境。
⁂
© 2001–9 Mark Pilgrim