Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ......

32
ix 前言 Python 程式語言調和了許多顯然的矛盾:優雅又實務、簡單且強大,非常 高階、但在你需要操作位元或位元組時也不會形成阻礙,它既適合程式新 手,也能成為專家的好工具。 這本書的目標讀者是之前有過一些 Python 經驗的程式設計師,以及來自其 他程式語言第一次使用 Python 的程式設計老手。本書是 Python 的快速參 考手冊,內容包括 Python 本身、其廣大的程式庫中最常被使用的部分,以 及一些最熱門且實用的第三方模組和套件,涵蓋了廣泛的應用領域,像是 Web 與網路程式設計、XML 處理、資料庫互動,以及高速數值計算。本書 著重於 Python 的跨平台能力,包含擴充 Python 和將之內嵌在其他應用程 式中的基礎知識。 本書的組織方式 本書有五個部分,分述如下。 第一部 Python 入門 1 Python 簡介 涵蓋 Python 語言的一般特徵、其實作、何處可尋求協助與相關資訊、 如何參與 Python 社群,以及如何取得 Python 並在你的電腦上安裝。

Transcript of Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ......

Page 1: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

ix

前言

Python程式語言調和了許多顯然的矛盾:優雅又實務、簡單且強大,非常高階、但在你需要操作位元或位元組時也不會形成阻礙,它既適合程式新

手,也能成為專家的好工具。

這本書的目標讀者是之前有過一些 Python經驗的程式設計師,以及來自其他程式語言第一次使用 Python的程式設計老手。本書是 Python的快速參考手冊,內容包括 Python本身、其廣大的程式庫中最常被使用的部分,以及一些最熱門且實用的第三方模組和套件,涵蓋了廣泛的應用領域,像是

Web與網路程式設計、XML處理、資料庫互動,以及高速數值計算。本書著重於 Python的跨平台能力,包含擴充 Python和將之內嵌在其他應用程式中的基礎知識。

本書的組織方式

本書有五個部分,分述如下。

第一部 Python入門第 1章,Python簡介

涵蓋 Python語言的一般特徵、其實作、何處可尋求協助與相關資訊、如何參與 Python社群,以及如何取得 Python並在你的電腦上安裝。

Page 2: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

x | 前言

第 2章,Python直譯器

涵蓋 Python的直譯器程式、其命令列選項,以及如何用它在互動式工作階段(interactive sessions)中執行 Python 程式。此章提及編輯Python程式所用的文字編輯器,以及用來檢查你 Python原始碼的輔助程式,還有一些功能完備的整合式開發環境,包括內附於標準 Python中的 IDLE。此章也涵蓋如何從命令列執行 Python程式。

第二部 核心 Python語言和內建功能第 3章,Python語言

涵蓋 Python的語法、內建資料型別、運算式、述句、流程控制,以及如何撰寫和呼叫函式。

第 4章,物件導向的 Python

涵蓋 Python中的物件導向程式設計。

第 5章,例外

涵蓋如何使用例外處理錯誤和特殊情況,以及記錄(logging)。

第 6章,模組

涵蓋如何在 Python中將程式碼包裹成模組和套件、如何定義及匯入模組,以及如何安裝第三方 Python套件。此章也涵蓋虛擬環境(virtual environments)的使用,以隔離專案的相依性。

第 7章,核心內建功能和標準程式庫模組

涵蓋內建的資料型別與函式,以及 Python標準程式庫中一些最基本的模組(大致來說,這些模組所提供的功能,在其他的一些程式語言

中,內建於語言本身。)。

第 8章,字串類的東西

涵蓋 Python的字串處理功能,包括 Unicode字串、位元組字串(byte strings),以及字串字面值(string literals)。

第 9章,正規表達式

涵蓋 Python對正規表達式(regular expressions)的支援。

Page 3: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

前言 | xi

第三部 Python程式庫和擴充模組第 10章,檔案與文字作業

涵蓋如何使用 Python標準程式庫的許多模組進行檔案或文字的處理,以及平台限定的擴充功能,用於更多樣的文字 I/O。此章也涵蓋了國際化(internationalization)和當地語系化(localization)的議題,以及定義 Python互動文字模式命令工作階段的特殊工作。

第 11章,續存和資料庫

涵蓋 Python的序列化(serialization)和續存(persistence)機制,以及 Python對 DBM資料庫和關聯式(基於 SQL的)資料庫的介面,特別是內附於 Python程式庫中便利的 SQLite。

第 12章,時間作業

涵蓋如何在 Python中處理時間與日期,使用標準程式庫和熱門的第三方擴充功能。

第 13章,執行的控制

幫助你達成 Python中進階的執行控制,包括執行動態產生的程式碼和記憶體回收的控制。此章也涵蓋一些 Python內部的型別,以及註冊要在程式終止時期執行的「清理(clean-up)」函式的特定議題。

第 14章,執行緒和行程

涵蓋 Python用於共時執行(concurrent execution)的功能,不管是透過在單一行程(process)中運行的多個執行緒(threads),或是在單一機器上運行的多個行程。此章也涵蓋如何存取行程的環境,以及如何

經由記憶體映射(memory-mapping)機制存取檔案。

第 15章,數值處理

展示 Python的數值計算功能,不管是標準程式庫模組所提供的,還是第三方擴充套件所提供的,特別是,此章涵蓋了十進位小數(decimal number)或分數(fractions)的使用方式,用以取代預設的二進位浮點數(floating-point numbers)。此章也涵蓋如何取得並使用偽隨機數(pseudorandom numbers)和真正的隨機數,以及如何快速地處理整個陣列(或矩陣)的數字。

Page 4: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

xii | 前言

第 16章,測試、除錯和最佳化

介紹能幫助你確保程式正確性(即你的程式會做它們應該做的事)、尋

找和修補程式中錯誤、以及提升程式效能的 Python工具和途徑。本章也涵蓋了「警告(warning)」的概念,以及用來處理它的 Python程式庫模組。

第四部 網路與Web程式設計第 17章,網路基礎

涵蓋 Python基本的網路功能。

第 18章,非同步的替代方案

涵蓋用於非同步(事件驅動)程式設計的替代方案,特別是現代

以 coroutine(協程)為基礎的架構,以及低階的多工(lower-level multiplexing),使用標準程式庫的模組。

第 19章,客戶端網路協定模組

涵蓋 Python標準程式庫中撰寫網路客戶端程式的模組,特別是用於客戶端處理各種網路協定、收發 emails,以及處理 URL的功能。

第 20章,HTTP網路服務

涵蓋如何在 Python中提供「web應用程式(web applications)」所需的 HTTP服務,使用熱門的第三方輕量化 Python框架,透過 Python的WSGI標準介面跟 web伺服器互動。

第 21章,Email、MIME與其他網路編碼

涵蓋 Python中 email訊息和其他具有網路結構且經過編碼的文件之處理。

第 22章,結構化文字:HTML

涵蓋用於處理、修改和產生 HTML文件的熱門第三方 Python擴充模組。

第 23章,結構化文字:XML

涵蓋用來處理、修改和產生 XML文件的 Python程式庫模組。

Page 5: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

前言 | xiii

第五部 擴充、發布、v2/v3移轉第 24章,擴充與內嵌標準型的 Python

涵蓋如何使用 Python的 C API和 Cython編寫 Python擴充模組。

第 25章,發布擴充功能與程式

涵蓋封裝並發布 Python擴充功能、模組和應用程式用的熱門工具與模組。

第 26章,v2/v3的移轉與共存

涵蓋幫助你撰寫同時適用 v2和 v3的程式碼、將現有程式碼從 v2移植到 v3,以及保持跨版本可移植性的模組和最佳實務做法。

本書所用之慣例

本書用到下列慣例。

參考慣例

在函式或方法的參考條目中,合適的話,每個選擇性參數(optional parameter)都會使用 Python語法 name=value顯示其預設值。內建函式不必接受具名參數(named parameters),所以參數名稱可能不是很重要。某些選擇性參數最佳的說明方式是它們的出現與否,而非透過預設值。在這

