### 9. 類
與其他編程語言相比,Python 的類機制用最少的語法和語義引入了類。它是 C++ 和 Modula-3 類機制的混合。Python 的類提供了面向對象編程的所有標準功能: 類繼承機制允許有多個基類,繼承的類可以覆蓋其基類或類的任何方法,方法能夠以相同的名稱調用基類中的方法。對象可以包含任意數量和種類的數據。和模塊一樣,類同樣具有 Python 的動態性質:它們在運行時創建,并可以在創建之后進一步修改。
用 C++ 術語來講,通常情況下類成員(包括數據成員)是*公有*的(其它情況見下文[*私有變量和類本地引用*](#)),所有的成員函數都是*虛* 的。與 Modula-3 一樣,在成員方法中沒有簡便的方式引用對象的成員:方法函數的聲明用顯式的第一個參數表示對象本身,調用時會隱式地引用該對象。與 Smalltalk 一樣,類本身也是對象。這給導入類和重命名類提供了語義上的合理性。與 C++ 和 Modula-3 不同,用戶可以用內置類型作為基類進行擴展。此外,像 C++ 一樣,類實例可以重定義大多數帶有特殊語法的內置操作符(算術運算符、 下標等)。
(由于沒有統一的達成共識的術語,我會偶爾使用 SmallTalk 和 C++ 的術語。我比較喜歡用 Modula-3 的術語,因為比起 C++,Python 的面向對象語法更像它,但是我想很少有讀者聽說過它。)
### 9.1. 名稱和對象
對象是獨立的,多個名字(在多個作用域中)可以綁定到同一個對象。這在其他語言中稱為別名。第一次粗略瀏覽 Python 時經常不會注意到這個特性,而且處理不可變的基本類型(數字,字符串,元組)時忽略這一點也沒什么問題。然而, 在Python 代碼涉及可變對象如列表、 字典和大多數其它類型時,別名可能具有意想不到語義效果。這通常有助于優化程序,因為別名的行為在某些方面類似指針。例如,傳遞一個對象的開銷是很小的,因為在實現上只是傳遞了一個指針;如果函數修改了參數傳遞的對象,調用者也將看到變化 —— 這就避免了類似 Pascal 中需要兩個不同參數的傳遞機制。
### 9.2. Python 作用域和命名空間
在介紹類之前,我首先要告訴你一些有關 Python 作用域的的規則。類的定義非常巧妙的運用了命名空間,要完全理解接下來的知識,需要先理解作用域和命名空間的工作原理。另外,這一切的知識對于任何高級 Python 程序員都非常有用。
讓我們從一些定義開始。
*命名空間*是從名稱到對象的映射。當前命名空間主要是通過 Python 字典實現的,不過通常不會引起任何關注(除了性能方面),它以后也有可能會改變。以下有一些命名空間的例子:內置名稱集(包括函數名例如[abs()](# "abs")和內置異常的名稱);模塊中的全局名稱;函數調用中的局部名稱。在某種意義上的一組對象的屬性也形成一個命名空間。關于命名空間需要知道的重要一點是不同命名空間的名稱絕對沒有任何關系;例如,兩個不同模塊可以都定義函數maximize而不會產生混淆 —— 模塊的使用者必須以模塊名為前綴引用它們。
順便說一句,我使用*屬性* 這個詞稱呼點后面的任何名稱 —— 例如,在表達式z.real中,real是z對象的一個屬性。嚴格地說,對模塊中的名稱的引用是屬性引用:在表達式modname.funcname中, modname是一個模塊對象,funcname是它的一個屬性。在這種情況下,模塊的屬性和模塊中定義的全局名稱之間碰巧是直接的映射:它們共享同一命名空間 ![[1]](#)
屬性可以是只讀的也可以是可寫的。在后一種情況下,可以對屬性賦值。模塊的屬性都是可寫的:你可以這樣寫modname.the_answer=42。可寫的屬性也可以用[del](#)語句刪除。例如,delmodname.the_answer將會刪除對象modname中的the_answer屬性。
各個命名空間創建的時刻是不一樣的,且有著不同的生命周期。包含內置名稱的命名空間在 Python 解釋器啟動時創建,永遠不會被刪除。模塊的全局命名空間在讀入模塊定義時創建;通常情況下,模塊命名空間也會一直保存到解釋器退出。在解釋器最外層調用執行的語句,不管是從腳本文件中讀入還是來自交互式輸入,都被當作模塊[__main__](# "__main__: The environment where the top-level script is run.")的一部分,所以它們有它們自己的全局命名空間。(內置名稱實際上也存在于一個模塊中,這個模塊叫[__builtin__](# "__builtin__: The module that provides the built-in namespace.")。)
函數的局部命名空間在函數調用時創建,在函數返回或者引發了一個函數內部沒有處理的異常時刪除。(實際上,用遺忘來形容到底發生了什么更為貼切。)當然,每個遞歸調用有它們自己的局部命名空間。
*作用域* 是 Python 程序中可以直接訪問一個命名空間的代碼區域。這里的“直接訪問”的意思是用沒有前綴的引用在命名空間中找到的相應的名稱。
雖然作用域的確定是靜態地,但它們的使用是動態地。程序執行過程中的任何時候,至少有三個嵌套的作用域,它們的命名空間是可以直接訪問的:
- 首先搜索最里面包含局部命名的作用域
- 其次搜索所有調用函數的作用域,從最內層調用函數的作用域開始,它們包含非局部但也非全局的命名
- 倒數第二個搜索的作用域是包含當前模塊全局命名的作用域
- 最后搜索的作用域是最外面包含內置命名的命名空間
如果一個命名聲明為全局的,那么對它的所有引用和賦值會直接搜索包含這個模塊全局命名的作用域。否則,在最里面作用域之外找到的所有變量都是只讀的(對這樣的變量賦值會在最里面的作用域創建一個*新* 的局部變量,外部具有相同命名的那個變量不會改變)。
通常情況下,局部作用域引用當前函數的本地命名。函數之外,局部作用域引用的命名空間與全局作用域相同:模塊的命名空間。類定義在局部命名空間中創建了另一個命名空間。
認識到作用域是由代碼確定的是非常重要的:函數的全局作用域是函數的定義所在的模塊的命名空間,與函數調用的位置或者別名無關。另一方面,命名的實際搜索過程是動態的,在運行時確定的——然而,Python 語言也在不斷發展,以后有可能會成為靜態的“編譯”時確定,所以不要依賴動態解析!(事實上,本地變量是已經確定靜態。)
Python的一個特別之處在于——如果沒有使用[global](#)語法——其賦值操作總是在最里層的作用域。賦值不會復制數據——只是將命名綁定到對象。刪除也是如此:delx只是從局部作用域的命名空間中刪除命名x。事實上,所有引入新命名的操作都作用于局部作用域: 特別是[import](#)語句和函數定義將模塊名或函數綁定于局部作用域。(可以使用 ? [Global](#) 語句將變量引入到全局作用域。)
### 9.3. 初識類
類引入了少量的新語法、三種新對象類型和一些新語義。
#### 9.3.1. 類定義語法
類定義的最簡單形式如下所示:
~~~
class ClassName:
<statement-1>
.
.
.
<statement-N>
~~~
類的定義就像函數定義([def](#)語句),要先執行才能生效。(你當然可以把它放進[if](#)語句的某一分支,或者一個函數的內部。)
實際應用中,類定義包含的語句通常是函數定義,不過其它語句也是可以的而且有時還會很有用——后面我們會再回來討論。類中的函數定義通常有一個特殊形式的參數列表,這是由方法調用的協議決定的——同樣后面會解釋這些。
進入類定義部分后,會創建出一個新的命名空間,作為局部作用域——因此,所有的賦值成為這個新命名空間的局部變量。特別是這里的函數定義會綁定新函數的名字。
類定義正常結束時,一個*類對象*也就創建了。基本上它是對類定義創建的命名空間進行了一個包裝;我們在下一節將進一步學習類對象的知識。原始的局部作用域(類定義引入之前生效的那個)得到恢復,類對象在這里綁定到類定義頭部的類名(例子中是ClassName)。
#### 9.3.2. 類對象
類對象支持兩種操作:屬性引用和實例化。
*屬性引用* 使用和Python中所有的屬性引用一樣的標準語法: obj.name。有效的屬性名稱是在該類的命名空間中的類對象被創建時的所有名稱。因此,如果類定義看起來像這樣:
~~~
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
~~~
那么 MyClass.i 和 MyClass.f 是有效的屬性引用,分別返回一個整數和一個方法對象。也可以對類屬性賦值,你可以通過給 MyClass.i 賦值來修改它。__doc__ 也是一個有效的屬性,返回類的文檔字符串: "Asimpleexampleclass"。
類的*實例化* 使用函數的符號。可以假設類對象是一個不帶參數的函數,該函數返回這個類的一個新的實例。例如(假設沿用上面的類):
~~~
x = MyClass()
~~~
創建這個類的一個新*實例*,并將該對象賦給局部變量x。
實例化操作(“調用”一個類對象)將創建一個空對象。很多類希望創建的對象可以自定義一個初始狀態。因此類可以定義一個名為[__init__()](# "object.__init__")的特殊方法,像下面這樣:
~~~
def __init__(self):
self.data = []
~~~
當類定義了[__init__()](# "object.__init__")方法,類的實例化會為新創建的類實例自動調用[__init__()](# "object.__init__")。所以在下面的示例中,可以獲得一個新的、已初始化的實例:
~~~
x = MyClass()
~~~
當然,[__init__()](# "object.__init__")方法可以帶有參數,這將帶來更大的靈活性。在這種情況下,類實例化操作的參數將傳遞給[__init__()](# "object.__init__")。例如,
~~~
>>> class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
~~~
#### 9.3.3. 實例對象
現在我們可以用實例對象做什么?實例對象唯一可用的操作就是屬性引用。有兩種有效的屬性名:數據屬性和方法。
*數據屬性*相當于 Smalltalk 中的"實例變量"或 C++ 中的"數據成員"。數據屬性不需要聲明;和局部變量一樣,它們會在第一次給它們賦值時生成。例如,如果x是上面創建的MyClass的實例,下面的代碼段將打印出值16而不會出現錯誤:
~~~
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print x.counter
del x.counter
~~~
實例屬性引用的另一種類型是*方法*。方法是"屬于"一個對象的函數。(在 Python,方法這個術語不只針對類實例:其他對象類型也可以具有方法。例如,列表對象有 append、insert、remove、sort 方法等等。但是在后面的討論中,除非明確說明,我們提到的方法特指類實例對象的方法。)
實例對象的方法的有效名稱依賴于它的類。根據定義,類中所有函數對象的屬性定義了其實例中相應的方法。所以在我們的示例中, x.f是一個有效的方法的引用,因為MyClass.f是一個函數,但x.i不是,因為MyClass.i不是一個函數。但x.f與MyClass.f也不是一回事 —— 它是一個*方法對象*,不是一個函數對象。
#### 9.3.4. 方法對象
通常情況下,方法在綁定之后被直接調用:
~~~
x.f()
~~~
在MyClass的示例中,這將返回字符串'helloworld'。然而,也不是一定要直接調用方法: x.f是一個方法對象,可以存儲起來以后調用。例如:
~~~
xf = x.f
while True:
print xf()
~~~
會不斷地打印helloworld。
調用方法時到底發生了什么?你可能已經注意到,上面x.f()的調用沒有參數,即使f ()函數的定義指定了一個參數。該參數發生了什么問題?當然如果函數調用中缺少參數 Python 會拋出異常——即使這個參數實際上沒有使用……
實際上,你可能已經猜到了答案:方法的特別之處在于實例對象被作為函數的第一個參數傳給了函數。在我們的示例中,調用x.f()完全等同于MyClass.f(x)。一般情況下,以*n* 個參數的列表調用一個方法就相當于將方法所屬的對象插入到列表的第一個參數的前面,然后以新的列表調用相應的函數。
如果你還是不明白方法的工作原理,了解一下它的實現或許有幫助。引用非數據屬性的實例屬性時,會搜索它的類。如果這個命名確認為一個有效的函數對象類屬性,就會將實例對象和函數對象封裝進一個抽象對象:這就是方法對象。以一個參數列表調用方法對象時,它被重新拆封,用實例對象和原始的參數列表構造一個新的參數列表,然后函數對象調用這個新的參數列表。
#### 9.3.5. 類和實例變量
一般來說,實例變量用于對每一個實例都是唯一的數據,類變量用于類的所有實例共享的屬性和方法:
~~~
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
~~~
正如在[*名稱和對象*](#)討論的,[*可變*](#)對象,例如列表和字典,的共享數據可能帶來意外的效果。例如,下面代碼中的*tricks* 列表不應該用作類變量,因為所有的*Dog* 實例將共享同一個列表:
~~~
class Dog:
tricks = [] # mistaken use of a class variable
def __init__(self, name):
self.name = name
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks # unexpectedly shared by all dogs
['roll over', 'play dead']
~~~
這個類的正確設計應該使用一個實例變量:
~~~
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each dog
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
~~~
### 9.4. 補充說明
數據屬性會覆蓋同名的方法屬性;為了避免意外的命名沖突,這在大型程序中可能帶來極難發現的 bug,使用一些約定來減少沖突的機會是明智的。可能的約定包括大寫方法名稱的首字母,使用一個小寫的獨特字符串(也許只是一個下劃線)作為數據屬性名稱的前綴,或者方法使用動詞而數據屬性使用名詞。
數據屬性可以被方法引用,也可以由一個對象的普通用戶(“客戶端”)使用。換句話說,類是不能用來實現純抽象數據類型。事實上,Python 中不可能強制隱藏數據——那全部基于約定。(另一方面,如果需要,使用 C 編寫的 Python 實現可以完全隱藏實現細節并控制對象的訪問;這可以用來通過 C 語言擴展 Python。)
客戶應該謹慎的使用數據屬性——客戶可能通過踐踏他們的數據屬性而使那些由方法維護的常量變得混亂。注意:只要能避免沖突,客戶可以向一個實例對象添加他們自己的數據屬性,而不會影響方法的正確性——再次強調,命名約定可以避免很多麻煩。
從方法內部引用數據屬性(或其他方法)并沒有快捷方式。我覺得這實際上增加了方法的可讀性:當瀏覽一個方法時,在局部變量和實例變量之間不會出現令人費解的情況。
通常,方法的第一個參數稱為self。這僅僅是一個約定:名字self對 Python 而言絕對沒有任何特殊含義。但是請注意:如果不遵循這個約定,對其他的 Python 程序員而言你的代碼可讀性就會變差,而且有些類 *查看* 器程序也可能是遵循此約定編寫的。
類屬性的任何函數對象都為那個類的實例定義了一個方法。函數定義代碼不一定非得定義在類中:也可以將一個函數對象賦值給類中的一個局部變量。例如:
~~~
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'hello world'
h = g
~~~
現在f、 g和h都是類C中引用函數對象的屬性,因此它們都是C的實例的方法 —— h完全等同于g。請注意,這種做法通常只會混淆程序的讀者。
通過使用self參數的方法屬性,方法可以調用其他方法:
~~~
class Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
~~~
方法可以像普通函數那樣引用全局命名。與方法關聯的全局作用域是包含類定義的模塊。(類本身永遠不會做為全局作用域使用。)盡管很少有好的理由在方法中使用全局數據,全局作用域確有很多合法的用途:其一是方法可以調用導入全局作用域的函數和模塊,也可以調用定義在其中的類和函數。通常,包含此方法的類也會定義在這個全局作用域,在下一節我們會了解為何一個方法要引用自己的類。
每個值都是一個對象,因此每個值都有一個*類*(也稱為*類型*)。它存儲為object.__class__。
### 9.5. 繼承
當然,一個語言特性不支持繼承是配不上“類”這個名字的。派生類定義的語法如下所示:
~~~
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
~~~
BaseClassName必須與派生類定義在一個作用域內。用其他任意表達式代替基類的名稱也是允許的。這可以是有用的,例如,當基類定義在另一個模塊中時:
~~~
class DerivedClassName(modname.BaseClassName):
~~~
派生類定義的執行過程和基類是相同的。類對象創建后,基類會被保存。這用于解析屬性的引用:如果在類中找不到請求的屬性,搜索會在基類中繼續。如果基類本身是由別的類派生而來,這個規則會遞歸應用。
派生類的實例化沒有什么特殊之處:DerivedClassName()創建類的一個新的實例。方法的引用按如下規則解析: 搜索對應的類的屬性,必要時沿基類鏈逐級搜索,如果找到了函數對象這個方法引用就是合法的。
派生的類可能重寫其基類的方法。因為方法調用本對象中的其它方法時沒有特權,基類的方法調用本基類的方法時,可能實際上最終調用了派生類中的覆蓋方法。(對于 C++ 程序員:Python 中的所有方法實際上都是虛的。)
派生類中的覆蓋方法可能是想要擴充而不是簡單的替代基類中的重名方法。有一個簡單的方法可以直接調用基類方法:只要調用BaseClassName.methodname(self,arguments)。有時這對于客戶端也很有用。(要注意只有BaseClassName在同一全局作用域定義或導入時才能這樣用。)
Python 有兩個用于繼承的函數:
- 使用[isinstance()](# "isinstance")來檢查實例類型:isinstance(obj,?int)只有obj.__class__是[int](# "int")或者是從[int](# "int")派生的類時才為True。
- 使用[issubclass()](# "issubclass")來檢查類的繼承: issubclass(bool,int)是True因為[bool](# "bool")是[int](# "int")的子類。然而, issubclass (unicode,str)是False因為[unicode](# "unicode")不是[str](# "str")的一個子類(它們只是共享一個共同的祖先, [basestring](# "basestring")) 。
#### 9.5.1. 多繼承
Python 也支持一定限度的多繼承形式。具有多個基類的類定義如下所示:
~~~
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
~~~
對于舊風格的類,唯一的規則是深度優先,從左到右。因此,如果在DerivedClassName中找不到屬性,它搜索Base1,然后(遞歸)基類中的Base1,只有沒有找到,它才會搜索base2,依此類推。
(對某些人,廣度優先——在搜索Base1的基類之前先搜索base2和Base3——看起來更自然。然而,在你能弄明白與base2中的一個屬性名稱沖突的后果之前,你需要知道Base1的某個特定屬性實際上是定義在Base1的還是在其某個基類中的。深度優先規則使Base1的直接屬性和繼承的屬性之間沒有差別)。
對于*[新風格的類](#)*,方法的解析順序動態變化地支持合作對[super()](# "super")的調用。這種方法在某些其它多繼承的語言中也有并叫做call-next-method,它比單繼承語言中的super調用更強大。
對于新風格的類,動態調整順序是必要的,因為所有的多繼承都會有一個或多個菱形關系(從最底部的類向上,至少會有一個父類可以通過多條路徑訪問到)。例如,所有新風格的類都繼承自[object](# "object"),所以任何多繼承都會有多條路徑到達[object](# "object")。為了防止基類被重復訪問,動態算法線性化搜索順序,每個類都按從左到右的順序特別指定了順序,每個父類只調用一次,這是單調的(也就是說一個類被繼承時不會影響它祖先的次序)。所有這些特性使得設計可靠并且可擴展的多繼承類成為可能。有關詳細信息,請參閱[http://www.python.org/download/releases/2.3/mro/](http://www.python.org/download/releases/2.3/mro/)。
### 9.6. 私有變量和類本地引用
在 Python 中不存在只能從對象內部訪問的“私有”實例變量。然而,有一項大多數 Python 代碼都遵循的公約:帶有下劃線(例如_spam)前綴的名稱應被視為非公開的 API 的一部分(無論是函數、 方法還是數據成員)。它應該被當做一個實現細節,將來如果有變化孰不另行通知。
因為有一個合理的類私有成員的使用場景(即為了避免名稱與子類定義的名稱沖突),Python 對這種機制有簡單的支持,叫做*name mangling*。__spam 形式的任何標識符(前面至少兩個下劃線,后面至多一個下劃線)將被替換為_classname__spam,classname是當前類的名字。此mangling會生效而不考慮該標識符的句法位置,只要它出現在類的定義的范圍內。
Name mangling 有利于子類重寫父類的方法而不會破壞類內部的方法調用。例如:
~~~
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
~~~
請注意 mangling 規則的目的主要是避免發生意外;訪問或者修改私有變量仍然是可能的。這在特殊情況下,例如調試的時候,還是有用的。
請注意傳遞給exec、 eval()或execfile()的代碼沒有考慮要將調用類的類名當作當前類;這類似于global語句的效果,影響只限于一起進行字節編譯的代碼。相同的限制適用于getattr()、 setattr()和delattr(),以及直接引用__dict__時。
### 9.7. 零碎的說明
有時候類似于Pascal 的"record" 或 C 的"struct"的數據類型很有用,它們把幾個已命名的數據項目綁定在一起。一個空的類定義可以很好地做到:
~~~
class Employee:
pass
john = Employee() # Create an empty employee record
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
~~~
某一段 Python 代碼需要一個特殊的抽象數據結構的話,通常可以傳入一個類來模擬該數據類型的方法。例如,如果你有一個用于從文件對象中格式化數據的函數,你可以定義一個帶有read ()和readline () 方法的類,以此從字符串緩沖讀取數據,然后將該類的對象作為參數傳入前述的函數。
實例的方法對象也有屬性: m.im_self是具有方法m()的實例對象,m.im_func是方法的函數對象。
### 9.8. 異常也是類
用戶定義的異常類也由類標識。利用這個機制可以創建可擴展的異常層次。
[raise](#)語句有兩種新的有效的(語義上的)形式:
~~~
raise Class, instance
raise instance
~~~
第一種形式中,instance必須是class或者它的子類的實例。第二種形式是一種簡寫:
~~~
raise instance.__class__, instance
~~~
[except](#)子句中的類如果與異常是同一個類或者是其基類,那么它們就是相容的(但是反過來是不行的——except子句列出的子類與基類是不相容的)。例如,下面的代碼將按該順序打印 B、 C、 D:
~~~
class B:
pass
class C(B):
pass
class D(C):
pass
for c in [B, C, D]:
try:
raise c()
except D:
print "D"
except C:
print "C"
except B:
print "B"
~~~
請注意,如果except 子句的順序倒過來 (excpet BB在最前面),它就會打印B,B,B —— 第一個匹配的異常被觸發。
打印一個異常類的錯誤信息時,先打印類名,然后是一個空格、一個冒號,然后是用內置函數[str()](# "str")將類轉換得到的完整字符串。
### 9.9. 迭代器
現在你可能注意到大多數容器對象都可以用[for](#)遍歷:
~~~
for element in [1, 2, 3]:
print element
for element in (1, 2, 3):
print element
for key in {'one':1, 'two':2}:
print key
for char in "123":
print char
for line in open("myfile.txt"):
print line,
~~~
這種風格的訪問明確、 簡潔和方便。迭代器的用法在 Python 中普遍而且統一。在后臺, [for](#)語句調用容器對象上的[iter()](# "iter") 。該函數返回一個定義了[next ()](# "iterator.next")方法的迭代器對象,它在容器中逐一訪問元素。沒有后續的元素時, [next ()](# "iterator.next")會引發[StopIteration](# "exceptions.StopIteration")異常,告訴[for](#)循環終止。此示例顯示它是如何工作:
~~~
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> it.next()
'a'
>>> it.next()
'b'
>>> it.next()
'c'
>>> it.next()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
it.next()
StopIteration
~~~
看過迭代器協議背后的機制后,將很容易將迭代器的行為添加到你的類中。定義一個[__iter__()](# "object.__iter__")方法,它使用[next()](# "next")方法返回一個對象。如果類定義了[next()](# "next"),[__iter__()](# "object.__iter__") 可以只返回self:
~~~
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def next(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
~~~
~~~
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
... print char
...
m
a
p
s
~~~
### 9.10. 生成器
[*生成器*](#)是創建迭代器的一種簡單而強大的工具。它們寫起來就像是正規的函數,只是需要返回數據的時候使用[yield](#)語句。每次[next()](# "next")調用時,生成器再恢復它離開的位置(它記憶語句最后一次執行的位置和所有的數據值)。以下示例演示了生成器可以非常簡單地創建出來:
~~~
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
~~~
~~~
>>> for char in reverse('golf'):
... print char
...
f
l
o
g
~~~
生成器能做到的什么事,前一節所述的基于類的迭代器也能做到。生成器這么緊湊的原因是因為[__iter__()](# "object.__iter__")和[next ()](# "next")方法是自動創建的。
另一個關鍵功能是調用時自動保存的本地變量和執行狀態。這使得該函數相比實例變量,如self.index和self.data方法,更容易寫,更清楚地使用。
除了自動方法創建和保存的程序狀態外,當創建完成,他們會自動拋出[StopIteration](# "exceptions.StopIteration")。組合起來,這些功能可以容易地創建迭代器如同編寫正規函數。
### 9.11. 生成器表達式
使用類似列表表示式的語法,一些簡單的生成器可以寫成簡潔的表達式,但是使用圓括號代替方括號。這些表達式用于生成器在封閉的函數中使用的情況。生成器表達式更緊湊但沒有完整的生成器定義用途廣泛,比等同的列表表示式消耗較少的內存。
例子:
~~~
>>> sum(i*i for i in range(10)) # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # dot product
260
>>> from math import pi, sin
>>> sine_table = dict((x, sin(x*pi/180)) for x in range(0, 91))
>>> unique_words = set(word for line in page for word in line.split())
>>> valedictorian = max((student.gpa, student.name) for student in graduates)
>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1,-1,-1))
['f', 'l', 'o', 'g']
~~~
腳注
| [[1]](#) | 有一件事除外。模塊對象具有一個隱藏的只讀屬性叫做__dict__,它返回用于實現模塊命名空間的字典;名稱__dict__是一個屬性而不是一個全局的名稱。很明顯,使用它違反了命名空間實現的抽象,應該限制在類似事后調試這樣的事情上。 |
|-----|-----|
- Python 2 教程
- 1. 吊吊你的胃口
- 2. Python 解釋器
- 3. Python簡介
- 4. 控制流
- 5. 數據結構
- 6. 模塊
- 7. 輸入和輸出
- 8. 錯誤和異常
- 9. 類
- 10. 標準庫概覽
- 11. 標準庫概覽 — 第II部分
- 12.現在怎么辦?
- 13. 交互式輸入的編輯和歷史記錄
- 14. 浮點數運算:問題和局限
- Python 2 標準庫
- 1. 引言
- 2. 內建函數
- 3. 不太重要的內建函數
- 4. 內建的常量
- 5. 內建的類型
- 6. 內建的異常
- 7. String Services
- 8. Data Types
- 9. Numeric and Mathematical Modules
- 10. File and Directory Access
- 11. Data Persistence
- 13. File Formats
- 14. Cryptographic Services
- 15. Generic Operating System Services
- 16. Optional Operating System Services
- 17. Interprocess Communication and Networking
- 18. Internet Data Handling
- 20. Internet Protocols and Support
- 26. Debugging and Profiling
- 28. Python Runtime Services
- Python 2 語言參考
- 1. 簡介
- 2. 詞法分析
- 3. 數據模型
- 4. 執行模型
- 5. 表達式
- 6. 簡單語句
- 7. 復合語句
- 8. 頂層的組件
- 9. 完整的語法規范
- Python 3 教程
- 1. 引言
- 2. Python 解釋器
- 3. Python簡介
- 4. 控制流
- 5. 數據結構
- 6. 模塊
- 7. 輸入和輸出
- 8. 錯誤和異常
- 9. 類
- 10. 標準庫概覽
- 11. 標準庫概覽 — 第II部分
- 12.現在怎么辦?
- 13. 交互式輸入的編輯和歷史記錄
- 14. 浮點數運算:問題和局限