9. Class(類別)¶
Class 提供了一種結合資料與功能的手段。建立一個 class 將會新增一個物件的型別 (type),並且允許建立該型別的新實例 (instance)。每一個 class 實例可以擁有一些維持該實例狀態的屬性 (attribute)。Class 實例也可以有一些(由其 class 所定義的)method(方法),用於修改該實例的狀態。
與其他程式語言相比,Python 的 class 機制為 class 增加了最少的新語法跟語意。他混合了 C++ 和 Modula-3 的 class 機制。Python 的 class 提供了所有物件導向程式設計 (Object Oriented Programming) 的標準特色:class 繼承機制允許多個 base class(基底類別),一個 derived class(衍生類別)可以覆寫 (override) 其 base class 的任何 method,且一個 method 可以用相同的名稱呼叫其 base class 的 method。物件可以包含任意數量及任意種類的資料。如同模組一樣,class 也具有 Python 的動態特性:他們在執行期 (runtime) 被建立,且可以在建立之後被修改。
在 C++ 的術語中,class 成員(包含資料成員)通常都是公開的(除了以下內容:私有變數),而所有的成員函式都是虛擬的。如同在 Modula-3 中一樣,Python 並沒有提供簡寫可以從物件的 method 裡參照其成員:method 函式與一個外顯的 (explicit)、第一個代表物件的引數被宣告,而此引數是在呼叫時隱性地 (implicitly) 被提供。如同在 Smalltak 中,class 都是物件,這為 import 及重新命名提供了語意。不像 C++ 和 Modula-3,Pyhon 內建的型別可以被使用者以 base class 用於其他擴充 (extension)。另外,如同在 C++ 中,大多數有著特別語法的內建運算子(算術運算子、下標等)都可以為了 class 實例而被重新定義。
(由於缺乏普遍能接受的術語來討論 class,我偶爾會使用 Smalltalk 和 C++ 的術語。我會使用 Modula-3 的術語,因為它比 C++ 更接近 Python 的物件導向語意,但我預期比較少的讀者會聽過它。)
9.1. 關於名稱與物件的一段話¶
物件有個體性 (individuality),且多個名稱(在多個作用域 (scope) )可以被連結到相同的物件。這在其他語言中被稱為別名 (aliasing)。初次接觸 Python 時通常不會注意這件事,而在處理不可變的基本型別(數值、字串、tuple)時,它也可以安全地被忽略。然而,別名在含有可變物件(如 list(串列)、dictionary(字典)、和大多數其他的型別)的 Python 程式碼語意中,可能會有意外的效果。這通常有利於程式,因為別名在某些方面表現得像指標 (pointer)。舉例來說,在實作時傳遞一個物件是便宜的,因為只有指標被傳遞;假如函式修改了一個作為引數傳遞的物件,呼叫函式者 (caller) 能夠見到這些改變——這消除了在 Pascal 中兩個相異引數傳遞機制的需求。
9.2. Python 作用域 (Scope) 及命名空間 (Namespace)¶
在介紹 class 之前,我必須先告訴你一些關於 Python 作用域的規則。Class definition(類別定義)以命名空間展現了一些俐落的技巧,而你需要了解作用域和命名空間的運作才能完整理解正在發生的事情。順帶一提,關於這個主題的知識對任何進階的 Python 程式設計師都是很有用的。
讓我們從一些定義開始。
命名空間是從名稱到物件的對映。大部分的命名空間現在都是以 Python 的 dictionary 被實作,但通常不會以任何方式被察覺(除了性能),且它可能會在未來改變。命名空間的例子有:內建名稱的集合(包含如 abs()
的函式,和內建的例外名稱);模組中的全域 (global) 名稱;和在函式調用中的區域 (local) 名稱。某種意義上,物件中的屬性集合也會形成一個命名空間。關於命名空間的重要一點是,不同命名空間中的名稱之間絕對沒有關係;舉例來說,兩個不一樣的模組都可以定義一個 maximize
函式而不會混淆——模組的使用者必須為它加上前綴 (prefix) 模組名稱。
順帶一提,我使用屬性 (attribute) 這個字,統稱句號 (dot) 後面的任何名稱——例如,運算式中的 z.real
,real
是物件 z
的一個屬性。嚴格來說,模組中名稱的參照都是屬性參照:在運算式 modname.funcname
中,modname
是模組物件而 funcname
是它的屬性。在這種情況下,模組的屬性和模組中定義的全域名稱碰巧有一個直接的對映:他們共享了相同的命名空間![1]
屬性可以是唯讀的或可寫的。在後者的情況下,對屬性的賦值是可能的。模組屬性是可寫的:你可以寫 modname.the_answer = 42
。可寫屬性也可以用 del
陳述式刪除。例如,del modname.the_answer
將從名為 modname
的物件中刪除屬性 the_answer
。
命名空間在不同的時刻被建立,並且有不同的壽命。當 Python 直譯器啟動時,含有內建名稱的命名空間會被建立,並且永遠不會被刪除。當模組定義被讀入時,模組的全域命名空間會被建立;一般情況下,模組的命名空間也會持續到直譯器結束。被直譯器的頂層調用 (top-level invocation) 執行的陳述式,不論是從腳本檔案讀取的或是互動模式中的,會被視為一個稱為 __main__
的模組的一部分,因此它們具有自己的全域命名空間。(內建名稱實際上也存在一個模組中,它被稱為 builtins
。)
函式的區域命名空間是在呼叫函式時建立的,而當函式返回,或引發了未在函式中處理的例外時,此命名空間將會被刪除。(實際上,忘記是描述實際發生的事情的更好方法。) 當然,每個遞迴調用 (recursive invocation) 都有自己的區域命名空間。
作用域是 Python 程式中的一個文本區域 (textual region),在此區域,命名空間是可直接存取的。這裡的「可直接存取的」意思是,對一個名稱的非限定參照 (unqualified reference) 可以在命名空間內嘗試尋找該名稱。
儘管作用域是靜態地被決定,但它們是動態地被使用的。在執行期間內的任何時間點,都會有 3 或 4 個巢狀的作用域,其命名空間是可以被直接存取的:
最內層作用域,會最先被搜尋,而它包含了區域名稱
任何外圍函式 (enclosing function) 的作用域,會從最近的外圍作用域開始搜尋,它包含了非區域 (non-local) 和非全域 (non-global) 的名稱
倒數第二個作用域,包含當前模組的全域名稱
最外面的作用域(最後搜尋),是包含內建名稱的命名空間
如果一個名稱被宣告為全域,則所有的參照和賦值將直接轉到包含模組全域名稱的倒數第二個作用域。要重新連結最內層作用域以外找到的變數,可以使用 nonlocal
陳述式;如果那些變數沒有被宣告為 nonlocal,則它們會是唯讀的(嘗試寫入這樣的變數只會在最內層的作用域內建立一個新的區域變數,同名的外部變數則維持不變)。
通常,區域作用域會參照(文本的)當前函式的區域名稱。在函式外部,區域作用域與全域作用域參照相同的命名空間:模組的命名空間。然而,Class definition 會在區域作用域中放置另一個命名空間。
務必要了解,作用域是按文本被決定的:在模組中定義的函式,其全域作用域便是該模組的命名空間,無論函式是從何處或以什麼別名被呼叫。另一方面,對名稱的實際搜尋是在執行時期 (run time) 動態完成的——但是,語言定義的發展,正朝向在「編譯」時期 (compile time) 的靜態名稱解析 (static name resolution),所以不要太依賴動態名稱解析 (dynamic name resolution)! (事實上,局部變數已經是靜態地被決定。)
一個 Python 的特殊癖好是——假如沒有 global
或 nonlocal
陳述式的效果——名稱的賦值 (assignment) 都會指向最內層作用域。賦值不會複製資料——它們只會把名稱連結至物件。刪除也是一樣:陳述式 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. 初見 class¶
Class 採用一些新的語法,三個新的物件型別,以及一些新的語意。
9.3.1. Class definition(類別定義)語法¶
Class definition 最簡單的形式如下:
class ClassName:
<statement-1>
.
.
.
<statement-N>
Class definition,如同函式定義(def
陳述式),必須在它們有任何效果前先執行。(你可以想像把 class definition 放在一個 if
陳述式的分支,或在函式裡。)
在實作時,class definition 內的陳述式通常會是函式定義,但其他陳述式也是允許的,有時很有用——我們稍後會回到這裡。Class 中的函式定義通常會有一個獨特的引數列表形式,取決於 method 的呼叫慣例——再一次地,這將會在稍後解釋。
當進入 class definition,一個新的命名空間將會被建立,並且作為區域作用域——因此,所有區域變數的賦值將進入這個新的命名空間。特別是,函式定義會在這裡連結新函式的名稱。
正常地(從結尾處)離開 class definition 時,一個 class 物件會被建立。基本上這是一個包裝器 (wrapper),裝著 class definition 建立的命名空間內容;我們將在下一節中更加了解 class 物件。原始的區域作用域(在進入 class definition 之前已生效的作用域)會恢復,在此 class 物件會被連結到 class definition 標頭中給出的 class 名稱(在範例中為 ClassName
)。
9.3.2. Class 物件¶
Class 物件支援兩種運算:屬性參照 (attribute reference) 和實例化 (instantiation)。
屬性參照使用 Python 中所有屬性參照的標準語法:obj.name
。有效的屬性名稱是 class 物件被建立時,class 的命名空間中所有的名稱。所以,如果 class definition 看起來像這樣:
class MyClass:
"""一個簡單的類別範例"""
i = 12345
def f(self):
return 'hello world'
那麼 MyClass.i
和 MyClass.f
都是有效的屬性參照,會分別回傳一個整數和一個函式物件。Class 屬性也可以被指派 (assign),所以你可以透過賦值改變 MyClass.i
的值。__doc__
也是一個有效的屬性,會回傳屬於該 class 的說明字串 (docstring):"A simple example class"
。
Class 實例化使用了函式記法 (function notation)。就好像 class 物件是一個沒有參數的函式,它回傳一個新的 class 實例。例如(假設是上述的 class):
x = MyClass()
建立 class 的一個新實例,並將此物件指派給區域變數 x
。
實例化運算(「呼叫」一個 class 物件)會建立一個空的物件。許多 class 喜歡在建立物件時有著自訂的特定實例初始狀態。因此,class 可以定義一個名為 __init__()
的特別 method,像這樣:
def __init__(self):
self.data = []
當 class 定義了 __init__()
method,class 實例化會為新建的 class 實例自動調用 __init__()
。所以在這個範例中,一個新的、初始化的實例可以如此獲得:
x = MyClass()
當然,__init__()
method 可能為了更多的彈性而有引數。在這種情況下,要給 class 實例化運算子的引數會被傳遞給 __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. 實例物件¶
現在,我們可以如何處理實例物件?實例物件能理解的唯一運算就是屬性參照。有兩種有效的屬性名稱:資料屬性 (data attribute) 和 method。
資料屬性對應 Smalltalk 中的「實例變數」,以及 C++ 中的「資料成員」。資料屬性不需要被宣告;和區域變數一樣,它們在第一次被賦值時就會立即存在。例如,如果 x
是 MyClass
在上述例子中建立的實例,下面的程式碼將印出值 16
,而不留下蹤跡:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
The other kind of instance attribute reference is a method. A method is a function that "belongs to" an object.
實例物件的有效 method 名稱取決於其 class。根據定義,一個 class 中所有的函式物件屬性,就定義了實例的對應 method。所以在我們的例子中,x.f
是一個有效的 method 參照,因為 MyClass.f
是一個函式,但 x.i
不是,因為 MyClass.i
不是。但 x.f
與 MyClass.f
是不一樣的——它是一個 method 物件,而不是函式物件。
9.3.4. Method 物件¶
通常,一個 method 在它被連結後隨即被呼叫:
x.f()
在 MyClass
的例子中,這將回傳字串 'hello world'
。然而,並沒有必要立即呼叫一個 method:x.f
是一個 method 物件,並且可以被儲藏起來,之後再被呼叫。舉例來說:
xf = x.f
while True:
print(xf())
將會持續印出 hello world
直到天荒地老。
當一個 method 被呼叫時究竟會發生什麼事?你可能已經注意到 x.f()
被呼叫時沒有任何的引數,儘管 f()
的函式定義有指定一個引數。這個引數發生了什麼事?當一個需要引數的函式被呼叫而沒有給任何引數時,Python 肯定會引發例外——即使該引數實際上沒有被使用...
事實上,你可能已經猜到了答案:method 的特殊之處在於,實例物件會作為函式中的第一個引數被傳遞。在我們的例子中,x.f()
這個呼叫等同於 MyClass.f(x)
。一般來說,呼叫一個有 n 個引數的 method,等同於呼叫一個對應函式,其引數列表 (argument list) 被建立時,會在第一個引數前插入該 method 的實例物件。
一般來說,方法的工作原理如下。當一個實例的非資料屬性被參照時,將會搜尋該實例的 class。如果該名稱是一個有效的 class 屬性,而且是一個函式物件,則對實例物件和函式物件的參照都會被打包到方法物件中。當使用引數串列呼叫方法物件時,會根據實例物件和引數串列來建構一個新的引數串列,並使用該新引數串列來呼叫函式物件。
9.3.5. Class 及實例變數¶
一般來說,實例變數用於每一個實例的獨特資料,而 class 變數用於該 class 的所有實例共享的屬性和 method:
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 物件,如 list 和 dictionary,可能會產生意外的影響。舉例來說,下列程式碼的 tricks list 不應該作為一個 class 變數使用,因為這個 list 將會被所有的 Dog 實例所共享:
class Dog:
tricks = [] # 誤用類別變數
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 # 出乎意料地被所有 dog 共享
['roll over', 'play dead']
正確的 class 設計應該使用實例變數:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # 為每一個 dog 建立空 list
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 中,則屬性的尋找會以實例為優先:
>>> 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
資料屬性可能被 method 或是被物件的一般使用者(「客戶端」)所參照。也就是說,class 不可用於實作純粹抽象的資料型別。事實上,在 Python 中沒有任何可能的方法,可強制隱藏資料——這都是基於慣例。(另一方面,以 C 編寫的 Python 實作可以完全隱藏實作細節並且在必要時控制物件的存取;這可以被以 C 編寫的 Python 擴充所使用。)
客戶端應該小心使用資料屬性——客戶端可能會因為覆寫他們的資料屬性,而破壞了被 method 維護的不變性。注意,客戶端可以增加他們自己的資料屬性到實例物件,但不影響 method 的有效性,只要避免名稱衝突即可——再一次提醒,命名慣例可以在這裡節省很多麻煩。
在 method 中參照資料屬性(或其他 method!)是沒有簡寫的。我發現這實際上增加了 method 的可閱讀性:在瀏覽 method 時,絕不會混淆區域變數和實例變數。
通常,方法的第一個引數稱為 self
。這僅僅只是一個慣例:self
這個名字對 Python 來說完全沒有特別的意義。但請注意,如果不遵循慣例,你的程式碼可能對其他 Python 程式設計師來說可讀性較低,此外,也可以想像一個可能因信任此慣例而編寫的 class 瀏覽器 (browser) 程式。
任何一個作為 class 屬性的函式物件都為該 class 的實例定義了一個相應的 method。函式定義不一定要包含在 class definition 的文本中:將函式物件指定給 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
都是 class C
的屬性,並指向函式物件,所以他們都是class C
實例的 method —— h
與 g
是完全一樣的。請注意,這種做法通常只會使該程式的讀者感到困惑。
Method 可以藉由使用 self
引數的 method 屬性,呼叫其他 method:
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)
Method 可以用與一般函式相同的方式參照全域名稱。與 method 相關的全域作用域,就是包含其定義的模組。(class 永遠不會被用作全域作用域。)雖然人們很少有在 method 中使用全域資料的充分理由,但全域作用域仍有許多合法的使用:比方說,被 import 至全域作用域的函式和模組,可以被 method 以及在該作用域中定義的函式和 class 所使用。通常,包含 method 的 class,它本身就是被定義在這個全域作用域,在下一節,我們將看到 method 想要參照自己的 class 的一些好原因。
每個值都是一個物件,因此都具有一個 class,也可以稱為它的 type(型別)。它以 object.__class__
被儲存。
9.5. 繼承 (Inheritance)¶
當然,如果沒有支援繼承,「class」這個語言特色就不值得被稱為 class。一個 derived class(衍生類別)定義的語法看起來如下:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
名稱 BaseClassName
必須被定義於作用域可及的命名空間,且該作用域要包含 derived class 定義。要代替 base class(基底類別)的名稱,用其他任意的運算式也是被允許的。這會很有用,例如,當一個 base class 是在另一個模組中被定義時:
class DerivedClassName(modname.BaseClassName):
執行 derived class 定義的過程,與執行 base class 相同。當 class 物件被建構時,base class 會被記住。這是用於解析屬性參照:如果一個要求的屬性無法在該 class 中找到,則會繼續在 base class 中搜尋。假如該 base class 本身也是衍生自其他 class,則這個規則會遞迴地被應用。
關於 derived class 的實例化並沒有特別之處:DerivedClassName()
會建立該 class 的一個新實例。Method 的參照被解析如下:對應的 class 屬性會被搜尋,如果需要,沿著 base class 的繼承鍊往下走,如果這產生了一個函式物件,則該 method 的參照是有效的。
Derived class 可以覆寫其 base class 的 method。因為 method 在呼叫同一個物件的其他 method 時沒有特別的特權,所以當 base class 的一個 method 在呼叫相同 base class 中定義的另一個 method 時,最終可能會呼叫到一個覆寫它的 derived class 中的 method。(給 C++ 程式設計師:Python 中所有 method 實際上都是 virtual
。)
一個在 derived class 覆寫的 method 可能事實上是想要擴充而非單純取代 base class 中相同名稱的 method。要直接呼叫 base class 的 method 有一個簡單的方法:只要呼叫 BaseClassName.methodname(self, arguments)
。這有時對客戶端也很有用。(請注意,只有在 base class 在全域作用域可以用 BaseClassName
被存取時,這方法才有效。)
Python 有兩個內建函式可以用於繼承:
使用
isinstance()
判斷一個實例的型別:isinstance(obj, int)
只有在obj.__class__
是int
或衍伸自int
時,結果才會是True
。使用
issubclass()
判斷 class 繼承:issubclass(bool, int)
會是True
,因為bool
是int
的 subclass(子類別)。但是,issubclass(float, int)
是False
,因為float
並不是int
的 subclass。
9.5.1. 多重繼承¶
Python 也支援多重繼承的形式。一個有多個 base class 的 class definition 看起來像這樣子:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
在大多數情況下,最簡單的例子裡,你可以這樣思考,對於繼承自 parent class(父類別)的屬性,其搜尋規則為:深度優先、從左到右、在階層裡重疊的相同 class 中不重複搜尋。因此,假如有一個屬性在 DerivedClassName
沒有被找到,則在 Base1
搜尋它,接著(遞迴地)在 Base1
的 base class 中搜尋,假如在那裡又沒有找到的話,會在 Base2
搜尋,依此類推。
事實上,它稍微複雜一些;method 的解析順序是動態地變化,以支援對 super()
的合作呼叫。這個方式在其他的多重繼承語言中,稱為呼叫下一個方法 (call-next-method),且比在單一繼承語言中的 super call(超級呼叫)來得更強大。
動態排序是必要的,因為多重繼承的所有情況都表現一或多的菱形關係(其中至少一個 parent class 可以從最底層 class 透過多個路徑存取)。例如,所有的 class 都繼承自 object
,因此任何多重繼承的情況都提供了多個到達 object
的路徑。為了避免 base class 被多次存取,動態演算法以這些方式將搜尋順序線性化 (linearize):保留每個 class 中規定的從左到右的順序、對每個 parent 只會呼叫一次、使用單調的 (monotonic) 方式(意思是,一個 class 可以被 subclassed(子類別化),而不會影響其 parent 的搜尋優先順序)。總之,這些特性使設計出可靠又可擴充、具有多重繼承的 class 成為可能。更多資訊,請見 The Python 2.3 Method Resolution Order。
9.6. 私有變數¶
「私有」(private) 實例變數,指的是不在物件內部便無法存取的變數,這在 Python 中是不存在的。但是,大多數 Python 的程式碼都遵守一個慣例:前綴為一個底線的名稱(如:_spam
)應被視為 API (應用程式介面)的非公有 (non-public) 部分(無論它是函式、方法或是資料成員)。這被視為一個實作細節,如有調整,亦不另行通知。
既然 class 私有的成員已有一個有效的用例(即避免名稱與 subclass 定義的名稱衝突),這種機制也存在另一個有限的支援,稱為 name mangling(名稱修飾)。任何格式為 __spam
(至少兩個前導下底線,最多一個尾隨下底線)的物件名稱 (identifier) 會被文本地被替換為 _classname__spam
,在此 classname
就是去掉前導下底線的當前 class 名稱。只要這個修飾是在 class 的定義之中發生,它就會在不考慮該物件名稱的語法位置的情況下完成。
也參考
參閱私有名稱修飾規格的詳情與特殊情況。
名稱修飾對於讓 subclass 覆寫 method 而不用破壞 class 內部的 method 呼叫,是有幫助的。舉例來說:
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
class 中,它會被替換為 _Mapping__update
,而在 MappingSubclass
class 中,它會被替換為 _MappingSubclass__update
。
請注意,修飾規則是被設計來避免意外;它仍可能存取或修改一個被視為私有的變數。這在特殊情況下甚至可能很有用,例如在除錯器 (debugger)。
另外也注意,傳遞給 exec()
或 eval()
的程式碼不會把調用 class 的名稱視為當前的 class;這和 global
陳述式的效果類似,該效果同樣僅限於整體被位元組編譯後 (byte-compiled) 的程式碼。同樣的限制適用於 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 程式碼,經常能以傳遞一個 class 來替代,此 class 模擬該資料型別的多種 method。例如,如果你有一個函式,它會從一個檔案物件來格式化某些資料,你也可以定義一個有 read()
和 readline()
method 的 class 作為替代方式,從字串緩衝區取得資料,並將其作為引數來傳遞。
實例的 method 物件也具有屬性:m.__self__
就是帶有 method m()
的實例物件,而 m.__func__
則是該 method 所對應的函式物件。
9.8. 疊代器 (Iterator)¶
到目前為止,你可能已經注意到大多數的容器 (container) 物件都可以使用 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__()
method,而此 method 會逐一存取容器中的元素。當元素用盡時,__next__()
將引發 StopIteration
例外,來通知 for
終止迴圈。你可以使用內建函式 next()
來呼叫 __next__()
method;這個例子展示了它的運作方式:
>>> 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
看過疊代器協定的幕後機制後,在你的 class 加入疊代器的行為就很容易了。定義一個 __iter__()
method 來回傳一個帶有 __next__()
method 的物件。如果 class 已定義了 __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]
>>> for char in reverse('golf'):
... print(char)
...
f
l
o
g
任何可以用產生器來完成的事,也能用以 class 為基礎的疊代器來完成,如同前一節的描述。而讓產生器的程式碼更為精簡的原因是,__iter__()
和 __next__()
method 會自動被建立。
另一個關鍵的特性在於,區域變數和執行狀態會在每次呼叫之間自動被儲存。這使得該函式比使用 self.index
和 self.data
這種實例變數的方式更容易編寫且更為清晰。
除了會自動建立 method 和儲存程式狀態,當產生器終止時,它們還會自動引發 StopIteration
。這些特性結合在一起,使建立疊代器能與編寫常規函式一樣容易。
9.10. 產生器運算式¶
某些簡單的產生器可以寫成如運算式一般的簡潔程式碼,所用的語法類似 list comprehension(串列綜合運算),但外層為括號而非方括號。這種運算式被設計用於產生器將立即被外圍函式 (enclosing function) 所使用的情況。產生器運算式與完整的產生器定義相比,程式碼較精簡但功能較少,也比等效的 list comprehension 更為節省記憶體。
例如:
>>> 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']
註解