# Chapter 7 類 & 迭代器
> " 東是東,西是西,東西不相及 "
> — [拉迪亞德·吉卜林](http://en.wikiquote.org/wiki/Rudyard_Kipling)
## 深入
生成器是一類特殊 _迭代器_。 一個產生值的函數 `yield` 是一種產生一個迭代器卻不需要構建迭代器的精密小巧的方法。 我會告訴你我是什么意思。
記得 [菲波拉稀生成器](generators.html#a-fibonacci-generator)嗎? 這里是一個從無到有的迭代器:
[[下載 `fibonacci2.py`](examples/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` 開始, 后面跟類名。 技術上來說,只需要這么多就夠了,因為一個類不是必須繼承其他類。
1. 類名是 `PapayaWhip`, 沒有從其他類繼承。 類名通常是大寫字母分隔, 如`EachWordLikeThis`, 但這只是個習慣,并非必須。
2. 你可能猜到,類內部的內容都需縮進,就像函數中的代碼一樣, `if` 語句, `for` 循環, 或其他代碼塊。第一行非縮進代碼表示到了類外。
`PapayaWhip` 類沒有定義任何方法和屬性, 但依據句法,應該在定義中有東西,這就是 `pass` 語句。 這是Python 保留字,意思是“繼續,這里看不到任何東西”。 這是一個什么都不做的語句,是一個很好的占位符,如果你的函數和類什么都不想做(刪空函數或類)。
> ?Python中的`pass` 就像Java 或 C中的空大括號對 (`{}`) 。
很多類繼承自其他類, 但這個類沒有。 很多類有方法,這個類也沒有。 Python 類不是必須有東西,除了一個名字。 特別是C++ 程序員發現 Python 類沒有顯式的構造和析構函數會覺得很古怪。 盡管不是必須, Python 類 _可以_ 具有類似構造函數的東西: `__init__()` 方法。
### `__init__()` 方法
本示例展示 `Fib` 類使用 `__init__` 方法。
```
class Fib:
```
1. 類同樣可以 (而且應該) 具有`docstring`, 與模塊和方法一樣。
2. 類實例創建后,`__init__()` 方法被立即調用。很容易將其——但技術上來說不正確——稱為該類的“構造函數” 。 很容易,因為它看起來很像 C++ 的構造函數(按約定,`__init__()` 是類中第一個被定義的方法),行為一致(是類的新實例中第一片被執行的代碼), 看起來完全一樣。 錯了, 因為`__init__()` 方法調用時,對象已經創建了,你已經有了一個合法類對象的引用。
每個方法的第一個參數,包括 `__init__()` 方法,永遠指向當前的類對象。 習慣上,該參數叫 `self`。 該參數和C++或Java中 `this` 角色一樣, 但 `self` 不是 Python的保留字, 僅僅是個命名習慣。 雖然如此,請不要取別的名字,只用 `self`; 這是一個很強的命名習慣。
在 `__init__()` 方法中, `self` 指向新創建的對象; 在其他類對象中, 它指向方法所屬的實例。盡管需在定義方法時顯式指定`self` ,調用方法時并 _不_ 必須明確指定。 Python 會自動添加。
## 實例化類
Python 中實例化類很直接。 實例化類時就像調用函數一樣簡單,將 `__init__()` 方法需要的參數傳入。 返回值就是新創建的對象。
```
>>> import fibonacci2
<fibonacci2.Fib object at 0x00DB8810>
<class 'fibonacci2.Fib'>
''
```
1. 你正創建一個 `Fib` 類的實例(在`fibonacci2` 模塊中定義) 將新創建的實例賦給變量`fib`。 你傳入一個參數 `100`, 這是`Fib`的`__init__()`方法作為`max`參數傳入的結束值。
2. `fib` 是 `Fib` 的實例。
3. 每個類實例具有一個內建屬性, `__class__`, 它是該對象的類。 Java 程序員可能熟悉 `Class` 類, 包含方法如 `getName()` 和 `getSuperclass()` 獲取對象相關元數據。 Python里面, 這類元數據由屬性提供,但思想一致。
4. 你可訪問對象的 `docstring` ,就像函數或模塊中的一樣。 類的所有實例共享一份 `docstring`。
> ?Python里面, 和調用函數一樣簡單的調用一個類來創建該類的新實例。 與C++ 或 Java不一樣,沒有顯式的 `new` 操作符。
## 實例變量
繼續下一行:
```
class Fib:
def __init__(self, max):
```
1. `self.max`是什么? 它就是實例變量。 與作為參數傳入 `__init__()` 方法的 `max`完全是兩回事。 `self.max` 是實例內 “全局” 的。 這意味著可以在其他方法中訪問它。
```
class Fib:
def __init__(self, max):
.
.
.
def __next__(self):
fib = self.a
```
1. `self.max` 在 `__init__()` 方法中定義……
2. ……在 `__next__()` 方法中引用。
實例變量特定于某個類的實例。 例如, 如果你創建 `Fib` 的兩個具有不同最大值的實例, 每個實例會記住自己的值。
```
>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200
```
## 菲波拉稀迭代器
_現在_ 你已經準備學習如何創建一個迭代器了。 迭代器就是一個定義了 `__iter__()` 方法的類。
<aside class="ots">這些類的所有三種方法, `__init__`, `__iter__`, 和 `__next__`, 起始和結束均為一對下劃線(`_`) 字符。 為什么這樣? 并無什么神奇之處, 只是通常表示這是“<dfn>特殊方法</dfn>。” 唯一“特殊”的地方,就是這些方法不是直接調用的; 當你使用類或實例的某些語法時,Python會自動調用他們。 [更多關于特殊方法](special-method-names.html)。
[[下載 `fibonacci2.py`](examples/fibonacci2.py)]
```
self.max = max
self.a = 0
self.b = 1
return self
fib = self.a
if fib > self.max:
self.a, self.b = self.b, self.a + self.b
```
1. 從無到有創建一個迭代器, `fib` 應是一個類,而不是一個函數。
2. “調用” `Fib(max)` 會創建該類一個真實的實例,并以`max`做為參數調用`__init__()` 方法。 `__init__()` 方法以實例變量保存最大值,以便隨后的其他方法可以引用。
3. 當有人調用`iter(fib)`的時候,`__iter__()`就會被調用。(正如你等下會看到的, `for` 循環會自動調用它, 你也可以自己手動調用。) 在完成迭代器初始化后,(在本例中, 重置我們兩個計數器 `self.a` 和 `self.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](generators.html#a-fibonacci-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__()` 方法。
## 復數規則迭代器
iter(f) 調用 f.__iter__
next(f) 調用 f.__next__
現在到曲終的時候了。我們重寫 [復數規則生成器](generators.html) 為迭代器。
[[下載`plural6.py`](examples/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):
```
1. 當我們實例化 `LazyRules` 類時, 打開模式文件,但不讀取任何東西。 (隨后再進行)
2. 打開模式文件之后,初始化緩存。 隨后讀取模式文件行的時候會用到它(在 `__next__()` 方法中) 。
我們繼續之前,讓我們近觀 `rules_filename`。它沒在 `__iter__()` 方法中定義。事實上,它沒在任何方法中定義。它定義于類級別。它是 _類變量_, 盡管訪問時和實例變量一樣 (`self.rules_filename`), `LazyRules` 類的所有實例共享該變量。
```
>>> import plural6
>>> r1 = plural6.LazyRules()
>>> r2 = plural6.LazyRules()
'plural6-rules.txt'
>>> r2.rules_filename
'plural6-rules.txt'
>>> r2.rules_filename
'r2-override.txt'
>>> r1.rules_filename
'plural6-rules.txt'
'plural6-rules.txt'
>>> r1.rules_filename
'papayawhip.txt'
'r2-overridetxt'
```
1. 類的每個實例繼承了 `rules_filename` 屬性及它在類中定義的值。
2. 修改一個實例屬性的值不影響其他實例……
3. ……也不會修改類的屬性。可以使用特殊的 `__class__` 屬性來訪問類屬性(于此相對的是單獨實例的屬性)。
4. 如果修改類屬性, 所有仍然繼承該實例的值的實例 (如這里的`r1` ) 會受影響。
5. 已經覆蓋(overridden)了該屬性(如這里的 `r2` )的所有實例 將不受影響。
現在回到我們的演示:
```
self.cache_index = 0
```
1. 無論何時有人——如 `for` 循環——調用 `iter(rules)`的時候,`__iter__()` 方法都會被調用。
2. 每個`__iter__()` 方法都需要做的就是必須返回一個迭代器。 在本例中,返回 `self`,意味著該類定義了`__next__()` 方法,由它來關注整個迭代過程中的返回值。
```
.
.
.
pattern, search, replace = line.split(None, 3)
pattern, search, replace)
return funcs
```
1. 無論何時有人——如 `for` 循環——調用 `__next__()` 方法, `next(rules)`都跟著被調用。 該方法僅在我們從后往前移動時比較好體會。所以我們就這么做。
2. 函數的最后一部分至少應該眼熟。 `build_match_and_apply_functions()` 函數還沒修改;與它從前一樣。
3. 唯一的不同是,在返回匹配和應用功能之前(保存在元組 `funcs`中),我們將其保存到 `self.cache`。
從后往前移動……
```
def __next__(self):
.
.
.
self.pattern_file.close()
.
.
.
```
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:
if self.pattern_file.closed:
.
.
.
```
1. `self.cache` 將是一個我們匹配并應用單獨規則的功能列表。 (至少_那個_應該看起來熟悉!) `self.cache_index` 記錄我們下一步返回的緩存條目。 如果我們還沒有耗盡緩存 (_舉例_ 如果 `self.cache` 的長度大于 `self.cache_index`),那么我們就會命中一條緩存! 哇! 我們可以從緩存中返回匹配和應用功能而不是從無到有創建。
2. 另一方面,如果我們沒有從緩存中命中條目, _并且_ 文件對象也已關閉(這會發生, 在本方法下面一點, 正如你從預覽的代碼片段中所看到的),那么我們什么都不能做。 如果文件被關閉,意味著我們已經用完了它——我們已經從頭至尾讀取了模式文件的每一行,而且已經對每個模式創建并緩存了匹配和應用功能。文件已經讀完;緩存已經用完;我也快完了。等等,什么?堅持一下,我們幾乎完成了。
放到一起,發生了什么事? 當:
* 當模塊引入時,創建了`LazyRules` 類的一個單一實例, 叫 `rules`, 它打開模式文件但并沒有讀取。
* 當要求第一個匹配和應用功能時,檢查緩存并發現緩存為空。 于是,從模式文件讀取一行, 從模式中創建匹配和應用功能,并緩存之。
* 假如,因為參數的緣故,正好是第一行匹配了。如果那樣,不會有更多的匹配和應用會創建,也不會有更多的行會從模式文件中讀取。
* 更進一步, 因為參數的緣故,假設調用者_再次_調用 `plural()` 函數來讓一個不同的單詞變復數。 `plural()` 函數中的`for` 循環會調用`iter(rules)`,這會重置緩存索引但不會重置打開的文件對象。
* 第一次遍歷, `for`循環會從`rules`中索要一個值,該值會調用其`__next__()`方法。然而這一次, 緩存已經被裝入了一個匹配和應用功能對, 與模式文件中第一行模式一致。 由于對前一個單詞做復數變換時已經被創建和緩存,它們被從緩存中返回。 緩存索引遞增,打開的文件無需訪問。
* 假如,因為參數的緣故,這一輪第一個規則 _不_ 匹配。 所以 `for` 循環再次運轉并從 `rules`請求一個值。 這會再次調用 `__next__()` 方法。 這一次, 緩存被用完了——它僅有一個條目,而我們被請求第二個——于是 `__next__()` 方法繼續。 從打開的文件中讀取下一行,從模式中創建匹配和應用功能,并緩存之。
* 該“讀取創建并緩存”過程一直持續直到我們從模式文件中讀取的規則與我們想變復數的單詞不匹配。 如果我們確實在文件結束前找到了一個匹配規則,我們僅需使用它并停止,文件還一直打開。文件指針會留在我們停止讀取,等待下一個 `readline()` 命令的地方。現在,緩存已經有更多條目了,并且再次從頭開始來將一個新單詞變復數,在讀取模式文件下一行之前,緩存中的每一個條目都將被嘗試。
我們已經到達復數變換的極樂世界。
1. **最小化初始代價。** 在 `import` 時發生的唯一的事就是實例化一個單一的類并打開一個文件(但并不讀取)。
2. **最大化性能** 前述示例會在每次你想讓一個單詞變復數時,讀遍文件并動態創建功能。本版本將在創建的同時緩存功能,在最壞情況下,僅需要讀完一遍文件,無論你要讓多少單詞變復數。
3. **將代碼和數據分離。** 所有模式被存在一個分開的文件。代碼是代碼,數據是數據,二者永遠不會交織。
> ?這真的是極樂世界? 嗯,是或不是。 這里有一些`LazyRules` 示例需要細想的地方: 模式文件被打開(在 `__init__()`中),并持續打開直到讀取最后一個規則。 當Python退出或最后一個`LazyRules` 類的實例銷毀,Python 會最終關閉文件,但是那仍然可能會是一個_很長_的時間。如果該類是一個“長時間運行”的Python進程的一部分,Python可能從不退出, `LazyRules` 對象就可能一直不會釋放。
>
> 這種情況有解決辦法。 不要在 `__init__()` 中打開文件并讓其在一行一行讀取規則時一直打開,你可以打開文件,讀取所有規則,并立即關閉文件。或你可以打開文件,讀取一條規則,用[`tell()` 方法](files.html#read)保存文件位置,關閉文件,后面再次打開它,使用[`seek()` 方法](files.html#read) 繼續從你離開的地方讀取。 或者你不需擔心這些就讓文件打開,如同本示例所做。 編程即是設計, 而設計牽扯到所有的權衡和限制。讓一個文件一直打開太長時間可能是問題;讓你代碼太復雜也可能是問題。哪一個是更大的問題,依賴于你的開發團隊,你的應用,和你的運行環境。
## 深入閱讀
* [迭代器類型](http://docs.python.org/3.1/library/stdtypes.html#iterator-types)
* [PEP 234: 迭代器( Iterators )](http://www.python.org/dev/peps/pep-0234/)
* [PEP 255:簡單生成器( Simple Generators )](http://www.python.org/dev/peps/pep-0255/)
* 系統程序員的生成器訣竅( Generator Tricks for Systems Programmers )
- 版權信息
- Chapter -1 《深入 Python 3》中有何新內容
- Chapter 0 安裝 Python
- Chapter 1 你的第一個 Python 程序
- Chapter 2 內置數據類型
- Chapter 3 解析
- Chapter 4 字符串
- Chapter 5 正則表達式
- Chapter 6 閉合 與 生成器
- Chapter 7 類 & 迭代器
- Chapter 8 高級迭代器
- Chapter 9 單元測試
- Chapter 10 重構
- Chapter 11 文件
- Chapter 12 XML
- Chapter 13 序列化Python對象
- Chapter 14 HTTP Web 服務
- Chapter 15 案例研究:將chardet移植到Python 3
- Chapter 16 打包 Python 類庫
- Chapter A 使用2to3將代碼移植到Python 3
- Chapter B 特殊方法名稱
- Chapter C 接下來閱讀什么?