種情況下,我們會以方括號([])圍住它來指出該參數是選擇性的。當有多個引數都是選擇性的,這些方括號會放在彼此之中成為巢狀的結構。

版本慣例

本書涵蓋 Python 3.5,我們以 v3稱呼它,還有 Python 2.7,以 v2稱之。Python 3.6(在本書即將付印時發行)的重點提示則以 3.6稱之。如果你用的是 v2,本書所有的範例都是假設你已執行 from __future__ import print_function(參閱後面章節的 Imports from _ _future_ _注意事項)。

印刷體例

斜體字(Italic)

用於檔名、程式名稱、URL以及介紹新術語。

Page 6: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

119

4物件導向的 Python

Python是一個物件導向(object-oriented,OO)程式語言。跟某些其他的物件導向語言不同,Python並不會強迫你只能使用物件導向典範:它也以模組(modules)和函式(functions)支援程序式程式設計(procedural programming),所以你能夠為你程式的各個部分選擇最佳的典範(paradigm)。物件導向典範能幫助你將狀態(資料)和行為(程式碼)包成一組便利的功能性。當你想要使用涵蓋於本章中的某些 Python物件導向機制,例如繼承(inheritance)或特殊方法(special methods),它也能派

上用場。如果你不需要物件導向程式設計的功能,那麼基於模組和函式的

程序式典範,可能會更為簡單且合適。透過 Python,你能夠混合搭配這些典範。

今日的 Python具有跟多年前不同的物件模型(object model)。本章只會描述所謂的新式模型(new-style)或新物件模型(new object model),它較

為簡單、更規則、更強大,而且也是我們建議你永遠都選用的那一個。每

當我們談及類別(classes)或實體(instances),我們指的都是新式的類別或實體。然而,為了回溯相容性(backward compatibility),v2中預設的物件模型是傳統物件模型(legacy object model),也叫做古典(classic)

或舊式(old-style)物件模型,而 v3中只有新式物件模型。要找舊式模型的資訊,請參閱來自 2002年的一些古老的線上文件(https://docs.python.

org/release/2.1.3/ref/datamodel.html)。

Page 7: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

120 | 第 4章 物件導向的 Python

本章也在後面的「特殊方法」一節中涵蓋特殊方法(special methods);

在後面的「抽象基礎類別」中涵蓋叫做抽象基礎類別(abstract

base classes)的進階概念;在後面「裝飾器」一節中涵蓋裝飾器(decorators),並在後面的「元類別」中涵蓋元類別(metaclasses)。

類別與實體

如果你熟悉其他語言(例如 C++或 Java)的物件導向程式設計,你對類別(classes)與實體(instances)大概就有很好的直觀理解:一個類別(class)是使用者所定義的型別(user-defined type),你能夠將之實體化(instantiate)以建立實體(instances),也就是該型別的物件。Python透過它的類別和實體物件來支援這些概念。

Python類別一個類別(class)是帶有下列幾個特徵的一個 Python物件:

• 你可以把一個類別物件(class object)當成函式來呼叫。這個一般稱為實體化(instantiation)的呼叫,會回傳一個物件,它叫做該類別的一

個實體(instance),而那個類別則被稱為該實體的型別(type)。

• 一個類別有你能夠繫結(bind)和參考(reference)的任意命名的屬性(attributes)。

• 類別屬性的值可以是描述器(descriptors,包括函式),涵蓋於後面的

「描述器」中,也可以是一般的資料物件。

• 繫結至函式的類別屬性叫做該類別的方法(methods)。

• 一個方法可以有 Python所定義的帶有兩個前導底線(underscores)和兩個尾隨底線的特殊名稱(常被稱為 dunder names,也就是「double-underscore names」的簡稱,舉例來說,__init__這個名稱通常讀作「dunder init」)。當各種運算發生在一個類別的實體上,Python會隱含地呼叫這種特殊方法,如果該類別有提供的話。

• 一個類別可以繼承(inherit)其他類別,這代表它會將在該類別本身找

不到的屬性之查找(lookup)動作,委派(delegate)給其他類別物件。

Page 8: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 121

一個類別的實體是一個 Python物件,它帶有你能夠繫結和參考的屬性,而這些屬性有任意的名稱。一個實體物件會隱含地將在該實體中找不到的屬

性之查找動作委派給它的類別,而這個類別也可以將同樣的查找動作委派

給它繼承的類別(如果有的話)。

在 Python中,類別是物件(值),處理起來就跟其他物件一樣。因此,你可以在函式呼叫中傳入一個類別作為引數。同樣地,函式也能夠回傳一個

類別作為呼叫的結果。一個類別,就跟其他任何物件一樣,可被繫結為一

個變數(區域或全域)、容器中的一個項目,或是物件的一個屬性。類別

也可以是字典中的鍵值(keys)。「類別在 Python中是普通物件」的這個事實,經常也以「類別是一級(first-class)物件」這個說法來表示。

class述句class述句是創建一個類別物件最常見的方式。class是一個單子句的複合述句,其語法如下:

class classname(base-classes): statement(s)

classname是一個識別字。它是會在 class述句執行完成之後,被繫結(或重新繫結)至類別物件的一個變數。

base-classes是逗號分隔的一系列運算式,其值必須是類別物件。這些類別在不同的程式語言中有不同的名稱,你可以自行選擇要稱它們為

所創建的類別之基礎類別(bases)、超類別(superclasses)或父類別(parents)。創建出來的類別可以說是繼承(inherit)、引申(derive)

自、擴充(extend),或 subclass(衍生出子類別自)這些基礎類別,取

決於你熟悉的程式語言是什麼。在本書中,我們一般會用擴充(extend)。

這個類別也叫做其基礎類別的直接子類別(direct subclass)或後裔類

別(descendant)。僅限於 v3,base-classes中也可以包括一個具名引數metaclass=...,代表該類別的元類別(metaclass),如後面「Python v3如何決定一個類別的元類別」中所涵蓋的。

語法上,base-classes是選擇性的:要表示你所創建的類別沒有基礎類別,你可以省略 base-classes(以及,選擇性地,省略它周圍的括弧),將冒號直接放在 classname後面。然而,在 v2中,為了回溯相容性,沒有基礎類別的類別會是舊式的類別(除非你定義了 __metaclass__ 屬性,涵蓋於後面的「Python v2如何決定一個類別的元類別」)。要在 v2中建立沒有

物件導向的

Python

Page 9: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

122 | 第 4章 物件導向的 Python

任何「真正的」基礎類別的新式類別 C,就寫成 class C(object):,因為每個型別都擴充自內建的 object,指定 object作為 base-classes的值單純只代表類別 C是新式的,而非舊式的。如果你類別的祖輩類別(ancestors)全都是舊式的,而且它沒有定義 __metaclass__,那你的類別即為舊式的,否則的話,一個具有基礎類別的類別永遠都是新式的(即使它的基礎類別

有些是新式的,有些是舊式的)。在 v3中,所有的類別都是新式的(想要的話,你仍然可以「繼承自 object」來讓你的程式碼回溯相容於 v2,不會有什麼傷害)。在我們的範例中,為了一致,我們永遠都會指定 (object)作為基礎類別,而不會讓一個類別「沒有基礎(base-less)」,然而,如果你只為 v3設計程式,那麼省去這種「fake subclassing(假的衍生子類別動作)」會比較優雅,而且是推薦的做法。

類別之間的子類別關係是有遞移性(transitive)的:如果 C1擴充自 C2,而C2擴充自 C3,那麼 C1就擴充自 C3。內建函式 issubclass(C1, C2)接受兩個類別物件作為引數:如果 C1擴充自 C2,它就回傳 True,否則回傳 False。任何類別都是自身的一個子類別,因此,issubclass(C, C)對任何 C都會回傳 True。我們會在後面的「繼承」中涵蓋基礎類別如何影響一個類別的功能性。

