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
另一種執行個體屬性參考是 方法 (method)。方法是"屬於"一個物件的函式。
實例物件的有效 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()
如果像上面一樣 x = 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' # 所有實例共享的類別變數
def __init__(self, name):
self.name = name # 每個實例獨有的實例變數
>>> d = Dog('Fido')
>>> e