當前位置: 首頁 深入 Python 3

難度級別: ♦♦♦♢♢

&迭代器

東是東,西是西,東西不相及
拉迪亞德·吉卜林

 

深入

生成器是一類特殊 迭代器。 一個產生值的函數 yield 是一種產生一個迭代器卻不需要構建迭代器的精密小巧的方法。 我會告訴你我是什么意思。

記得 菲波拉稀生成器嗎? 這里是一個從無到有的迭代器:

[下載 fibonacci2.py]

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           
  1. 類名是 PapayaWhip, 沒有從其他類繼承。 類名通常是大寫字母分隔, 如EachWordLikeThis, 但這只是個習慣,并非必須。
  2. 你可能猜到,類內部的內容都需縮進,就像函數中的代碼一樣, if 語句, for 循環, 或其他代碼塊。第一行非縮進代碼表示到了類外。

PapayaWhip 類沒有定義任何方法和屬性, 但依據句法,應該在定義中有東西,這就是 pass 語句。 這是Python 保留字,意思是“繼續,這里看不到任何東西”。 這是一個什么都不做的語句,是一個很好的占位符,如果你的函數和類什么都不想做(刪空函數或類)。

Python中的pass 就像Java 或 C中的空大括號對 ({}) 。

很多類繼承自其他類, 但這個類沒有。 很多類有方法,這個類也沒有。 Python 類不是必須有東西,除了一個名字。 特別是C++ 程序員發現 Python 類沒有顯式的構造和析構函數會覺得很古怪。 盡管不是必須, Python 類 可以 具有類似構造函數的東西: __init__() 方法。

__init__() 方法

本示例展示 Fib 類使用 __init__ 方法。

class Fib:
    '''生成菲波拉稀數列的迭代器'''  

    def __init__(self, max):      
  1. 類同樣可以 (而且應該) 具有docstring, 與模塊和方法一樣。
  2. 類實例創建后,__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__                
'生成菲波拉稀數列的迭代器'
  1. 你正創建一個 Fib 類的實例(在fibonacci2 模塊中定義) 將新創建的實例賦給變量fib。 你傳入一個參數 100, 這是Fib__init__()方法作為max參數傳入的結束值。
  2. fibFib 的實例。
  3. 每個類實例具有一個內建屬性, __class__, 它是該對象的類。 Java 程序員可能熟悉 Class 類, 包含方法如 getName()getSuperclass() 獲取對象相關元數據。 Python里面, 這類元數據由屬性提供,但思想一致。
  4. 你可訪問對象的 docstring ,就像函數或模塊中的一樣。 類的所有實例共享一份 docstring

Python里面, 和調用函數一樣簡單的調用一個類來創建該類的新實例。 與C++ 或 Java不一樣,沒有顯式的 new 操作符。

實例變量

繼續下一行:

class Fib:
    def __init__(self, max):
        self.max = max        
  1. self.max是什么? 它就是實例變量。 與作為參數傳入 __init__() 方法的 max完全是兩回事。 self.max 是實例內 “全局” 的。 這意味著可以在其他方法中訪問它。
class Fib:
    def __init__(self, max):
        self.max = max        
    .
    .
    .
    def __next__(self):
        fib = self.a
        if fib > self.max:    
  1. self.max__init__() 方法中定義……
  2. ……在 __next__() 方法中引用。

實例變量特定于某個類的實例。 例如, 如果你創建 Fib 的兩個具有不同最大值的實例, 每個實例會記住自己的值。

>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200

菲波拉稀迭代器

現在 你已經準備學習如何創建一個迭代器了。 迭代器就是一個定義了 __iter__() 方法的類。

[下載 fibonacci2.py]

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                                
  1. 從無到有創建一個迭代器, fib 應是一個類,而不是一個函數。
  2. “調用” Fib(max) 會創建該類一個真實的實例,并以max做為參數調用__init__() 方法。 __init__() 方法以實例變量保存最大值,以便隨后的其他方法可以引用。
  3. 當有人調用iter(fib)的時候,__iter__()就會被調用。(正如你等下會看到的, for 循環會自動調用它, 你也可以自己手動調用。) 在完成迭代器初始化后,(在本例中, 重置我們兩個計數器 self.aself.b), __iter__() 方法能返回任何實現了 __next__() 方法的對象。 在本例(甚至大多數例子)中, __iter__() 僅簡單返回 self, 因為該類實現了自己的 __next__() 方法。
  4. 當有人在迭代器的實例中調用next()方法時,__next__() 會自動調用。 隨后會有更多理解。
  5. __next__() 方法拋出 StopIteration 異常, 這是給調用者表示迭代用完了的信號。 和大多數異常不同, 這不是錯誤;它是正常情況,僅表示迭代器沒有值可產生了。 如果調用者是 for 循環, 它會注意到該 StopIteration 異常并優雅的退出。 (換句話說,它會吞掉該異常。) 這點神奇之處就是使用 for 的關鍵。
  6. 為了分離出下一個值, 迭代器的 __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 循環內有魔力。下面是究竟發生了什么:

復數規則迭代器