接在 class述句後的非空述句序列叫做類別主體(class body)。類別主體

會在 class述句的執行過程中作為它的一部分立即被執行。直到主體執行完成之前,新的類別物件都不算存在,而 classname也尚未繫結(或重新繫結)。後面的「一個元類別如何建立一個類別」為 class述句執行時會發生什麼事情提供了更多細節。

最後,請注意,class述句並不會立即為新的類別建立任何實體,而是定義出一組屬性讓後續你呼叫該類別建立的所有實體共用。

類別主體

類別的主體(body)是一般你為該類別指定屬性(attributes)的地方,這些屬性可以是描述器物件(descriptor objects,包括函式)或任何型別的普通資料物件(類別的屬性也可以是另一個類別,因此,舉例來說,你可以

將一個 class述句內嵌在另一個 class述句中形成「巢狀」)。

Page 10: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 123

類別物件的屬性

你為一個類別物件指定一個屬性的方式通常是在它的類別主體中,將一個

值繫結至一個識別字,例如:

class C1(object): x = 23print(C1.x) # 印出:23

類別物件 C1有一個名為 x的屬性,繫結至值 23,而 C1.x指涉(refers to)該屬性。

你也可以在類別主體之外繫結類別屬性,或為它們解除繫結,例如:

class C2(object): passC2.x = 23print(C2.x) # 印出:23

如果你只以類別主體內的述句來繫結(因而創建)類別屬性,你的程式通

常會比較易讀。然而,如果你希望在類別層級攜帶狀態資訊,而非在實體

層級,那麼在其他地方重新繫結它們可能會是必要的。Python會讓你那樣做,只要你想要。在類別主體中創建的類別屬性與在主體之外經由對屬性

的指定(assigning)而創建或重新繫結的類別屬性之間,沒有任何差異。

如我們稍後會討論的,一個類別的所有實體都共用該類別全部的屬性。

class述句會隱含地設定某些類別屬性。屬性 __name__是 class述句中所用的 classname識別字字串。屬性 __bases__是在 class述句中給定作為基礎類別的類別物件所構成的元組(tuple)。以我們剛才所建立的類別 C1為例:

print(C1.__name__, C1.__bases__)# 印出:C1 (<type 'object'>,)

類別也會有一個 __dict__屬性,它是類別用來保存其他屬性的映射物件(mapping object,即它的命名空間,namespace),在類別中,這個映射

是唯讀的。

直接放在類別主體內的述句中,對類別屬性的參考必須使用簡單的名稱,

而非經過完整資格修飾的名稱(fully qualified name)。舉例來說:

class C3(object): x = 23 y = x + 22 # 必須使用單純的 x,而非 C3.x

物件導向的

Python

Page 11: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

124 | 第 4章 物件導向的 Python

然而,類別主體中定義的方法內的述句,要參考類別的屬性時,必須使用

經過完整資格修飾的名稱,而非簡單名稱,例如:

class C4(object): x = 23 def amethod(self): print(C4.x) # 必須使用 C4.x或 self.x,而非單純的 x!

請注意,屬性參考(attribute references,像是 C.s這樣的運算式)的語意比屬性繫結(attribute bindings)還要豐富。我們會在後面的「屬性參考基礎」中詳細涵蓋這種參考。

類別主體中的函式定義

大多數的類別主體都包括 def述句,因為對大部分的類別物件而言,函式(在這種情境下稱為方法)都是重要的屬性。類別主體中的 def述句遵守前面「函式」一節中所描述的規則。此外,定義在類別主體中的方法都有

一個必要的第一參數,依照慣例命名為 self,它指涉你在其上呼叫該方法的實體。self參數在方法呼叫中扮演特殊的角色,如後面「已繫結和未繫結的方法」中所涵蓋的。

這裡有包含了一個方法定義的範例類別:

class C5(object): def hello(self): print('Hello')

一個類別可以定義各種特殊方法(名稱帶有兩個前導底線以及兩個尾隨底

線的方法,它們偶爾也被稱為「魔術方法」,「magic methods」,口語上經常被稱作「dunder」方法,例如 __init__叫做「dunder init」),這些方法與該類別之實體上的特定運算有關。我們會在後面的「特殊方法」中討論

特殊方法。

類別的私有變數

當類別主體(或主體內的方法)中的述句使用以兩個底線開頭(但並非以

底線結尾)的識別字,例如 __ident,Python編譯器就會隱含地將那個識別字改成 _classname__ident,其中的 classname是類別的名稱。這讓一個類別將「私有的(private)」名稱用於屬性、方法、全域變數,或其他用途,減少意外重複使用他處用過的名稱之風險,特別是在子類別中。

Page 12: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 125

依照慣例,以單一個底線開頭的識別字是要當作繫結它們的範疇(scope)之私有名稱,不管這個範疇是不是一個類別。Python編譯器並不強制施加這個隱私慣例:是否要遵守它,由程式設計師自行決定。

類別說明文件字串

如果類別主體中的第一個述句是一個字串字面值(string literal),編譯器就會將那個字串繫結為該類別的說明文件字串(documentation string)。名為 __doc__的屬性,是類別的 docstring。說明文件字串的更多資訊,請參

閱前面的「Docstrings」。

描述器

一個描述器(descriptor)是其類別提供 __get__特殊方法的任何物件。作為類別屬性的描述器控制該類別的實體上屬性存取與設定的語意。粗略地

說,當你存取一個實體屬性(instance attribute),Python就會呼叫對應的描述器上的 __get__(如果有的話)來取得該屬性的值。舉例來說:

class Const(object): # 覆寫式描述器,參閱後面 def __init__(self, value): self.value = value def __set__(self, *_): # 忽略任何設定的嘗試 pass def __get__(self, *_): # 永遠都回傳這個常數值 return self.value

class X(object): c = Const(23)

x=X()print(x.c) # 印出:23x.c = 42print(x.c) # 印出:23

更多細節,請參閱後面的「屬性參考基礎」。

覆寫式和非覆寫式描述器

如果一個描述器的類別也提供名為 __set__的特殊方法,那麼這個描述器就叫做一個覆寫式的描述器(overriding descriptor,或是較舊的、更普遍

但有點令人困惑的名稱:資料描述器,data descriptor)。如果描述器的類

別只提供 __get__而沒有 __set__,那麼該描述器就叫做非覆寫式描述器(nonoverriding descriptor,或非資料描述器,nondata descriptor)。

物件導向的

Python

Page 13: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

126 | 第 4章 物件導向的 Python

舉例來說,函式物件的類別提供了 __get__,但不提供 __set__,因此,函式物件是非覆寫式描述器。大致來說,當你指定一個值給一個實體屬性,

而它對應的描述器是覆寫式的,Python就會呼叫那個描述器上的 __set__來設定該屬性的值。更多細節,請參閱後面的「實體物件的屬性」。

實體

要建立一個類別的實體(instance),就把它的類別物件當成一個函式來呼叫。每次呼叫都會回傳型別為該類別的一個新實體:

an_instance = C5()

你可用一個類別物件作為引數 C來呼叫內建函式 isinstance(i, C)。isinstance會在物件 i是類別 C或 C的任何子類別的實體時回傳 True。否則,isinstance就會回傳 False。

__init__

當一個類別定義或繼承名為 __init__的一個方法,呼叫該類別物件會隱含地在新的實體上執行 __init__以進行任何必要的實體初始化(initialization)動作。在呼叫中傳入的引數必須對應 __init__除了 self以外的參數。舉例來說,請考慮:

class C6(object): def __init__(self, n): self.x = n

這裡是你為 C6類別創建一個實體的方式:

another_instance = C6(42)

如 C6類別中所示,__init__方法通常會包含繫結實體屬性的述句。一個__init__方法必不能回傳 None以外的值,如果它這樣做,Python會提出一個 TypeError例外。

