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 個巢狀作用域,其名稱空間可以直接訪問

  • 最內層作用域,首先搜尋,包含區域性名稱

  • 任何封閉函式的作用域,從最近的封閉作用域開始搜尋,包含非區域性但也是非全域性的名稱

  • 倒數第二個作用域包含當前模組的全域性名稱

  • 最外層作用域(最後搜尋)是包含內建名稱的名稱空間

如果一個名稱被宣告為全域性的,那麼所有引用和賦值都直接轉到包含模組全域性名稱的倒數第二個作用域。要重新繫結在最內層作用域之外找到的變數,可以使用 nonlocal 語句;如果未宣告為 nonlocal,則這些變數是隻讀的(嘗試寫入此類變數只會最內層作用域中建立一個新的區域性變數,而保持同名的外部變數不變)。

通常,區域性作用域引用(文字上)當前函式的區域性名稱。在函式外部,區域性作用域引用與全域性作用域相同的名稱空間:模組的名稱空間。類定義將另一個名稱空間置於區域性作用域中。

重要的是要意識到作用域是文字上確定的:在模組中定義的函式的全域性作用域是該模組的名稱空間,無論從哪裡或透過什麼別名呼叫該函式。另一方面,對名稱的實際搜尋是在執行時動態完成的——但是,語言定義正在朝著在“編譯”時進行靜態名稱解析的方向發展,因此不要依賴動態名稱解析!(事實上,區域性變數已經靜態確定了。)

Python 的一個特殊之處在於,如果沒有任何 globalnonlocal 語句生效,則對名稱的賦值總是進入最內層的作用域。賦值不會複製資料,它們只是將名稱繫結到物件。刪除也是如此:語句 del x 從區域性作用域引用的名稱空間中移除 x 的繫結。事實上,所有引入新名稱的操作都使用區域性作用域:特別是 import 語句和函式定義會將模組或函式名稱繫結到區域性作用域中。

global 語句可以用來指示特定的變數存在於全域性作用域中,並且應該在那裡重新繫結;nonlocal 語句指示特定的變數存在於封閉作用域中,並且應該在那裡重新繫結。

9.2.1. 作用域和名稱空間示例

這是一個演示如何引用不同的作用域和名稱空間,以及 globalnonlocal 如何影響變數繫結的示例

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_testspam 的繫結。nonlocal 賦值改變了 scope_testspam 的繫結,而 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.iMyClass.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.fMyClass.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'

正如 關於名稱和物件的說明 中討論的那樣,共享資料可能會在使用 可變 物件(例如列表和字典)時產生可能令人驚訝的效果。例如,以下程式碼中的 *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

現在 fgh 都是類 C 的屬性,它們引用函式物件,因此它們都是 C 例項的方法——hg 完全等效。請注意,這種做法通常只會使程式的讀者感到困惑。

方法可以透過使用 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,因為 boolint 的子類。但是,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 的路徑。為了防止基類被訪問多次,動態演算法會以一種保留每個類中指定的從左到右的順序,並且每個父類只調用一次的單調方式來線性化搜尋順序(這意味著可以在不影響其父類的優先順序順序的情況下對類進行子類化)。綜上所述,這些屬性使得設計具有多重繼承的可靠且可擴充套件的類成為可能。有關更多詳細資訊,請參閱 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 “record” 或 C “struct” 的資料型別很有用,它可以將一些命名的資料項捆綁在一起。慣用的方法是為此目的使用 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

瞭解了迭代器協議背後的機制後,很容易將迭代器行為新增到你的類中。定義一個返回帶有 __next__() 方法的物件的 __iter__() 方法。如果該類定義了 __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.indexself.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']

腳註