現在到曲終的時候了。我們重寫 復數規則生成器 為迭代器。

[下載plural6.py]

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 = []                                                  
  1. 當我們實例化 LazyRules 類時, 打開模式文件,但不讀取任何東西。 (隨后再進行)
  2. 打開模式文件之后,初始化緩存。 隨后讀取模式文件行的時候會用到它(在 __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'
  1. 類的每個實例繼承了 rules_filename 屬性及它在類中定義的值。
  2. 修改一個實例屬性的值不影響其他實例……
  3. ……也不會修改類的屬性。可以使用特殊的 __class__ 屬性來訪問類屬性(于此相對的是單獨實例的屬性)。
  4. 如果修改類屬性, 所有仍然繼承該實例的值的實例 (如這里的r1 ) 會受影響。
  5. 已經覆蓋(overridden)了該屬性(如這里的 r2 )的所有實例 將不受影響。

現在回到我們的演示:

    def __iter__(self):       
        self.cache_index = 0
        return self           
  1. 無論何時有人——如 for 循環——調用 iter(rules)的時候,__iter__() 方法都會被調用。
  2. 每個__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
  1. 無論何時有人——如 for 循環——調用 __next__() 方法, next(rules)都跟著被調用。 該方法僅在我們從后往前移動時比較好體會。所以我們就這么做。
  2. 函數的最后一部分至少應該眼熟。 build_match_and_apply_functions() 函數還沒修改;與它從前一樣。
  3. 唯一的不同是,在返回匹配和應用功能之前(保存在元組 funcs中),我們將其保存到 self.cache

從后往前移動……

    def __next__(self):
        .
        .
        .
        line = self.pattern_file.readline()  
        if not line:                         
            self.pattern_file.close()
            raise StopIteration              
        .
        .
        .
  1. 這里有點高級文件操作的技巧。 readline() 方法 (注意:是單數,不是復數 readlines()) 從一個打開的文件中精確讀取一行,即下一行。(文件對象同樣也是迭代器! 它自始至終是迭代器……
  2. 如果有一行 readline() 可以讀, line 就不會是空字符串。 甚至文件包含一個空行, line 將會是一個字符的字符串 '\n' (回車換行符)。 如果 line 是真的空字符串, 就意味著文件已經沒有行可讀了。
  3. 當我們到達文件尾時, 我們應關閉文件并拋出神奇的 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                         
        .
        .
        .
  1. self.cache 將是一個我們匹配并應用單獨規則的功能列表。 (至少那個應該看起來熟悉!) self.cache_index 記錄我們下一步返回的緩存條目。 如果我們還沒有耗盡緩存 (舉例 如果 self.cache 的長度大于 self.cache_index),那么我們就會命中一條緩存! 哇! 我們可以從緩存中返回匹配和應用功能而不是從無到有創建。
  2. 另一方面,如果我們沒有從緩存中命中條目, 并且 文件對象也已關閉(這會發生, 在本方法下面一點, 正如你從預覽的代碼片段中所看到的),那么我們什么都不能做。 如果文件被關閉,意味著我們已經用完了它——我們已經從頭至尾讀取了模式文件的每一行,而且已經對每個模式創建并緩存了匹配和應用功能。文件已經讀完;緩存已經用完;我也快完了。等等,什么?堅持一下,我們幾乎完成了。

放到一起,發生了什么事? 當:

我們已經到達復數變換的極樂世界。

  1. 最小化初始代價。import 時發生的唯一的事就是實例化一個單一的類并打開一個文件(但并不讀取)。
  2. 最大化性能 前述示例會在每次你想讓一個單詞變復數時,讀遍文件并動態創建功能。本版本將在創建的同時緩存功能,在最壞情況下,僅需要讀完一遍文件,無論你要讓多少單詞變復數。
  3. 將代碼和數據分離。 所有模式被存在一個分開的文件。代碼是代碼,數據是數據,二者永遠不會交織。

這真的是極樂世界? 嗯,是或不是。 這里有一些LazyRules 示例需要細想的地方: 模式文件被打開(在 __init__()中),并持續打開直到讀取最后一個規則。 當Python退出或最后一個LazyRules 類的實例銷毀,Python 會最終關閉文件,但是那仍然可能會是一個很長的時間。如果該類是一個“長時間運行”的Python進程的一部分,Python可能從不退出, LazyRules 對象就可能一直不會釋放。

這種情況有解決辦法。 不要在 __init__() 中打開文件并讓其在一行一行讀取規則時一直打開,你可以打開文件,讀取所有規則,并立即關閉文件。或你可以打開文件,讀取一條規則,用tell() 方法保存文件位置,關閉文件,后面再次打開它,使用seek() 方法 繼續從你離開的地方讀取。 或者你不需擔心這些就讓文件打開,如同本示例所做。 編程即是設計, 而設計牽扯到所有的權衡和限制。讓一個文件一直打開太長時間可能是問題;讓你代碼太復雜也可能是問題。哪一個是更大的問題,依賴于你的開發團隊,你的應用,和你的運行環境。

深入閱讀

© 2001–9 Mark Pilgrim

<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            亚洲欧美在线