GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing python.github.io — 9. Class(類別) — Python 3.15.2 說明文件 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 被實作,但通常不會以任何方式被察覺(除了性能),且它可能會在未來改變。命名空間的例子有:內建名稱的集合(包含如 順帶一提,我使用屬性 (attribute) 這個字,統稱句號 (dot) 後面的任何名稱——例如,運算式中的 屬性可以是唯讀的或可寫的。在後者的情況下,對屬性的賦值是可能的。模組屬性是可寫的:你可以寫 命名空間在不同的時刻被建立,並且有不同的壽命。當 Python 直譯器啟動時,含有內建名稱的命名空間會被建立,並且永遠不會被刪除。當模組定義被讀入時,模組的全域命名空間會被建立;一般情況下,模組的命名空間也會持續到直譯器結束。被直譯器的頂層呼叫 (top-level invocation) 執行的陳述式,不論是從腳本檔案讀取的或是互動模式中的,會被視為一個稱為 函式的區域命名空間是在呼叫函式時建立的,而當函式回傳,或引發了未在函式中處理的例外時,此命名空間將會被刪除。(實際上,忘記是描述實際發生的事情的更好方法。) 當然,每個遞迴呼叫 (recursive invocation) 都有自己的區域命名空間。 作用域是 Python 程式中的一個文本區域 (textual region),在此區域,命名空間是可直接存取的。這裡的「可直接存取的」意思是,對一個名稱的非限定參照 (unqualified reference) 可以在命名空間內嘗試尋找該名稱。 儘管作用域是靜態地被決定,但它們是動態地被使用的。在執行期間內的任何時間點,都會有 3 或 4 個巢狀的作用域,其命名空間是可以被直接存取的:
如果一個名稱被宣告為全域,則所有的參照和賦值將直接轉到包含模組全域名稱的倒數第二個作用域。要重新連結最內層作用域以外找到的變數,可以使用 通常,區域作用域會參照(文本的)目前函式的區域名稱。在函式外部,區域作用域與全域作用域參照相同的命名空間:模組的命名空間。然而,Class definition 會在區域作用域中放置另一個命名空間。 務必要了解,作用域是按文本被決定的:在模組中定義的函式,其全域作用域便是該模組的命名空間,無論函式是從何處或以什麼別名被呼叫。另一方面,對名稱的實際搜尋是在執行時期 (run time) 動態完成的——但是,語言定義的發展,正朝向在「編譯」時期 (compile time) 的靜態名稱解析 (static name resolution),所以不要太依賴動態名稱解析 (dynamic name resolution)! (事實上,局部變數已經是靜態地被決定。) 一個 Python 的特殊癖好是——假如沒有
9.2.1. 作用域和命名空間的範例¶這是一個範例,演示如何參照不同的作用域和命名空間,以及 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 的連結。 你還可以發現,在 9.3. 初見 class¶Class 採用一些新的語法,三個新的物件型別,以及一些新的語意。 9.3.1. Class definition(類別定義)語法¶Class definition 最簡單的形式如下: class ClassName: <statement-1> . . . <statement-N> Class definition,如同函式定義( 在實作時,class definition 內的陳述式通常會是函式定義,但其他陳述式也是允許的,有時很有用——我們稍後會回到這裡。Class 中的函式定義通常會有一個獨特的引數列表形式,取決於 method 的呼叫慣例——再一次地,這將會在稍後解釋。 當進入 class definition,一個新的命名空間將會被建立,並且作為區域作用域——因此,所有區域變數的賦值將進入這個新的命名空間。特別是,函式定義會在這裡連結新函式的名稱。 正常地(從結尾處)離開 class definition 時,一個 class 物件會被建立。基本上這是一個包裝器 (wrapper),裝著 class definition 建立的命名空間內容;我們將在下一節中更加了解 class 物件。原始的區域作用域(在進入 class definition 之前已生效的作用域)會恢復,在此 class 物件會被連結到 class definition 標頭中給出的 class 名稱(在範例中為 9.3.2. Class 物件¶Class 物件支援兩種運算:屬性參照 (attribute reference) 和實例化 (instantiation)。 屬性參照使用 Python 中所有屬性參照的標準語法: class MyClass: """一個簡單的類別範例""" i = 12345 def f(self): return 'hello world' 那麼 Class 實例化使用了函式記法 (function notation)。就好像 class 物件是一個沒有參數的函式,它回傳一個新的 class 實例。例如(假設是上述的 class): x = MyClass() 建立 class 的一個新實例,並將此物件指派給區域變數 實例化運算(「呼叫」一個 class 物件)會建立一個空的物件。許多 class 喜歡在建立物件時有著自訂的特定實例初始狀態。因此,class 可以定義一個名為 def __init__(self): self.data = [] 當 class 定義了 x = MyClass() 當然, >>> 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.counter = 1 while x.counter < 10: x.counter = x.counter * 2 print(x.counter) del x.counter 另一種執行個體屬性參考是 方法 (method)。方法是"屬於"一個物件的函式。 實例物件的有效 method 名稱取決於其 class。根據定義,一個 class 中所有的函式物件屬性,就定義了實例的對應 method。所以在我們的例子中, 9.3.4. Method 物件¶通常,一個 method 在它被連結後隨即被呼叫: x.f() 如果像上面一樣 xf = x.f while True: print(xf()) 將會持續印出 當一個 method 被呼叫時究竟會發生什麼事?你可能已經注意到 事實上,你可能已經猜到了答案: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'] 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 時,絕不會混淆區域變數和實例變數。 通常,方法的第一個引數稱為 任何一個作為 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 現在 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(型別)。它以 9.5. 繼承 (Inheritance)¶當然,如果沒有支援繼承,「class」這個語言特色就不值得被稱為 class。一個 derived class(衍生類別)定義的語法看起來如下: class DerivedClassName(BaseClassName): <statement-1> . . . <statement-N> 名稱 class DerivedClassName(modname.BaseClassName): 執行 derived class 定義的過程,與執行 base class 相同。當 class 物件被建構時,base class 會被記住。這是用於解析屬性參照:如果一個要求的屬性無法在該 class 中找到,則會繼續在 base class 中搜尋。假如該 base class 本身也是衍生自其他 class,則這個規則會遞迴地被應用。 關於 derived class 的實例化並沒有特別之處: Derived class 可以覆寫其 base class 的 method。因為 method 在呼叫同一個物件的其他 method 時沒有特別的特權,所以當 base class 的一個 method 在呼叫相同 base class 中定義的另一個 method 時,最終可能會呼叫到一個覆寫它的 derived class 中的 method。(給 C++ 程式設計師:Python 中所有 method 實際上都是 一個在 derived class 覆寫的 method 可能事實上是想要擴充而非單純取代 base class 中相同名稱的 method。要直接呼叫 base class 的 method 有一個簡單的方法:只要呼叫 Python 有兩個內建函式可以用於繼承:
9.5.1. 多重繼承¶Python 也支援多重繼承的形式。一個有多個 base class 的 class definition 看起來像這樣子: class DerivedClassName(Base1, Base2, Base3): <statement-1> . . . <statement-N> 在大多數情況下,最簡單的例子裡,你可以這樣思考,對於繼承自 parent class(父類別)的屬性,其搜尋規則為:深度優先、從左到右、在階層裡重疊的相同 class 中不重複搜尋。因此,假如有一個屬性在 事實上,它稍微複雜一些;method 的解析順序是動態地變化,以支援對 動態排序是必要的,因為多重繼承的所有情況都表現一或多的菱形關係(其中至少一個 parent class 可以從最底層 class 透過多個路徑存取)。例如,所有的 class 都繼承自 In some cases multiple inheritance is not allowed; see Multiple inheritance for details. 9.6. 私有變數¶「私有」(private) 實例變數,指的是不在物件內部便無法存取的變數,這在 Python 中是不存在的。但是,大多數 Python 的程式碼都遵守一個慣例:前綴為一個底線的名稱(如: 既然 class 私有的成員已有一個有效的用例(即避免名稱與 subclass 定義的名稱衝突),這種機制也存在另一個有限的支援,稱為 name mangling(名稱修飾)。任何格式為 也參考 參閱私有名稱修飾規格的詳情與特殊情況。 名稱修飾對於讓 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) 在上例中,就算在 請注意,修飾規則是被設計來避免意外;它仍可能存取或修改一個被視為私有的變數。這在特殊情況下甚至可能很有用,例如在除錯器 (debugger)。 另外也注意,傳遞給 9.7. 補充說明¶如果有一種資料型別,類似於 Pascal 的「record」或 C 的「struct」,可以將一些有名稱的資料項目捆綁在一起,有時候這會很有用。符合語言習慣的做法是使用 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。例如,如果你有一個函式,它會從一個檔案物件來格式化某些資料,你也可以定義一個有 實例的 method 物件也具有屬性: 9.8. 疊代器 (Iterator)¶到目前為止,你可能已經注意到大多數的容器 (container) 物件都可以使用 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 中處處可見且用法一致。在幕後, >>> 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 加入疊代器的行為就很容易了。定義一個 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) ... m a p s 9.9. 產生器 (Generator)¶產生器是一個用於建立疊代器的簡單而強大的工具。它們的寫法和常規的函式一樣,但當它們要回傳資料時,會使用 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 為基礎的疊代器來完成,如同前一節的描述。而讓產生器的程式碼更為精簡的原因是, 另一個關鍵的特性在於,區域變數和執行狀態會在每次呼叫之間自動被儲存。這使得該函式比使用 除了會自動建立 method 和儲存程式狀態,當產生器終止時,它們還會自動引發 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'] >>> x = [[1,2,3], [], [4, 5]] >>> g = (*i for i in x) >>> list(g) [1, 2, 3, 4, 5] In most cases, generator expressions must be wrapped in parentheses. As a
special case, however, when provided as the sole argument to a function (as in
the examples involving >>> f(x for x in y) >>> f((x for x in y)) as are the following: >>> f(*x for x in y) >>> f((*x for x in y)) 註解 |
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |