9. 類?
類提供了一種組合數據和功能的方法。 創建一個新類意味著創建一個新的對象 類型,從而允許創建一個該類型的新 實例 。 每個類的實例可以擁有保存自己狀態的屬性。 一個類的實例也可以有改變自己狀態的(定義在類中的)方法。
和其他編程語言相比,Python 用非常少的新語法和語義將類加入到語言中。它是 C++ 和 Modula-3 中類機制的結合。Python 的類提供了面向對象編程的所有標準特性:類繼承機制允許多個基類,派生類可以覆蓋它基類的任何方法,一個方法可以調用基類中相同名稱的的方法。對象可以包含任意數量和類型的數據。和模塊一樣,類也擁有 Python 天然的動態特性:它們在運行時創建,可以在創建后修改。
在 C++ 術語中,通常類成員(包括數據成員)是 public (例外見下文 私有變量),所有成員函數都是 virtual。 與在 Modula-3 中一樣,沒有用于從其方法引用對象成員的簡寫:方法函數使用表示對象的顯式第一個參數聲明,該參數由調用隱式提供。 與 Smalltalk 一樣,類本身也是對象。 這為導入和重命名提供了語義。 與 C++ 和 Modula-3 不同,內置類型可以用作用戶擴展的基類。 此外,與 C++ 一樣,大多數具有特殊語法(算術運算符,下標等)的內置運算符都可以為類實例而重新定義。
(由于缺乏關于類的公認術語,我會偶爾使用 Smalltalk 和 C++ 的用辭。 我還會使用 Modula-3 的術語,因為其面向對象的語義比 C++ 更接近 Python,但我預計少有讀者聽說過它。)
9.1. 名稱和對象?
對象具有個性,多個名稱(在多個作用域內)可以綁定到同一個對象。 這在其他語言中稱為別名。 乍一看 Python 時通常不會理解這一點,在處理不可變的基本類型(數字,字符串,元組)時可以安全地忽略它。 但是,別名對涉及可變對象,如列表,字典和大多數其他類型的 Python 代碼的語義可能會產生驚人的影響。 通常這樣做是為了使程序受益,因為別名在某些方面表現得像指針。 例如,傳遞一個對象的代價很小,因為實現只傳遞一個指針;如果函數修改了作為參數傳遞的對象,調用者將看到更改 --- 這就不需要像 Pascal 中那樣使用兩個不同的參數傳遞機制。
9.2. Python 作用域和命名空間?
在介紹類之前,我首先要告訴你一些 Python 的作用域規則。類定義對命名空間有一些巧妙的技巧,你需要知道作用域和命名空間如何工作才能完全理解正在發生的事情。順便說一下,關于這個主題的知識對任何高級 Python 程序員都很有用。
讓我們從一些定義開始。
namespace (命名空間)是一個從名字到對象的映射。 大部分命名空間當前都由 Python 字典實現,但一般情況下基本不會去關注它們(除了要面對性能問題時),而且也有可能在將來更改。 下面是幾個命名空間的例子:存放內置函數的集合(包含 abs() 這樣的函數,和內建的異常等);模塊中的全局名稱;函數調用中的局部名稱。 從某種意義上說,對象的屬性集合也是一種命名空間的形式。 關于命名空間的重要一點是,不同命名空間中的名稱之間絕對沒有關系;例如,兩個不同的模塊都可以定義一個 maximize 函數而不會產生混淆 --- 模塊的用戶必須在其前面加上模塊名稱。
順便說明一下,我把任何跟在一個點號之后的名稱都稱為 屬性 --- 例如,在表達式 z.real 中,real 是對象 z 的一個屬性。按嚴格的說法,對模塊中名稱的引用屬于屬性引用:在表達式 modname.funcname 中,modname 是一個模塊對象而 funcname 是它的一個屬性。在此情況下在模塊的屬性和模塊中定義的全局名稱之間正好存在一個直觀的映射:它們共享相同的命名空間! 1
屬性可以是只讀或者可寫的。如果為后者,那么對屬性的賦值是可行的。模塊屬性是可以寫,你可以寫出 modname.the_answer = 42 ??蓪懙膶傩酝瑯涌梢杂?del 語句刪除。例如, del modname.the_answer 將會從名為 modname 的對象中移除 the_answer 屬性。
在不同時刻創建的命名空間擁有不同的生存期。包含內置名稱的命名空間是在 Python 解釋器啟動時創建的,永遠不會被刪除。模塊的全局命名空間在模塊定義被讀入時創建;通常,模塊命名空間也會持續到解釋器退出。被解釋器的頂層調用執行的語句,從一個腳本文件讀取或交互式地讀取,被認為是 __main__ 模塊調用的一部分,因此它們擁有自己的全局命名空間。(內置名稱實際上也存在于一個模塊中;這個模塊稱作 builtins 。)
一個函數的本地命名空間在這個函數被調用時創建,并在函數返回或拋出一個不在函數內部處理的錯誤時被刪除。(事實上,比起描述到底發生了什么,忘掉它更好。)當然,每次遞歸調用都會有它自己的本地命名空間。
一個 作用域 是一個命名空間可直接訪問的 Python 程序的文本區域。 這里的 “可直接訪問” 意味著對名稱的非限定引用會嘗試在命名空間中查找名稱。
作用域被靜態確定,但被動態使用。 在程序運行的任何時間,至少有三個命名空間可被直接訪問的嵌套作用域:
最先搜索的最內部作用域包含局部名稱
從最近的封閉作用域開始搜索的任何封閉函數的作用域包含非局部名稱,也包括非全局名稱
倒數第二個作用域包含當前模塊的全局名稱
最外面的作用域(最后搜索)是包含內置名稱的命名空間
如果一個名稱被聲明為全局變量,則所有引用和賦值將直接指向包含該模塊的全局名稱的中間作用域。 要重新綁定在最內層作用域以外找到的變量,可以使用 nonlocal 語句聲明為非本地變量。 如果沒有被聲明為非本地變量,這些變量將是只讀的(嘗試寫入這樣的變量只會在最內層作用域中創建一個 新的 局部變量,而同名的外部變量保持不變)。
通常,當前局部作為域將(按字面文本)引用當前函數的局部名稱。 在函數以外,局部作用域將引用與全局作用域相一致的命名空間:模塊的命名空間。 類定義將在局部命名空間內再放置另一個命名空間。
重要的是應該意識到作用域是按字面文本來確定的:在一個模塊內定義的函數的全局作用域就是該模塊的命名空間,無論該函數從什么地方或以什么別名被調用。 另一方面,實際的名稱搜索是在運行時動態完成的 --- 但是,Python 正在朝著“編譯時靜態名稱解析”的方向發展,因此不要過于依賴動態名稱解析! (事實上,局部變量已經是被靜態確定了。)
Python 的一個特殊規定是這樣的 -- 如果不存在生效的 global 或 nonlocal 語句 -- 則對名稱的賦值總是會進入最內層作用域。 賦值不會復制數據 --- 它們只是將名稱綁定到對象。 刪除也是如此:語句 del x 會從局部作用域所引用的命名空間中移除對 x 的綁定。 事實上,所有引入新名稱的操作都是使用局部作用域:特別地,import 語句和函數定義會在局部作用域中綁定模塊或函數名稱。
global 語句可被用來表明特定變量生存于全局作用域并且應當在其中被重新綁定;nonlocal 語句表明特定變量生存于外層作用域中并且應當在其中被重新綁定。
9.2.1. 作用域和命名空間示例?
這個例子演示了如何引用不同作用域和名稱空間,以及 global 和 nonlocal 會如何影響變量綁定:
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
示例代碼的輸出是:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
請注意 局部 賦值(這是默認狀態)不會改變 scope_test 對 spam 的綁定。 nonlocal 賦值會改變 scope_test 對 spam 的綁定,而 global 賦值會改變模塊層級的綁定。
您還可以在 global 賦值之前看到之前沒有 spam 的綁定。
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__ 也是一個有效的屬性,將返回所屬類的文檔字符串: "A simple example class"。
類的 實例化 使用函數表示法。 可以把類對象視為是返回該類的一個新實例的不帶參數的函數。 舉例來說(假設使用上述的類):
x = MyClass()
創建類的新 實例 并將此對象分配給局部變量 x。
實例化操作(“調用”類對象)會創建一個空對象。 許多類喜歡創建帶有特定初始狀態的自定義實例。 為此類定義可能包含一個名為 __init__() 的特殊方法,就像這樣:
def __init__(self):
self.data = []
當一個類定義了 __init__() 方法時,類的實例化操作會自動為新創建的類實例發起調用 __init__()。 因此在這個示例中,可以通過以下語句獲得一個經初始化的新實例:
x = MyClass()
當然,__init__() 方法還可以有額外參數以實現更高靈活性。 在這種情況下,提供給類實例化運算符的參數將被傳遞給 __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 示例中,這將返回字符串 'hello world'。 但是,立即調用一個方法并不是必須的: x.f 是一個方法對象,它可以被保存起來以后再調用。 例如:
xf = x.f
while True:
print(xf())
將繼續打印 hello world,直到結束。
當一個方法被調用時到底發生了什么? 你可能已經注意到上面調用 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'
正如 名稱和對象 中已討論過的,共享數據可能在涉及 mutable 對象例如列表和字典的時候導致令人驚訝的結果。 例如以下代碼中的 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. 補充說明?
數據屬性會覆蓋掉具有相同名稱的方法屬性;為了避免會在大型程序中導致難以發現的錯誤的意外名稱沖突,明智的做法是使用某種約定來最小化沖突的發生幾率。 可能的約定包括方法名稱使用大寫字母,屬性名稱加上獨特的短字符串前綴(或許只加一個下劃線),或者是用動詞來命名方法,而用名詞來命名數據屬性。
數據屬性可以被方法以及一個對象的普通用戶(“客戶端”)所引用。 換句話說,類不能用于實現純抽象數據類型。 實際上,在 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 中所有的方法實際上都是 virtual 方法。)
在派生類中的重載方法實際上可能想要擴展而非簡單地替換同名的基類方法。 有一種方式可以簡單地直接調用基類方法:即調用 BaseClassName.methodname(self, arguments)。 有時這對客戶端來說也是有用的。 (請注意僅當此基類可在全局作用域中以 BaseClassName 的名稱被訪問時方可使用此方式。)
Python有兩個內置函數可被用于繼承機制:
使用
isinstance()來檢查一個實例的類型:isinstance(obj, int)僅會在obj.__class__為int或某個派生自int的類時為True。使用
issubclass()來檢查類的繼承關系:issubclass(bool, int)為True,因為bool是int的子類。 但是,issubclass(float, int)為False,因為float不是int的子類。
9.5.1. 多重繼承?
Python 也支持一種多重繼承。 帶有多個基類的類定義語句如下所示:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
對于多數應用來說,在最簡單的情況下,你可以認為搜索從父類所繼承屬性的操作是深度優先、從左至右的,當層次結構中存在重疊時不會在同一個類中搜索兩次。 因此,如果某一屬性在 DerivedClassName 中未找到,則會到 Base1 中搜索它,然后(遞歸地)到 Base1 的基類中搜索,如果在那里未找到,再到 Base2 中搜索,依此類推。
真實情況比這個更復雜一些;方法解析順序會動態改變以支持對 super() 的協同調用。 這種方式在某些其他多重繼承型語言中被稱為后續方法調用,它比單繼承型語言中的 super 調用更強大。
動態改變順序是有必要的,因為所有多重繼承的情況都會顯示出一個或更多的菱形關聯(即至少有一個父類可通過多條路徑被最底層類所訪問)。 例如,所有類都是繼承自 object,因此任何多重繼承的情況都提供了一條以上的路徑可以通向 object。 為了確保基類不會被訪問一次以上,動態算法會用一種特殊方式將搜索順序線性化, 保留每個類所指定的從左至右的順序,只調用每個父類一次,并且保持單調(即一個類可以被子類化而不影響其父類的優先順序)。 總而言之,這些特性使得設計具有多重繼承的可靠且可擴展的類成為可能。 要了解更多細節,請參閱 https://www.python.org/download/releases/2.3/mro/。
9.6. 私有變量?
那種僅限從一個對象內部訪問的“私有”實例變量在 Python 中并不存在。 但是,大多數 Python 代碼都遵循這樣一個約定:帶有一個下劃線的名稱 (例如 _spam) 應該被當作是 API 的非公有部分 (無論它是函數、方法或是數據成員)。 這應當被視為一個實現細節,可能不經通知即加以改變。
由于存在對于類私有成員的有效使用場景(例如避免名稱與子類所定義的名稱相沖突),因此存在對此種機制的有限支持,稱為 名稱改寫。 任何形式為 __spam 的標識符(至少帶有兩個前綴下劃線,至多一個后綴下劃線)的文本將被替換為 _classname__spam,其中 classname 為去除了前綴下劃線的當前類名稱。 這種改寫不考慮標識符的句法位置,只要它出現在類定義內部就會進行。
名稱改寫有助于讓子類重載方法而不破壞類內方法調用。例如:
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)
上面的示例即使在 MappingSubclass 引入了一個 __update 標識符的情況下也不會出錯,因為它會在 Mapping 類中被替換為 _Mapping__update 而在 MappingSubclass 類中被替換為 _MappingSubclass__update。
請注意,改寫規則的設計主要是為了避免意外沖突;訪問或修改被視為私有的變量仍然是可能的。這在特殊情況下甚至會很有用,例如在調試器中。
請注意傳遞給 exec() 或 eval() 的代碼不會將發起調用類的類名視作當前類;這類似于 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.__self__ 就是帶有 m() 方法的實例對象,而 m.__func__ 則是該方法所對應的函數對象。
9.8. 迭代器?
到目前為止,您可能已經注意到大多數容器對象都可以使用 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, end='')
這種訪問風格清晰、簡潔又方便。 迭代器的使用非常普遍并使得 Python 成為一個統一的整體。 在幕后,for 語句會在容器對象上調用 iter()。 該函數返回一個定義了 __next__() 方法的迭代器對象,此方法將逐一訪問容器中的元素。 當元素用盡時,__next__() 將引發 StopIteration 異常來通知終止 for 循環。 你可以使用 next() 內置函數來調用 __next__() 方法;這個例子顯示了它的運作方式:
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
看過迭代器協議的幕后機制,給你的類添加迭代器行為就很容易了。 定義一個 __iter__() 方法來返回一個帶有 __next__() 方法的對象。 如果類已定義了 __next__(),則 __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.9. 生成器?
Generator 是一個用于創建迭代器的簡單而強大的工具。 它們的寫法類似標準的函數,但當它們要返回數據時會使用 yield 語句。 每次對生成器調用 next() 時,它會從上次離開位置恢復執行(它會記住上次執行語句時的所有數據值)。 顯示如何非常容易地創建生成器的示例如下:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
>>> char in reverse('golf'):
... print(char)
...
f
l
o
g
可以用生成器來完成的操作同樣可以用前一節所描述的基于類的迭代器來完成。 但生成器的寫法更為緊湊,因為它會自動創建 __iter__() 和 __next__() 方法。
另一個關鍵特性在于局部變量和執行狀態會在每次調用之間自動保存。 這使得該函數相比使用 self.index 和 self.data 這種實例變量的方式更易編寫且更為清晰。
除了會自動創建方法和保存程序狀態,當生成器終結時,它們還會自動引發 StopIteration。 這些特性結合在一起,使得創建迭代器能與編寫常規函數一樣容易。
9.10. 生成器表達式?
某些簡單的生成器可以寫成簡潔的表達式代碼,所用語法類似列表推導式,但外層為圓括號而非方括號。 這種表達式被設計用于生成器將立即被外層函數所使用的情況。 生成器表達式相比完整的生成器更緊湊但較不靈活,相比等效的列表推導式則更為節省內存。
示例:
>>> 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 = {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']
備注