__init__的主要用途是繫結,也因此建立出,新創實體的屬性。你也可以在 __init__外部繫結、重新繫結實體屬性,或解除繫結,如你稍後會看到的。然而,如果你一開始就在 __init__方法中繫結一個類別實體的所有屬性,那麼你的程式碼會更有可讀性。

如果類別定義中沒有 __init__ (也沒繼承自任何基礎類別),你必須不帶引數呼叫該類別,而新實體不會有實體專屬的屬性。

Page 14: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 127

實體物件的屬性

一旦你建立了一個實體,你就能使用點號(.)存取它的屬性(資料和方法),例如:

an_instance.hello() # 印出:Helloprint(another_instance.x) # 印出:42

像這樣的屬性參考(attribute references)在 Python中有相當豐富的語意,我們會在後面的「屬性參考基礎」中涵蓋它們。

你可以將一個值繫結至一個屬性參考,藉此賦予實體物件一個任意的屬

性,例如:

class C7: passz = C7()z.x = 23print(z.x) # 印出:23

實體物件 z現在有一個名為 x的屬性,繫結至值 23,而 z.x指的就是那個屬性。請注意 __setattr__特殊方法,若有出現,會攔截繫結一個屬性的每個嘗試(我們會在表 4-1涵蓋 __setattr__)。當你試圖在一個實體上繫結其名稱對應到該類別中一個覆寫式描述器的屬性,該描述器的 __set__方法就會攔截那個嘗試:假若 C7.x是一個覆寫式描述器,那麼 z.x=23這個指定(assignment)就會執行 type(z).x.__set__(z, 23)。

創建一個實體會隱含地設定兩個實體屬性。對於任何實體 z,z.__class__就是 z所屬類別之類別物件,而 z.__dict__則是 z用來保存它的其他屬性的映射(mapping)。以我們剛才建立的實體 z為例:

print(z.__class__.__name__, z.__dict__) # 印出:C7 {'x':23}

你可以重新繫結這兩個屬性(但不可解除繫結),但很少會需要這樣做。

對於任何實體 z,任何物件 x,以及任何識別字 S(除了 __class__和 __dict__以外),z.S=x等同於 z.__dict__['S']=x(除非有一個 __setattr__特殊方法,或一個覆寫式描述器的 __set__特殊方法,攔截了繫結的嘗試)。再次以我們剛才建立的 z為例:

z.y = 45z.__dict__['z'] = 67print(z.x, z.y, z.z) # 印出:23 45 67

物件導向的

Python

Page 15: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

128 | 第 4章 物件導向的 Python

對屬性進行指定所建立的實體屬性與明確繫結 z.__dict__中一個條目(entry)而建立的屬性之間,沒有任何差異。

工廠函式慣用語(factory-function idiom)

我們通常會需要依據某些條件建立不同類別的實體,或在有既存的實體可

重複使用時,避免新實體的創建。有一個常見的誤解是,讓 __init__回傳一個特殊的物件,就能滿足這樣的需求,但這種做法是不可行的:如果 __init__回傳 None以外的任何值,Python就會提出一個例外。實作有彈性的物件創建方式最好的辦法是使用一個函式,而不直接呼叫類別物件。

以這種方式使用的函式叫做工廠函式(factory function)。

呼叫一個工廠函式是一種有彈性的做法:這個函式可以回傳一個現有的、

可重複使用的實體,或呼叫適當的類別來建立一個新實體。假設你有兩個

幾乎可互換的類別(SpecialCase和 NormalCase),而你想要依據一個引數有彈性地產生它們兩個中任一個的實體。下面作為一個「玩具」範例的

appropriate_case工廠函式,就能讓你達成此目的(我們會在後面的「已繫結和未繫結的方法」中涵蓋 self參數的角色):

class SpecialCase(object): def amethod(self): print('special')class NormalCase(object): def amethod(self): print('normal')def appropriate_case(isnormal=True): if isnormal: return NormalCase() else: return SpecialCase()aninstance = appropriate_case(isnormal=False)aninstance.amethod() # 印出:special

__new__

每個類別都有(或繼承了)一個名為 __new__的類別方法(我們在後面的「類別方法」中涵蓋類別方法)。當你呼叫 C(*args,**kwds)來創建類別 C的一個新實體,Python會先呼叫 C.__new__(C,*args,**kwds)。Python使用 __new__的回傳值 x作為新創建的實體。然後,Python會呼叫 C.__init__(x,*args,**kwds),不過這只在 x確實是 C或其子類別的實體時進行(否則,x的狀態仍然維持 __new__回傳時那樣)。因此,舉例來說,述句x=C(23)就等同於:

x = C.__new__(C, 23)if isinstance(x, C): type(x).__init__(x, 23)

Page 16: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 129

object.__new__接受一個類別作為第一引數,並創建該類別的一個新的、未初始化的實體。若是該類別有 __init__方法,它就會忽略其他的引數,但如果除了第一個之外,它還接收到到其他的引數,而作為第一引數

的那個類別沒有 __init__方法,它就會提出一個例外。當你在一個類別主體中覆寫(override)__new__方法,跟一般不同的是,你不需要加上 __new__=classmethod(__new__)或使用 @classmethod裝飾器(decorator):Python認得 __new__這個名稱,並會在這個情境中以特殊方式對待它。在非常罕見的情況下,你可能會在之後於類別 C的主體外部重新繫結 C.__new__,這時你就需要使用 C.__new__=classmethod(whatever)。

__new__有工廠函式所具備的大部分彈性,如前面「工廠函式慣用語」中所涵蓋的。__new__可以選擇回傳一個現有的實體,或製作一個新的,視情況而定。當 __new__確實需要建立一個新實體時,它通常會呼叫object.__new__或 C的其他超類別(superclass)的 __new__ 方法來將創建的動作委派出去。下列的範例顯示如何覆寫類別方法 __new__以實作一種Singleton(單件)設計模式:

class Singleton(object): _singletons = {} def __new__(cls, *args, **kwds): if cls not in cls._singletons: cls._singletons[cls] = super(Singleton, cls).__new__(cls) return cls._singletons[cls]

(我們會在後面的「合作式超類別方法呼叫」中涵蓋內建的 super。)

Singleton的任何子類別(沒有進一步覆寫 __new__的)都只會有一個實體。如果子類別有定義 __init__,它就得確定這個 __init__在單一且唯一的類別實體上被重複呼叫(每次有創建請求的時候)時,會是安全的,

這是因為 Singleton任何子類別上所定義的 __init__,都會在每次你實體化該子類別的時候,於那個為 Singleton的每個子類別而存在的唯一實體上重複地執行。

v3能讓你用簡單且更為清晰的方式編寫這個範例我們以在 v2及 v3中都能運作得一樣好的方式來編寫這個

範例。在 v3中,你能以更簡單且乾淨的方式來編寫它,不

用給 Singleton任何超類別,並可不帶引數呼叫 super。

物件導向的

Python

Page 17: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

130 | 第 4章 物件導向的 Python

屬性參考基礎

一個屬性參考(attribute reference)是 x.name這種形式的一個運算式,其中 x是任何運算式,而 name是叫做屬性名稱(attribute name)的一個識別

字。許多種類的 Python物件都有屬性,但當 x指涉的是一個類別或實體,屬性參考就會有特別豐富的語意。請記得,方法(methods)也是屬性,所以我們對於屬性的所有一般性介紹,也都適用於可呼叫(callable)的屬性(即方法)。

假設 x是類別 C的一個實體,而後者繼承基礎類別 B。這兩個類別和那個實體都有數個屬性(資料與方法),如下:

class B(object): a = 23 b = 45 def f(self): print('method f in class B') def g(self): print('method g in class B')class C(B): b = 67 c = 89 d = 123 def g(self): print('method g in class C') def h(self): print('method h in class C')x = C()x.d = 77x.e = 88

