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 = Dog('Buddy')
>>> d.kind # 為所有 Dog 實例所共享
'canine'
>>> e.kind # 為所有 Dog 實例所共享
'canine'
>>> d.name # d 獨有
'Fido'
>>> e.name # 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']
資料屬性可能被 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 成為可能。更多資訊,請見 Python 2.3 方法解析順序。
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 # 原始 update() 方法的私有副本
class MappingSubclass(Mapping):
def update(self, keys, values):
# 為 update() 提供新的函式簽名
# 但不會破壞 __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
用來處理特殊抽象資料型別的一段 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)
<str_iterator object at 0x10c90e650>
>>> next(it)
>>> next(it)
>>> next(it)
>>> 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:
"""用於向後迴圈遍歷序列的疊代器。"""
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)
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)
任何可以用產生器來完成的事,也能用以 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)) # 平方和
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # 向量內積
>>> 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']
[1]
有一個例外。模組物件有一個秘密的唯讀屬性,稱為 __dict__,它回傳用於實作模組命名空間的 dictionary;__dict__ 這個名稱是一個屬性但不是全域名稱。顯然,使用此屬性將違反命名空間實作的抽象化,而應該僅限用於事後除錯器 (post-mortem debugger) 之類的東西。