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()
這樣的函式和內建異常名稱);模組中的全域性名稱;以及函式呼叫中的區域性名稱。從某種意義上說,物件的屬性集也形成一個名稱空間。關於名稱空間需要了解的重要一點是,不同名稱空間中的名稱之間絕對沒有關係;例如,兩個不同的模組都可以定義一個函式 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 程式中可以直接訪問名稱空間的文字區域。“直接可訪問”在這裡意味著對名稱的不合格引用會嘗試在名稱空間中查詢該名稱。
雖然作用域是靜態確定的,但它們是動態使用的。在執行的任何時候,都有 3 或 4 個巢狀作用域的名稱空間可以直接訪問
最內層作用域,首先被搜尋,包含區域性名稱
任何 enclosing 函式的作用域,從最近的 enclosing 作用域開始搜尋,包含非區域性但也不是全域性的名稱
倒數第二個作用域包含當前模組的全域性名稱
最外層作用域(最後搜尋)是包含內建名稱的名稱空間
如果一個名稱被宣告為全域性的,那麼所有的引用和賦值都會直接指向包含模組全域性名稱的倒數第二個作用域。為了重新繫結在最內層作用域之外找到的變數,可以使用 nonlocal
語句;如果沒有宣告為非區域性變數,這些變數是隻讀的(嘗試寫入這樣的變數只會建立最內層作用域中的一個 新 區域性變數,而使同名的外部變數保持不變)。
通常,區域性作用域引用(文字上)當前函式的區域性名稱。在函式外部,區域性作用域引用與全域性作用域相同的名稱空間:模組的名稱空間。類定義會在區域性作用域中放置另一個名稱空間。
重要的是要認識到作用域是文字決定的:在模組中定義的函式的全域性作用域是該模組的名稱空間,無論函式是從何處或以何別名呼叫。另一方面,名稱的實際搜尋是在執行時動態完成的——但是,語言定義正在朝著在“編譯”時進行靜態名稱解析的方向發展,所以不要依賴動態名稱解析!(事實上,區域性變數已經靜態確定了。)
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
另一種例項屬性引用是 方法。方法是“屬於”一個物件的函式。
例項物件的有效方法名稱取決於其類。根據定義,類的所有函式物件屬性都定義了其例項的相應方法。因此在我們的示例中,x.f
是一個有效的方法引用,因為 MyClass.f
是一個函式,而 x.i
不是,因為 MyClass.i
不是。但是 x.f
與 MyClass.f
不同——它是一個 方法物件,而不是一個函式物件。
9.3.4. 方法物件¶
通常,方法在繫結後立即呼叫
x.f()
如果 x = 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'
如 關於名稱和物件 中所討論的,共享資料對於涉及 可變 物件(如列表和字典)的情況可能會產生令人驚訝的影響。例如,以下程式碼中的 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. 隨筆¶
如果例項和類中出現相同的屬性名稱,則屬性查詢會優先考慮例項
>>> class Warehouse:
... purpose = 'storage'
... region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
資料屬性既可以被方法引用,也可以被物件的普通使用者(“客戶端”)引用。換句話說,類不能用於實現純抽象資料型別。實際上,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()
的協作呼叫。這種方法在其他一些多重繼承語言中被稱為 call-next-method,並且比單繼承語言中的 super 呼叫更強大。
動態排序是必要的,因為所有多重繼承的情況都表現出一個或多個菱形關係(其中至少一個父類可以透過多條路徑從最底層的類訪問)。例如,所有類都繼承自 object
,因此任何多重繼承的情況都提供了多條路徑來達到 object
。為了防止基類被多次訪問,動態演算法以一種保持每個類中指定的從左到右排序、只調用每個父類一次並且是單調的方式線性化搜尋順序(這意味著一個類可以被子類化而不影響其父類的優先順序)。總而言之,這些特性使得設計具有多重繼承的可靠且可擴充套件的類成為可能。有關更多詳細資訊,請參閱 Python 2.3 方法解析順序。
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 的“記錄”或 C 的“結構”的資料型別很有用,它將一些命名資料項捆綁在一起。慣用的方法是為此目的使用 dataclasses
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> 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
<str_iterator object at 0x10c90e650>
>>> 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. 生成器¶
生成器 是建立迭代器的一個簡單而強大的工具。它們像普通函式一樣編寫,但在需要返回資料時使用 yield
語句。每次呼叫 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__()
和 __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
>>> 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']
腳註