名稱前後帶有雙底線(dunder-names)的幾個屬性是特殊的。C.__name__是字串 'C',即該類別的名稱。C.__bases__是由 C的基礎類別所構成的一個元組 (B,)。x.__class__是類別 C,即 x所屬的類別。當你使用這些特殊名稱之一指涉一個屬性時,這個屬性參考會直接查看該類別或實體物件中

一個專用的插槽(slot),並取回在那裡找到的值。你無法為這些屬性解除繫結(unbind)。你可以動態地重新繫結(rebind)它們,以變更一個類別的名稱或基礎類別,或是變更一個實體的類別,但這些進階技巧很少會是

必要的。

類別 C和實體 x都有另外一個特殊屬性:一個名為 __dict__的映射(mapping)。一個類別或實體的其他所有屬性,除了少數幾個特殊的以外,都會作為項目(items)保存在該類別或實體的 __dict__屬性中。

Page 18: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 131

從一個類別取得一個屬性

當你使用 C.name這個語法來指涉類別物件 C上的一個屬性,查找(lookup)動作會以兩個步驟進行:

1. 如果 'name'是 C.__dict__中的一個鍵值,C.name會從 C.__dict__['name']擷取其值 v。然後,若 v是一個描述器(descriptor,即type(v)提供一個名為 __get__的方法),那麼 C.name的值就會是呼叫type(v).__get__(v, None, C)的結果。假使 v不是一個描述器,C.name的值就會是 v。

2. 當 'name' 不是 C.__dict__中的一個鍵值,C.name會將查找動作委派給 C的基礎類別(base classes),這表示它會以迴圈跑過 C的祖輩類別(ancestor classes),試著在每一個上查找 name(以方法解析順序,method resolution order進行,如後面「方法解析順序」中所涵蓋的)。

從一個實體取得一個屬性

當你使用語法 x.name來指涉類別 C的實體 x的一個屬性,查找動作會以三個步驟進行:

1. 如果在 C中找到作為覆寫式描述器 v(也就是 type(v)提供方法 __get__和 __set__)名稱的 'name'

• x.name的值就會是 type(v).__get__(v, x, C)的結果

2. 否則的話,如果 'name'是 x.__dict__中的一個鍵值

• x.name就會擷取並回傳位於 x.__dict__['name']中的值

3. 再不然,x.name就會將查找動作委派給 x的類別(以剛才詳細描述過,跟 C.name所用相同的雙步驟進行查找)

• 若找到一個描述器 v,屬性查找動作的整體結果就會是跟前面一樣的 type(v).__get__(v, x, C)

• 若找到的是非描述器值 v,那麼屬性查找動作的整體結果就單純是v。

如果這些查找步驟找不到屬性,Python 就會提出一個 AttributeError例外。然而,查找 x.name的時候,若是 C定義或繼承了特殊方法 __getattr__,Python就會呼叫 C.__getattr__(x,'name'),而非提出例外。

物件導向的

Python

Page 19: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

132 | 第 4章 物件導向的 Python

然後就由 __getattr__決定要回傳一個合適的值,或是提出適當的例外,一般來說會是 AttributeError。

請考慮下列的屬性參考,如前面定義過的:

print(x.e, x.d, x.c, x.b, x.a) # 印出:88 77 89 67 23

x.e和 x.d在實體查找程序的第 2個步驟中成功找到值,既然沒有涉及任何描述器,'e'和 'd'都只是 x.__dict__中的鍵值。因此,查找程序不會繼續,而是回傳 88和 77。另外三個參考必須進行到實體查找程序的步驟 3,並在 x.__class__(即 C)中尋找。x.c和 x.b在類別查找程序的步驟 1中成功找到值,因為 'c'和 'b'都是 C.__dict__中的鍵值。因此,查找程序不會繼續,而是回傳 89和 67。x.a要一直進行到類別查找程序的步驟 2,在 C.__bases__[0](即 B)中尋找。'a'是 B.__dict__中的一個鍵值,所以 x.a終於成功了,並且回傳 23。

設定一個屬性

請注意,剛才所描述的屬性查找步驟只發生在你指涉(refer to)一個屬性

的時候,而非在你繫結(bind)一個屬性的時候。當你(在一個類別或一

個實體上)繫結其名稱並非特殊的一個屬性時(除非有一個 __setattr__方法,或某個覆寫式描述器的 __set__方法,攔截了該實體屬性的繫結動作),你只會影響到(類別或實體中)該屬性的 __dict__條目(entry)。換句話說,屬性的繫結,除了檢查是否有覆寫式描述器以外,並不涉及查

找程序。

已繫結和未繫結的方法

一個函式物件的 __get__方法可以回傳一個未繫結的方法物件(unbound

method object,在 v2中),或該函式物件本身(在 v3中),或是包裹該函式的一個已繫結的方法物件(bound method object)。未繫結和已繫結方法

之間的關鍵差異在於,一個未繫結的方法(僅限 v2)並沒有與特定的實體有關聯,而一個已繫結方法則有。

在前一節的程式碼中,屬性 f、g與 h都是函式,因此,對它們之中任一個的屬性參考都會回傳包裹了對應函式的一個方法物件。請考慮下列 程式碼:

print(x.h, x.g, x.f, C.h, C.g, C.f)

Page 20: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 133

這個述句會輸出三個已繫結的方法,以這樣的字串表示:

<bound method C.h of <__main__.C object at 0x8156d5c>>

然後,在 v2中,三個未繫結的方法,以這樣的字串表示:

<unbound method C.h>

或者,在 v3中,三個函式物件,以這樣的字串表示:

<function C.h at 0x102cabae8>

已繫結方法 vs.未繫結方法當屬性參考是在實體 x上,我們會得到已繫結的方法。如

果屬性參考是在類別 C上,我們會得到未繫結的方法(在

v3中則是函式物件)。

因為一個已繫結方法已經關聯至一個特定的實體,你可以像這樣來呼叫該

方法:

x.h() # 印出:method h in class C

這裡要注意的重點是,你不會以一般的引數傳遞語法傳入方法的第一個引

數 self。取而代之,實體 x的一個已繫結方法會隱含地將 self參數繫結至物件 x。因此,雖然我們沒有傳入一個明確的引數給方法,方法的主體還是能夠把該實體的屬性當成 self的屬性來存取。

然而,一個未繫結的方法(僅限 v2),並沒有與特定的實體關聯,所以呼叫一個未繫結的方法時,你必須指定一個適當的實體作為第一個引數, 例如:

C.h(x) # 印出:method h in class C

你呼叫未繫結方法的機會比已繫結方法少很多。未繫結方法的一個重要用

途是取用已被覆寫的方法(overridden methods),如後面「繼承」中所討論的。不過即使是這個用途,通常最好還是使用內建的 super,涵蓋於後面的「合作式超類別方法呼叫」中。另一個常會用到未繫結方法的地方是在

高階函式(higher-order functions)中,舉例來說,要以字母順序但不在意大小寫的方式排序(sort)由字串組成的一個串列(list of strings),只要使用 los.sort(key=str.lower)就行了。

物件導向的

Python

Page 21: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

134 | 第 4章 物件導向的 Python

未繫結方法的詳細介紹(僅限 v2)

如我們剛才討論過的,當類別上的一個屬性參考指的是一個函式,那麼對

該屬性的參考,在 v2中,就會回傳包裹了該函式的一個未繫結方法物件。除了它所包裹的函式物件之屬性,一個未繫結的方法還有另外三個屬性:

im_class是提供該方法的類別物件,im_func是被包裹的那個函式,而 im_self則永遠是 None。這些屬性全都是唯讀的,代表試著重新繫結它們或解除它們的繫結都會提出一個例外。

呼叫一個未繫結方法的方式就跟呼叫它的 im_func函式一樣,不過這種呼叫中的第一個引數必須是 im_class或其子裔類別的一個實體。換句話說,對未繫結方法的呼叫至少必須有一個引數,它對應到所包裹函式的第一個

