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,一個新的命名空間將會被建立,並且作為區域作用域——因此,所有區域變數的賦值將進入這個新的命名空間。特別是,函式定義會在這裡連結新函式的名稱。
当 (从结尾处) 正常离开类定义时,将创建一个 类对象。 这基本上是一个围绕类定义所创建的命名空间的包装器;我们将在下一节中了解有关类对象的更多信息。 原始的 (在进入类定义之前有效的) 作用域将重新生效,类对象将在这里与类定义头所给出的类名称进行绑定 (在这个示例中为 ClassName
)。
9.3.2. Class 物件¶
Class 物件支援兩種運算:屬性參照 (attribute reference) 和實例化 (instantiation)。
屬性參照使用 Python 中所有屬性參照的標準語法:obj.name
。有效的屬性名稱是 class 物件被建立時,class 的命名空間中所有的名稱。所以,如果 class definition 看起來像這樣:
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"
。
Class 實例化使用了函式記法 (function notation)。就好像 class 物件是一個沒有參數的函式,它回傳一個新的 class 實例。例如(假設是上述的 class):
x = MyClass()
建立 class 的一個新實例,並將此物件指派給區域變數 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. 實例物件¶
現在,我們可以如何處理實例物件?實例物件能理解的唯一運算就是屬性參照。有兩種有效的屬性名稱:資料屬性 (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 是一個「屬於」物件的函式。(在 Python 中,術語 method 並不是 class 實例所獨有的:其他物件型別也可以有 method。例如,list 物件具有稱為 append、insert、remove、sort 等 method。但是,在下面的討論中,我們將用術語 method 來專門表示 class 實例物件的 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()
在 MyClass
示例中,这将返回字符串 'hello world'
。 但是,方法并不是必须立即调用: x.f
是一个方法对象,它可以被保存起来以后再调用。 例如:
xf = x.f
while True:
print(xf())
將會持續印出 hello world
直到天荒地老。
当一个方法被调用时究竟会发生什么? 你可能已经注意到尽管 f()
的函数定义指定了一个参数,但上面调用 x.f()
时却没有带参数。 这个参数发生了什么事? 当一个需要参数的函数在不附带任何参数的情况下被调用时 Python 肯定会引发异常 --- 即使参数实际上没有被使用...
事實上,你可能已經猜到了答案:method 的特殊之處在於,實例物件會作為函式中的第一個引數被傳遞。在我們的例子中,x.f()
這個呼叫等同於 MyClass.f(x)
。一般來說,呼叫一個有 n 個引數的 method,等同於呼叫一個對應函式,其引數列表 (argument list) 被建立時,會在第一個引數前插入該 method 的實例物件。
总而言之,方法的运作方式如下。 当一个实例的非数据属性被引用时,将搜索该实例所属的类。 如果名称表示一个属于函数对象的有效类属性,则指向实例对象和函数对象的引用将被打包为一个方法对象。 当传入一个参数列表调用该方法对象时,将基于实例对象和参数列表构造一个新的参数列表,并传入这个新参数列表调用相应的函数对象。
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 = [] # 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 設計應該使用實例變數:
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 中,則屬性的尋找會以實例為優先:
>>> 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 中的區域變數也是可以的。例如:
# 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
完全等价。 但请注意这种做法通常只会使程序的阅读者感到迷惑。
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>
The name BaseClassName
must be defined in a scope containing the
derived class definition. In place of a base class name, other arbitrary
expressions are also allowed. This can be useful, for example, when the base
class is defined in another module:
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>
对于多数目的来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左到右的,当层次结构存在重叠时不会在同一个类中搜索两次。 因此,如果某个属性在 DerivedClassName
中找不到,就会在 Base1
中搜索它,然后(递归地)在 Base1
的基类中搜索,如果在那里也找不到,就将在 Base2
中搜索,依此类推。
事實上,它稍微複雜一些;method 的解析順序是動態地變化,以支援對 super()
的合作呼叫。這個方式在其他的多重繼承語言中,稱為呼叫下一個方法 (call-next-method),且比在單一繼承語言中的 super call(超級呼叫)來得更強大。
動態排序是必要的,因為多重繼承的所有情況都表現一或多的菱形關係(其中至少一個 parent class 可以從最底層 class 透過多個路徑存取)。例如,所有的 class 都繼承自 object
,因此任何多重繼承的情況都提供了多個到達 object
的路徑。為了避免 base class 被多次存取,動態演算法以這些方式將搜尋順序線性化 (linearize):保留每個 class 中規定的從左到右的順序、對每個 parent 只會呼叫一次、使用單調的 (monotonic) 方式(意思是,一個 class 可以被 subclassed(子類別化),而不會影響其 parent 的搜尋優先順序)。總之,這些特性使設計出可靠又可擴充、具有多重繼承的 class 成為可能。更多資訊,請見 https://www.python.org/download/releases/2.3/mro/。
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 代码通常可以通过传入一个模拟了该数据类型的方法的类作为替代。 例如,如果你有一个基于文件对象来格式化某些数据的函数,你可以定义一个带有 read()
和 readline()
方法以便从字典串缓冲区获取数据的类,并将其作为参数传入。
实例方法对象 也具有属性: m.__self__
就是带有 m()
方法的实例对象,而 m.__func__
就是该方法所对应的 函数对象。
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
了解了迭代器协议背后的机制后,就可以轻松地为你的类添加迭代器行为了。 定义 __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]
>>> for char in reverse('golf'):
... print(char)
...
f
l
o
g
可以用生成器来完成的任何功能同样可以通用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()
和 __next__()
方法。
另一個關鍵的特性在於,區域變數和執行狀態會在每次呼叫之間自動被儲存。這使得該函式比使用 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']
註解