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']
資料屬性可能被 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
用來處理特殊抽象資料型別的一段 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:
"""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)
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)) # sum of squares
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # dot product
>>> 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) 之類的東西。
2001-2025, Python Software Foundation.
This page is licensed under the Python Software Foundation License Version 2.
Examples, recipes, and other code in the documentation are additionally licensed under the Zero Clause BSD License.
See History and License for more information.