形式參數(依照慣例叫做 self)。

已繫結方法的詳細介紹

當一個實體上的一個屬性參考,在查找的過程中,找到了作為該實體之類

別中某個屬性的一個函式物件,查找程序就會呼叫該函式的 __get__方法來取得該屬性的值。在這種情況下,此呼叫會創建並回傳包裹了該函式的

一個已繫結方法。

請注意,當屬性參考的查找動作在 x.__dict__中找到一個函式物件,那個屬性參考運算並不會創建一個已繫結的方法:在這種情況下,Python不會把那個函式當成一個描述器(descriptor),不會呼叫該函式的 __get__方法,而是那個函式物件本身即為屬性的值。同樣地,Python不會為不是正規函式的 callable建立已繫結方法,例如內建的(相對於以 Python編寫的)函式,因為這種 callables並不是描述器。

已繫結方法與未繫結方法的類似之處在於,除了它包裹的函式所具備的屬

性外,它還有三個唯讀屬性。就跟在未繫結方法中一樣,im_class是提供該方法的類別物件,而 im_func是被包裹的函式。然而,在一個已繫結方法中,屬性 im_self指的會是 x,也就是你從它那取得該方法的實體。

使用一個已繫結方法的方式就跟它的 im_func函式一樣,不過對已繫結方法的呼叫並不會明確提供一個引數給對應的那個第一形式參數(慣例上名

為 self)。當你呼叫一個已繫結方法,那個方法就會把 im_self放在呼叫時給定的其他引數(如果有的話)之前,作為第一個引數傳入給 im_func。

Page 22: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 135

讓我們以詳細到有點痛苦的方式來追蹤透過常規語法 x.name(arg)進行的方法呼叫中所涉及的概念步驟。在下列的情境中:

def f(a, b): ... # 帶有兩個引數的函式 f

class C(object): name = fx = C()

x是類別 C的一個實體物件,name這個識別字是 x的一個方法(C的一個屬性,其值為一個函式,在此就是函式 f)之名稱,而 arg是任何的運算式。Python首先會檢查 'name'是否是 C中作為屬性的一個覆寫式描述器的名稱,不過它並不是,函式是描述器沒錯,因為它們的型別定義有方法

__get__,但並非覆寫式的,因為它們的型別沒有定義方法 __set__。接著Python會檢查 'name'是否為 x.__dict__中的一個鍵值,但它不是。所以Python會在 C中找尋 name(如果 name透過繼承關係在 C的 __bases__所含的任一個基礎類別中被找到,所有的事情都還是會以同樣的方式運作)。

Python注意到該屬性的值,即函式物件 f,是一個描述器。因此,Python呼叫 f.__get__(x, C),這會創建一個已繫結方法物件,其 im_func被設為f,im_class被設為 C,而 im_self被設為 x。然後 Python以 arg作為唯一引數來呼叫這個已繫結方法。已繫結方法會將 im_self(即 x)插入作為第一個引數,而 arg就變為第二個引數,以此呼叫已繫結方法的 im_func(即函式 f)。這整體的效果就像是呼叫:

x.__class__.__dict__['name'](x, arg)

一個已繫結方法的函式主體執行時,它與其 self物件或任何類別都沒有特殊的命名空間(namespace)關係。所參考的變數是區域性或全域性的,就跟其他的任何函式一樣,如前面「命名空間」中所涵蓋的。變數並不

隱含地代表 self中的屬性,也不代表任何類別物件中的屬性。當方法需要指涉、繫結其 self物件的一個屬性,或為之解除繫結時,它必須透過標準的屬性參考語法(例如 self.name)來進行。這種隱含範疇(implicit scoping)的缺乏可能需要一點時間適應(單純因為 Python在這個方面與其他許多物件導向語言不同),但它的結果是清晰度、簡單性,並且移除

了可能的歧義。

已繫結方法物件是一級物件(first-class objects):你可以把它們用在任何可使用 callable物件的地方。因為一個已繫結物件持有它所包裹的物件之參考,以及它在其上執行的 self物件之參考,它可以是一個 closure(涵蓋於前面的「巢狀函式和巢狀範疇」中)強大且有彈性的替代品。其類別提

物件導向的

Python

Page 23: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

136 | 第 4章 物件導向的 Python

供特殊方法 __call__(涵蓋於表 4-1)的實體物件則是另一種可行的替代品。這幾種構造(constructs)都能讓你把某些行為(程式碼)和某些狀態(資料)包成單一個 callable物件。Closures(閉包)是最簡單的,但適用範圍最有限。這裡有來自前面「巢狀函式和巢狀範疇」的 closure範例:

def make_adder_as_closure(augend): def add(addend, _augend=augend): return addend+_augend return add

已繫結方法(bound methods)和可呼叫的實體(callable instances)比closures更強大且更有彈性。這裡示範如何以一個已繫結方法實作相同的功能性:

def make_adder_as_bound_method(augend): class Adder(object): def __init__(self, augend): self.augend = augend def add(self, addend): return addend+self.augend return Adder(augend).add

而這裡是以一個可呼叫實體(其類別提供特殊方法 __call__的一個實體)實作的方式:

def make_adder_as_callable_instance(augend): class Adder(object): def __init__(self, augend): self.augend = augend def __call__(self, addend): return addend+self.augend return Adder(augend)

從呼叫這些函式的程式碼的觀點來看,所有的這些工廠函式都是可互換

的,因為它們全都會回傳多型的(polymorphic,也就是能以相同方式使用的)callable物件。就實作來說,closure是最簡單的,已繫結方法和可呼叫實體使用更有彈性、更通用且強大的機制,但這種簡單的例子不需要那

些額外的能力。

繼承

當你在一個類別物件 C上使用屬性參考 C.name,而 'name'不是 C.__dict__中的一個鍵值,查找程序就會隱含地推進到 C.__bases__中的每個類別物件,並以特定順序(因為歷史因素,這個順序被叫做 method resolution

order,或 MRO,但它適用於所有屬性,不只是方法)處理它們。C的基礎類別也可能會有它們自己的基礎類別。查找程序會檢查直接和間接的祖輩

類別,以 MRO順序逐個檢查,並在找到 'name'時停止。

Page 24: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 137

方法解析順序(Method Resolution Order)

類別中屬性名稱的查找動作基本上就是以從左到右、深度優先(left-to-right, depth-first)的順序訪問祖輩類別(ancestor classes)。然而,在多重繼承(multiple inheritance,這會使 inheritance graph變成一般的 Directed Acyclic Graph,而非樹狀結構)的情況下,這種簡單的做法可能會導致某些祖輩類別被訪問兩次。若是如此,遇到同樣的類別時,這種解析順序只

會在查找序列(lookup sequence)中留下最右邊(rightmost)的那一個。

每個類別和內建型別都有一個特殊且唯讀的類別屬性,叫做 __mro__,它是用於方法解析(method resolution)的那些型別依序列出所成的一個元組。你只能在類別上取用 __mro__,不能在實體上使用,也因為 __mro__是唯讀的屬性,你也無法為它重新繫結或解除繫結。想要找 Python MRO所有面向詳細且高度技術性的解說,你可能會想要研讀 Michele Simionato所 著 的 線 上 論 文 The Python 2.3 Method Resolution Order(https://www.

python.org/download/releases/2.3/mro/), 以 及 GvR 在 Python History 網 站(http://python-history.blogspot.tw/2010/06/method-resolution-order.html)上的

歷史記錄。

覆寫屬性

如我們剛才所見,屬性的搜尋動作會依照 MRO(通常是在繼承樹狀結構中往上找)進行,並且一找到屬性就會停止。子裔類別(descendant classes)永遠都會在它們的祖輩之前先被檢視,當一個子類別(subclass)定義了與它超類別(superclass)中屬性同名的屬性,搜尋動作就會找到子類別中的定義,並停在那裡。這被稱為子類別覆寫(overriding)超類別中

的定義。請考慮下列程式碼:

class B(object): a = 23 b = 45 def f(self): print('method f in class B') def g(self): print('method g in class B')class C(B): b = 67 c = 89 d = 123 def g(self): print('method g in class C') def h(self): print('method h in class C')

物件導向的

Python

Page 25: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

138 | 第 4章 物件導向的 Python

在這段程式碼中,類別 C覆寫了其超類別 B的屬性 b和 g,不同於其他的某些語言,在 Python中你可以覆寫資料屬性(data attributes),就跟覆寫可呼叫的屬性(方法)一樣容易。

委派至超類別的方法

當一個子類別 C覆寫了其超類別 B的方法 f,C.f的主體通常會想要將它運算的某些部分委派(delegate)給它超類別的那個方法的實作。這有的時候可以藉由 v2中的未繫結方法(在 v3中,它會是一個函式物件,但運作起來都一樣)來辦到,如下:

class Base(object): def greet(self, name): print('Welcome', name)class Sub(Base): def greet(self, name): print('Well Met and', end=' ') Base.greet(self, name)x = Sub()x.greet('Alex')

在 Sub.greet主體中對超類別的委派,使用透過超類別上的屬性參考 Base.greet所取得的一個未繫結方法(在 v2中是如此,而在 v3中,相同的語法會取得一個函式物件,它的運作方式也相同),因此可正常傳入所有的引

數,包括 self。委派至超類別的實作是 v2中未繫結方法最常見的用途。

委派常用在特殊方法 __init__。Python創建一個實體的時候,基礎類別的__init__方法並不會自動被呼叫,跟其他某些物件導向語言不同。因此,適當的超類別初始化動作,是由子類別來進行,必要時就會使用委派。舉

例來說:

class Base(object): def __init__(self): self.anattribute = 23class Derived(Base): def __init__(self): Base.__init__(self) self.anotherattribute = 45

如果類別 Derived的 __init__方法沒有明確地呼叫類別 Base的相同方法,Derived的實體就會少了某部分的初始化動作,因此這種實體會沒有屬性anattribute。如果子類別沒有定義 __init__,這種問題就不會發生,因為在那種情況下,它會從它的超類別繼承那個方法。所以你永遠都沒必要這

樣寫:

Page 26: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 139

class Derived(Base): def __init__(self): Base.__init__(self)

永遠都不要寫出只會委派運算到超類別的方法

永遠都不要定義一個在語意上沒有其他作用的 _ _init_ _

(也就是只負責把運算委派給超類別),而是從超類別繼

承 __init__。這個建議適用於所有方法,不管特殊與否,但是,因為某些因素,編寫這種語意上空乏的方法的壞習

慣,最常出現在 __init__身上。

合作式超類別方法呼叫(Cooperative superclass method calling)

以未繫結方法語法呼叫一個方法的超類別版本,在具有鑽石型繼承圖

(diamond-shaped graphs)的多重繼承的情況下,相當容易出問題。請考慮下列定義:

class A(object): def met(self): print('A.met')class B(A): def met(self): print('B.met') A.met(self)class C(A): def met(self): print('C.met') A.met(self)class D(B,C): def met(self): print('D.met') B.met(self) C.met(self)

在這段程式碼中,當我們呼叫 D().met(),A.met最後會被呼叫兩次。我們要如何確保每個祖輩類別的方法實作都只被呼叫一次呢?解法是使用內建

的型別 super。在 v2中,我們得呼叫 super(aclass, obj),這會回傳物件obj的一個特殊的超物件(superobject)。當我們在這個超物件上查找一個屬性,查找動作會從 obj的 MRO中的類別 aclass 之後開始進行。因此,在 v2中,我們可以像這樣改寫前面的程式碼:

物件導向的

Python

Page 27: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

140 | 第 4章 物件導向的 Python

class A(object): def met(self): print('A.met')class B(A): def met(self): print('B.met') super(B,self).met()class C(A): def met(self): print('C.met') super(C,self).met()class D(B,C): def met(self): print('D.met') super(D,self).met()

在 v3中,雖然 v2的語法仍然可行(因此前面的程式碼片段可以順利運作),但 super的語意有經過強化,所以你可以將上面的每個 super呼叫改成不帶引數的 super()。

現在,D().met()只會呼叫每個類別的 met方法剛好一次。如果你養成永遠都以 super編寫超類別呼叫的習慣,那麼即使在複雜的繼承結構之下,你的類別也能合作無礙。就算最後的繼承結構很簡單,也不會有什麼不良的

影響。

只有在各類別的同一個方法之特徵式(signatures)都不一樣且不相容的時候,你才可能選用以未繫結方法語法來呼叫超類別方法的這種較粗略的做

法。這在許多方面都是令人不悅的情況,但如果你真的必須應付它,那麼

未繫結方法語法有的時候可能是最不險惡的了。多重繼承的正確使用本來

就阻礙重重,但即使是 OOP最基本的特性,例如基礎類別和子類別實體之間的多型(polymorphism),都會在你賦予超類別和子類別中同名方法不同且不相容的特徵式時,受到嚴重妨礙。

「刪除」類別屬性

繼承和覆寫提供了一種簡單且有效的方式來非侵入式地(noninvasively,代表不用修改定義那些屬性的基礎類別)新增或修改(覆寫)類別屬性

(例如方法):只要在子類別中新增或覆寫屬性即可。然而,繼承並沒有提

供非侵入式刪除(隱藏)基礎類別之屬性的方式。如果子類別沒定義(覆

寫)一個屬性,Python就會尋找基礎類別的定義。如果你需要進行這種刪除動作,可能性包括了:

Page 28: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 141

• 覆寫該方法,並在方法主體中提出一個例外。

• 避開繼承,將屬性保存在子類別的 __dict__以外的地方,並定義 __getattr__來進行選擇式的委派(selective delegation)。

• 覆寫 __getattribute__以取得類似效果。

最後一個技巧涵蓋於後面的「_ _getattribute_ _」。

內建的 object型別內建的 object型別是所有內建型別(built-in types)和新式類別(new-style classes)的祖先。object型別定義了一些特殊方法(記載於後面的「特殊方法」中)來實作物件(objects)的預設語意:

__new__ __init__

你可以不帶任何引數呼叫 object()來建立 object的一個直接實體(direct instance)。 這 個 呼 叫 會 隱 含 地 使 用 object.__new__和object.__init__來製作與回傳沒有屬性的一個實體物件(甚至沒有用來保存屬性的 __dict__)。這種實體物件可以當成「哨符(sentinels)」使用,與其他不同的物件比較起來保證不相等。

__delattr__ __getattribute__ __setattr__

預設情況下,任何物件都會使用 object的這些方法來處理屬性參考(如前面的「屬性參考基礎」中所涵蓋的)。

__hash__ __repr__ __str__

任何物件都可以被傳入給函式 hash和 repr,還有型別 str。

object的子類別可以覆寫這些方法或新增其他的。

類別層級的方法

Python 提供兩個內建的非覆寫式描述器型別(nonoverriding descriptor types),這能夠賦予類別兩種不同的「類別層級方法(class-level methods)」:靜態方法(static methods)和類別方法(class methods)。

靜態方法

一個靜態方法(static method)是你能在一個類別或該類別的任何實體上呼叫的一個方法,都沒有普通方法(不管是已繫結的或未繫結的)關於第一

物件導向的

Python

Page 29: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

142 | 第 4章 物件導向的 Python

個參數的特殊行為或限制。一個靜態方法可以有任何特徵式(signature),它可以沒有參數,而第一個參數(如果有的話),並不扮演特殊的角色。

你可以把一個靜態方法想成是你能夠正常呼叫的普通函式,儘管它剛好被

繫結至一個類別屬性。

雖然定義靜態方法永遠都不是必要的(你隨時都可以選擇改在類別之外定

義一個正常的函式),當一個函式的用途與某個特定類別有緊密的關聯,

有些程式設計師認為它們是優雅的替代語法。

要建立一個靜態方法,就呼叫內建型別 staticmethod,並將其結果繫結至一個類別屬性。就跟類別屬性的所有繫結一樣,這通常會在類別的主體

中進行,但你也可以選擇在其他地方進行。staticmethod的唯一引數是Python呼叫該靜態方法時要呼叫的函式。下列範例展示了定義與呼叫靜態方法的一種方式:

class AClass(object): def astatic(): print('a static method') astatic = staticmethod(astatic)an_instance = AClass()AClass.astatic() # 印出:a static methodan_instance.astatic() # 印出:a static method

這個範例將同樣的名稱用於傳入給 staticmethod的函式,以及繫結至staticmethod結果的屬性。這種命名方式並非強制性的,但它是個好主意,而我們推薦你永遠都用它。Python也提供了一種簡化過的特殊語法來支援這種風格,涵蓋於後面的「裝飾器」。

類別方法

一個類別方法(class method)是你能在一個類別或該類別任何實體上呼叫

的一個方法。Python會將這種方法的第一個參數繫結至你在其上呼叫該方法的類別。它並不會像一般的已繫結方法那樣將之繫結到實體。類別方法

的第一個參數依照慣例被命名為 cls。

雖然定義類別方法永遠都不是必要的(你隨時都可以選擇改在類別外定義

一個正常函式,接受類別物件作為它的第一個參數),但類別方法是那種

函式的優雅替代品(特別是因為必要時它們能在子類別中被覆寫)。

要建立一個類別方法,就呼叫內建型別 classmethod,並將它的結果繫結至一個類別屬性。就跟類別屬性的所有繫結一樣,這通常是在類別的主體中

進行,但你也可以選擇在別的地方進行。classmethod唯一的引數是 Python

Page 30: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 143

呼叫該類別方法時要呼叫的函式。這裡有你定義與呼叫一個類別方法的一

種方式:

class ABase(object): def aclassmet(cls): print('a class method for', cls.__name__) aclassmet = classmethod(aclassmet)class ADeriv(ABase): passb_instance = ABase()d_instance = ADeriv()ABase.aclassmet() # 印出:a class method for ABaseb_instance.aclassmet() # 印出:a class method for ABaseADeriv.aclassmet() # 印出:a class method for ADerivd_instance.aclassmet() # 印出:a class method for ADeriv

這個範例將同樣的名稱用於傳入給 classmethod的函式,以及繫結至classmethod結果的屬性。這種命名方式並非強制性的,但它是個好主意,而我們推薦你永遠都使用它。Python提供一種簡化過的特殊語法來支援這種風格,如後面「裝飾器」中所涵蓋的。

特性

Python提供一種內建的覆寫式描述器型別(overriding descriptor type),你可以用它來賦予特性(properties)給一個類別實體。

一個特性(property)是具有特殊功能性的一個實體屬性。你對這種屬性

的參考、繫結或解除繫結,都是使用一般的語法(例如 print(x.prop)、x.prop=23、del x.prop)。然而,它們的語意與一般的屬性參考、繫結和解除繫結不同,這些存取動作會在實體 x上呼叫你指定作為引數傳入給內建型別 property的方法。這裡有定義一個唯讀特性的一種方式:

class Rectangle(object): def __init__(self, width, height): self.width = width self.height = height def get_area(self): return self.width * self.height area = property(get_area, doc='area of the rectangle')

類別 Rectangle的每個實體 r都會有一個合成的唯讀屬性 r.area,它是在方法 r.get_area()中將邊長相乘動態地計算出來的。Rectangle.area.__doc__這個 docstring是 'area of the rectangle'。屬性 r.area是唯讀的(試圖對它繫結或解除繫結都會失敗),因為我們只在對 property的呼叫中指定了一個 get方法,而沒有 set或 del方法。

物件導向的

Python

Page 31: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

144 | 第 4章 物件導向的 Python

特性所進行的任務與特殊方法 __getattr__、__setattr__和 __delattr__(涵蓋於後面的「一般用途的特殊方法」中)類似,但更快速且較簡單。

要建立一個特性,就呼叫內建型別 property並將它的結果繫結至一個類別屬性。就跟類別屬性的所有繫結一樣,這一般是在類別的主體中進行,

但你也可以選擇在他處進行。在一個類別 C的主體中,你可以使用下列語法:

attrib = property(fget=None, fset=None, fdel=None, doc=None)

當 x是 C的一個實體,而你參考 x.attrib,Python就會在 x上呼叫你傳入作為引數 fget給特性建構器(property constructor)的方法,並不帶任何引數。當你指定 x.attrib = value,Python會呼叫你傳入作為引數 fset的方法,並以 value作為唯一引數。當你執行 del x.attrib,Python就會呼叫你傳入作為引數 fdel的方法,不帶任何引數。Python使用你傳入的 doc引數作為該屬性的 docstring。property的所有參數都是選擇性的。若有一個引數沒出現,那麼對應的運算就會被禁止(Python會在有程式碼試圖進行該運算時提出一個例外)。以 Rectangle為例,我們讓特性 area是唯讀的,因為我們只傳入一個引數給參數 fget,而沒有傳入引數給其他的參數fset和 fdel。

在類別中建立特性較為優雅的語法是將 property當成一個裝飾器(decorator,參閱後面的「裝飾器」)來用:

class Rectangle(object): def __init__(self, width, height): self.width = width self.height = height @property def area(self): '''area of the rectangle''' return self.width * self.height

要使用這個語法,你必須賦予取值器方法(getter method)你希望特性擁有的相同名稱,該方法的 docstring就會成為特性的 docstring。如果你也想要新增一個設值器(setter)或刪除器(deleter),就使用(在這個例子中)名為 area.setter和 area.deleter的裝飾器,並賦予它們所裝飾的方法與特性相同的名稱,例如:

class Rectangle(object): def __init__(self, width, height): self.width = width self.height = height

Page 32: Python - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/A492.pdf · 第一部Python 入門. ... 涵蓋如何使用Python 的C API 和Cython 編寫Python ...

類別與實體 | 145

@property def area(self): '''area of the rectangle''' return self.width * self.height @area.setter def area(self, value): scale = math.sqrt(value/self.area) self.width *= scale self.height *= scale

為何特性很重要

特性關鍵的重要性在於,它們的存在讓你能夠絕對安全地(並且確實是可

取的)對外提供公開的資料屬性作為你類別公開介面的一部分。在你的類

別或需要對它多型的其他類別之未來版本中,若有必要在屬性被參考、重

新繫結或解除繫結時執行某些程式碼,你就知道你可以將那個普通的屬性

變為一個特性,並且得到想要的效果,而不會對使用你類別的任何程式碼

(即「客戶端程式碼」,「client code」)有任何衝擊。這能讓你避開缺乏特性或等效機制的 OO語言中所需的古怪慣用語,例如存取器(accessor)和

變動器(mutator)方法。舉例來說,客戶端程式碼可以單純使用像這樣的

自然慣用語:

some_instance.widget_count += 1

而非像這樣被迫進到存取器和變動器扭曲的巢穴之中:

some_instance.set_widget_count(some_instance.get_widget_count() + 1)

如果你曾經試著編寫你會想要將它們取名為 get_this或 set_that這樣的方法,那麼就改為將它們包裹成特性,以讓你的程式碼更易讀。

特性與繼承

特性的繼承就跟其他任何的屬性一樣。然而,有一個小陷阱要留意:存

取一個特性時被呼叫的方法是在定義該特性本身的類別中所定義的那

些,而不會使用子類別中可能出現的覆寫版本。舉例來說:

class B(object): def f(self): return 23 g = property(f)class C(B): def f(self): return 42c = C()print(c.g) # 印出:23,而非 42

物件導向的

Python