9. 類?
類把數(shù)據(jù)與功能綁定在一起。創(chuàng)建新類就是創(chuàng)建新的對(duì)象 類型,從而創(chuàng)建該類型的新 實(shí)例 。類實(shí)例支持維持自身狀態(tài)的屬性,還支持(由類定義的)修改自身狀態(tài)的方法。
和其他編程語(yǔ)言相比,Python 的類只使用了很少的新語(yǔ)法和語(yǔ)義。Python 的類有點(diǎn)類似于 C++ 和 Modula-3 中類的結(jié)合體,而且支持面向?qū)ο缶幊蹋∣OP)的所有標(biāo)準(zhǔn)特性:類的繼承機(jī)制支持多個(gè)基類、派生的類能覆蓋基類的方法、類的方法能調(diào)用基類中的同名方法。對(duì)象可包含任意數(shù)量和類型的數(shù)據(jù)。和模塊一樣,類也支持 Python 動(dòng)態(tài)特性:在運(yùn)行時(shí)創(chuàng)建,創(chuàng)建后還可以修改。
如果用 C++ 術(shù)語(yǔ)來(lái)描述的話,類成員(包括數(shù)據(jù)成員)通常為 public (例外的情況見下文 私有變量),所有成員函數(shù)都是 virtual。與在 Modula-3 中一樣,沒有用于從對(duì)象的方法中引用對(duì)象成員的簡(jiǎn)寫形式:方法函數(shù)在聲明時(shí),有一個(gè)顯式的參數(shù)代表本對(duì)象,該參數(shù)由調(diào)用隱式提供。 與在 Smalltalk 中一樣,Python 的類也是對(duì)象,這為導(dǎo)入和重命名提供了語(yǔ)義支持。與 C++ 和 Modula-3 不同,Python 的內(nèi)置類型可以用作基類,供用戶擴(kuò)展。 此外,與 C++ 一樣,算術(shù)運(yùn)算符、下標(biāo)等具有特殊語(yǔ)法的內(nèi)置運(yùn)算符都可以為類實(shí)例而重新定義。
由于缺乏關(guān)于類的公認(rèn)術(shù)語(yǔ),本章中偶爾會(huì)使用 Smalltalk 和 C++ 的術(shù)語(yǔ)。本章還會(huì)使用 Modula-3 的術(shù)語(yǔ),Modula-3 的面向?qū)ο笳Z(yǔ)義比 C++ 更接近 Python,但估計(jì)聽說(shuō)過(guò)這門語(yǔ)言的讀者很少。
9.1. 名稱和對(duì)象?
對(duì)象之間相互獨(dú)立,多個(gè)名稱(在多個(gè)作用域內(nèi))可以綁定到同一個(gè)對(duì)象。 其他語(yǔ)言稱之為別名。Python 初學(xué)者通常不容易理解這個(gè)概念,處理數(shù)字、字符串、元組等不可變基本類型時(shí),可以不必理會(huì)。 但是,對(duì)涉及可變對(duì)象,如列表、字典等大多數(shù)其他類型的 Python 代碼的語(yǔ)義,別名可能會(huì)產(chǎn)生意料之外的效果。這樣做,通常是為了讓程序受益,因?yàn)閯e名在某些方面就像指針。例如,傳遞對(duì)象的代價(jià)很小,因?yàn)閷?shí)現(xiàn)只傳遞一個(gè)指針;如果函數(shù)修改了作為參數(shù)傳遞的對(duì)象,調(diào)用者就可以看到更改 --- 無(wú)需 Pascal 用兩個(gè)不同參數(shù)的傳遞機(jī)制。
9.2. Python 作用域和命名空間?
在介紹類前,首先要介紹 Python 的作用域規(guī)則。類定義對(duì)命名空間有一些巧妙的技巧,了解作用域和命名空間的工作機(jī)制有利于加強(qiáng)對(duì)類的理解。并且,即便對(duì)于高級(jí) Python 程序員,這方面的知識(shí)也很有用。
接下來(lái),我們先了解一些定義。
namespace (命名空間)是映射到對(duì)象的名稱?,F(xiàn)在,大多數(shù)命名空間都使用 Python 字典實(shí)現(xiàn),但除非涉及到優(yōu)化性能,我們一般不會(huì)關(guān)注這方面的事情,而且將來(lái)也可能會(huì)改變這種方式。命名空間的幾個(gè)常見示例: abs()
函數(shù)、內(nèi)置異常等的內(nèi)置函數(shù)集合;模塊中的全局名稱;函數(shù)調(diào)用中的局部名稱。對(duì)象的屬性集合也算是一種命名空間。關(guān)于命名空間的一個(gè)重要知識(shí)點(diǎn)是,不同命名空間中的名稱之間絕對(duì)沒有關(guān)系;例如,兩個(gè)不同的模塊都可以定義 maximize
函數(shù),且不會(huì)造成混淆。用戶使用函數(shù)時(shí)必須要在函數(shù)名前面附加上模塊名。
點(diǎn)號(hào)之后的名稱是 屬性。例如,表達(dá)式 z.real
中,real
是對(duì)象 z
的屬性。嚴(yán)格來(lái)說(shuō),對(duì)模塊中名稱的引用是屬性引用:表達(dá)式 modname.funcname
中,modname
是模塊對(duì)象,funcname
是模塊的屬性。模塊屬性和模塊中定義的全局名稱之間存在直接的映射:它們共享相同的命名空間! 1
屬性可以是只讀或者可寫的。如果可寫,則可對(duì)屬性賦值。模塊屬性是可寫時(shí),可以使用 modname.the_answer = 42
。del
語(yǔ)句可以刪除可寫屬性。例如, del modname.the_answer
會(huì)刪除 modname
對(duì)象中的 the_answer
屬性。
命名空間是在不同時(shí)刻創(chuàng)建的,且擁有不同的生命周期。內(nèi)置名稱的命名空間是在 Python 解釋器啟動(dòng)時(shí)創(chuàng)建的,永遠(yuǎn)不會(huì)被刪除。模塊的全局命名空間在讀取模塊定義時(shí)創(chuàng)建;通常,模塊的命名空間也會(huì)持續(xù)到解釋器退出。從腳本文件讀取或交互式讀取的,由解釋器頂層調(diào)用執(zhí)行的語(yǔ)句是 __main__
模塊調(diào)用的一部分,也擁有自己的全局命名空間。內(nèi)置名稱實(shí)際上也在模塊里,即 builtins
。
函數(shù)的本地命名空間在調(diào)用該函數(shù)時(shí)創(chuàng)建,并在函數(shù)返回或拋出不在函數(shù)內(nèi)部處理的錯(cuò)誤時(shí)被刪除。 (實(shí)際上,用“遺忘”來(lái)描述實(shí)際發(fā)生的情況會(huì)更好一些。) 當(dāng)然,每次遞歸調(diào)用都會(huì)有自己的本地命名空間。
作用域 是命名空間可直接訪問(wèn)的 Python 程序的文本區(qū)域。 “可直接訪問(wèn)” 的意思是,對(duì)名稱的非限定引用會(huì)在命名空間中查找名稱。
作用域雖然是靜態(tài)確定的,但會(huì)被動(dòng)態(tài)使用。執(zhí)行期間的任何時(shí)刻,都會(huì)有 3 或 4 個(gè)命名空間可被直接訪問(wèn)的嵌套作用域:
最內(nèi)層作用域,包含局部名稱,并首先在其中進(jìn)行搜索
封閉函數(shù)的作用域,包含非局部名稱和非全局名稱,從最近的封閉作用域開始搜索
倒數(shù)第二個(gè)作用域,包含當(dāng)前模塊的全局名稱
最外層的作用域,包含內(nèi)置名稱的命名空間,最后搜索
如果把名稱聲明為全局變量,則所有引用和賦值將直接指向包含該模塊的全局名稱的中間作用域。重新綁定在最內(nèi)層作用域以外找到的變量,使用 nonlocal
語(yǔ)句把該變量聲明為非局部變量。未聲明為非局部變量的變量是只讀的,(寫入只讀變量會(huì)在最內(nèi)層作用域中創(chuàng)建一個(gè) 新的 局部變量,而同名的外部變量保持不變。)
通常,當(dāng)前局部作用域?qū)ⅲò醋置嫖谋荆┮卯?dāng)前函數(shù)的局部名稱。在函數(shù)之外,局部作用域引用與全局作用域一致的命名空間:模塊的命名空間。 類定義在局部命名空間內(nèi)再放置另一個(gè)命名空間。
劃重點(diǎn),作用域是按字面文本確定的:模塊內(nèi)定義的函數(shù)的全局作用域就是該模塊的命名空間,無(wú)論該函數(shù)從什么地方或以什么別名被調(diào)用。另一方面,實(shí)際的名稱搜索是在運(yùn)行時(shí)動(dòng)態(tài)完成的。但是,Python 正在朝著“編譯時(shí)靜態(tài)名稱解析”的方向發(fā)展,因此不要過(guò)于依賴動(dòng)態(tài)名稱解析?。ň植孔兞恳呀?jīng)是被靜態(tài)確定了。)
Python 有一個(gè)特殊規(guī)定。如果不存在生效的 global
或 nonlocal
語(yǔ)句,則對(duì)名稱的賦值總是會(huì)進(jìn)入最內(nèi)層作用域。賦值不會(huì)復(fù)制數(shù)據(jù),只是將名稱綁定到對(duì)象。刪除也是如此:語(yǔ)句 del x
從局部作用域引用的命名空間中移除對(duì) x
的綁定。所有引入新名稱的操作都是使用局部作用域:尤其是 import
語(yǔ)句和函數(shù)定義會(huì)在局部作用域中綁定模塊或函數(shù)名稱。
global
語(yǔ)句用于表明特定變量在全局作用域里,并應(yīng)在全局作用域中重新綁定;nonlocal
語(yǔ)句表明特定變量在外層作用域中,并應(yīng)在外層作用域中重新綁定。
9.2.1. 作用域和命名空間示例?
下例演示了如何引用不同作用域和名稱空間,以及 global
和 nonlocal
對(duì)變量綁定的影響:
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
注意,局部 賦值(這是默認(rèn)狀態(tài))不會(huì)改變 scope_test 對(duì) spam 的綁定。 nonlocal
賦值會(huì)改變 scope_test 對(duì) spam 的綁定,而 global
賦值會(huì)改變模塊層級(jí)的綁定。
而且,global
賦值前沒有 spam 的綁定。
9.3. 初探類?
類引入了一點(diǎn)新語(yǔ)法,三種新的對(duì)象類型和一些新語(yǔ)義。
9.3.1. 類定義語(yǔ)法?
最簡(jiǎn)單的類定義形式如下:
class ClassName:
<statement-1>
.
.
.
<statement-N>
與函數(shù)定義 (def
語(yǔ)句) 一樣,類定義必須先執(zhí)行才能生效。把類定義放在 if
語(yǔ)句的分支里或函數(shù)內(nèi)部試試。
在實(shí)踐中,類定義內(nèi)的語(yǔ)句通常都是函數(shù)定義,但也可以是其他語(yǔ)句。這部分內(nèi)容稍后再討論。類里的函數(shù)定義一般是特殊的參數(shù)列表,這是由方法調(diào)用的約定規(guī)范所指明的 --- 同樣,稍后再解釋。
當(dāng)進(jìn)入類定義時(shí),將創(chuàng)建一個(gè)新的命名空間,并將其用作局部作用域 --- 因此,所有對(duì)局部變量的賦值都是在這個(gè)新命名空間之內(nèi)。 特別的,函數(shù)定義會(huì)綁定到這里的新函數(shù)名稱。
當(dāng)(從結(jié)尾處)正常離開類定義時(shí),將創(chuàng)建一個(gè) 類對(duì)象。 這基本上是一個(gè)包圍在類定義所創(chuàng)建命名空間內(nèi)容周圍的包裝器;我們將在下一節(jié)了解有關(guān)類對(duì)象的更多信息。 原始的(在進(jìn)入類定義之前起作用的)局部作用域?qū)⒅匦律?,類?duì)象將在這里被綁定到類定義頭所給出的類名稱 (在這個(gè)示例中為 ClassName
)。
9.3.2. Class 對(duì)象?
類對(duì)象支持兩種操作:屬性引用和實(shí)例化。
屬性引用 使用 Python 中所有屬性引用所使用的標(biāo)準(zhǔn)語(yǔ)法: obj.name
。 有效的屬性名稱是類對(duì)象被創(chuàng)建時(shí)存在于類命名空間中的所有名稱。 因此,如果類定義是這樣的:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那么 MyClass.i
和 MyClass.f
就是有效的屬性引用,將分別返回一個(gè)整數(shù)和一個(gè)函數(shù)對(duì)象。 類屬性也可以被賦值,因此可以通過(guò)賦值來(lái)更改 MyClass.i
的值。 __doc__
也是一個(gè)有效的屬性,將返回所屬類的文檔字符串: "A simple example class"
。
類的 實(shí)例化 使用函數(shù)表示法。 可以把類對(duì)象視為是返回該類的一個(gè)新實(shí)例的不帶參數(shù)的函數(shù)。 舉例來(lái)說(shuō)(假設(shè)使用上述的類):
x = MyClass()
創(chuàng)建類的新 實(shí)例 并將此對(duì)象分配給局部變量 x
。
實(shí)例化操作(“調(diào)用”類對(duì)象)會(huì)創(chuàng)建一個(gè)空對(duì)象。 許多類喜歡創(chuàng)建帶有特定初始狀態(tài)的自定義實(shí)例。 為此類定義可能包含一個(gè)名為 __init__()
的特殊方法,就像這樣:
def __init__(self):
self.data = []
當(dāng)一個(gè)類定義了 __init__()
方法時(shí),類的實(shí)例化操作會(huì)自動(dòng)為新創(chuàng)建的類實(shí)例發(fā)起調(diào)用 __init__()
。 因此在這個(gè)示例中,可以通過(guò)以下語(yǔ)句獲得一個(gè)經(jīng)初始化的新實(shí)例:
x = MyClass()
當(dāng)然,__init__()
方法還可以有額外參數(shù)以實(shí)現(xiàn)更高靈活性。 在這種情況下,提供給類實(shí)例化運(yùn)算符的參數(shù)將被傳遞給 __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. 實(shí)例對(duì)象?
現(xiàn)在我們能用實(shí)例對(duì)象做什么? 實(shí)例對(duì)象所能理解的唯一操作是屬性引用。 有兩種有效的屬性名稱:數(shù)據(jù)屬性和方法。
數(shù)據(jù)屬性 對(duì)應(yīng)于 Smalltalk 中的“實(shí)例變量”,以及 C++ 中的“數(shù)據(jù)成員”。 數(shù)據(jù)屬性不需要聲明;像局部變量一樣,它們將在第一次被賦值時(shí)產(chǎn)生。 例如,如果 x
是上面創(chuàng)建的 MyClass
的實(shí)例,則以下代碼段將打印數(shù)值 16
,且不保留任何追蹤信息:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
另一類實(shí)例屬性引用稱為 方法。 方法是“從屬于”對(duì)象的函數(shù)。 (在 Python 中,方法這個(gè)術(shù)語(yǔ)并不是類實(shí)例所特有的:其他對(duì)象也可以有方法。 例如,列表對(duì)象具有 append, insert, remove, sort 等方法。 然而,在以下討論中,我們使用方法一詞將專指類實(shí)例對(duì)象的方法,除非另外顯式地說(shuō)明。)
實(shí)例對(duì)象的有效方法名稱依賴于其所屬的類。 根據(jù)定義,一個(gè)類中所有是函數(shù)對(duì)象的屬性都是定義了其實(shí)例的相應(yīng)方法。 因此在我們的示例中,x.f
是有效的方法引用,因?yàn)?MyClass.f
是一個(gè)函數(shù),而 x.i
不是方法,因?yàn)?MyClass.i
不是函數(shù)。 但是 x.f
與 MyClass.f
并不是一回事 --- 它是一個(gè) 方法對(duì)象,不是函數(shù)對(duì)象。
9.3.4. 方法對(duì)象?
通常,方法在綁定后立即被調(diào)用:
x.f()
在 MyClass
示例中,這將返回字符串 'hello world'
。 但是,立即調(diào)用一個(gè)方法并不是必須的: x.f
是一個(gè)方法對(duì)象,它可以被保存起來(lái)以后再調(diào)用。 例如:
xf = x.f
while True:
print(xf())
將持續(xù)打印 hello world
,直到結(jié)束。
當(dāng)一個(gè)方法被調(diào)用時(shí)到底發(fā)生了什么? 你可能已經(jīng)注意到上面調(diào)用 x.f()
時(shí)并沒有帶參數(shù),雖然 f()
的函數(shù)定義指定了一個(gè)參數(shù)。 這個(gè)參數(shù)發(fā)生了什么事? 當(dāng)不帶參數(shù)地調(diào)用一個(gè)需要參數(shù)的函數(shù)時(shí) Python 肯定會(huì)引發(fā)異常 --- 即使參數(shù)實(shí)際未被使用...
實(shí)際上,你可能已經(jīng)猜到了答案:方法的特殊之處就在于實(shí)例對(duì)象會(huì)作為函數(shù)的第一個(gè)參數(shù)被傳入。 在我們的示例中,調(diào)用 x.f()
其實(shí)就相當(dāng)于 MyClass.f(x)
。 總之,調(diào)用一個(gè)具有 n 個(gè)參數(shù)的方法就相當(dāng)于調(diào)用再多一個(gè)參數(shù)的對(duì)應(yīng)函數(shù),這個(gè)參數(shù)值為方法所屬實(shí)例對(duì)象,位置在其他參數(shù)之前。
如果你仍然無(wú)法理解方法的運(yùn)作原理,那么查看實(shí)現(xiàn)細(xì)節(jié)可能會(huì)弄清楚問(wèn)題。 當(dāng)一個(gè)實(shí)例的非數(shù)據(jù)屬性被引用時(shí),將搜索實(shí)例所屬的類。 如果被引用的屬性名稱表示一個(gè)有效的類屬性中的函數(shù)對(duì)象,會(huì)通過(guò)打包(指向)查找到的實(shí)例對(duì)象和函數(shù)對(duì)象到一個(gè)抽象對(duì)象的方式來(lái)創(chuàng)建方法對(duì)象:這個(gè)抽象對(duì)象就是方法對(duì)象。 當(dāng)附帶參數(shù)列表調(diào)用方法對(duì)象時(shí),將基于實(shí)例對(duì)象和參數(shù)列表構(gòu)建一個(gè)新的參數(shù)列表,并使用這個(gè)新參數(shù)列表調(diào)用相應(yīng)的函數(shù)對(duì)象。
9.3.5. 類和實(shí)例變量?
一般來(lái)說(shuō),實(shí)例變量用于每個(gè)實(shí)例的唯一數(shù)據(jù),而類變量用于類的所有實(shí)例共享的屬性和方法:
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'
正如 名稱和對(duì)象 中已討論過(guò)的,共享數(shù)據(jù)可能在涉及 mutable 對(duì)象例如列表和字典的時(shí)候?qū)е铝钊梭@訝的結(jié)果。 例如以下代碼中的 tricks 列表不應(yīng)該被用作類變量,因?yàn)樗械?Dog 實(shí)例將只共享一個(gè)單獨(dú)的列表:
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']
正確的類設(shè)計(jì)應(yīng)該使用實(shí)例變量:
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. 補(bǔ)充說(shuō)明?
如果同樣的屬性名稱同時(shí)出現(xiàn)在實(shí)例和類中,則屬性查找會(huì)優(yōu)先選擇實(shí)例:
>>> 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
數(shù)據(jù)屬性可以被方法以及一個(gè)對(duì)象的普通用戶(“客戶端”)所引用。 換句話說(shuō),類不能用于實(shí)現(xiàn)純抽象數(shù)據(jù)類型。 實(shí)際上,在 Python 中沒有任何東西能強(qiáng)制隱藏?cái)?shù)據(jù) --- 它是完全基于約定的。 (而在另一方面,用 C 語(yǔ)言編寫的 Python 實(shí)現(xiàn)則可以完全隱藏實(shí)現(xiàn)細(xì)節(jié),并在必要時(shí)控制對(duì)象的訪問(wèn);此特性可以通過(guò)用 C 編寫 Python 擴(kuò)展來(lái)使用。)
客戶端應(yīng)當(dāng)謹(jǐn)慎地使用數(shù)據(jù)屬性 --- 客戶端可能通過(guò)直接操作數(shù)據(jù)屬性的方式破壞由方法所維護(hù)的固定變量。 請(qǐng)注意客戶端可以向一個(gè)實(shí)例對(duì)象添加他們自己的數(shù)據(jù)屬性而不會(huì)影響方法的可用性,只要保證避免名稱沖突 --- 再次提醒,在此使用命名約定可以省去許多令人頭痛的麻煩。
在方法內(nèi)部引用數(shù)據(jù)屬性(或其他方法!)并沒有簡(jiǎn)便方式。 我發(fā)現(xiàn)這實(shí)際上提升了方法的可讀性:當(dāng)瀏覽一個(gè)方法代碼時(shí),不會(huì)存在混淆局部變量和實(shí)例變量的機(jī)會(huì)。
方法的第一個(gè)參數(shù)常常被命名為 self
。 這也不過(guò)就是一個(gè)約定: self
這一名稱在 Python 中絕對(duì)沒有特殊含義。 但是要注意,不遵循此約定會(huì)使得你的代碼對(duì)其他 Python 程序員來(lái)說(shuō)缺乏可讀性,而且也可以想像一個(gè) 類瀏覽器 程序的編寫可能會(huì)依賴于這樣的約定。
任何一個(gè)作為類屬性的函數(shù)都為該類的實(shí)例定義了一個(gè)相應(yīng)方法。 函數(shù)定義的文本并非必須包含于類定義之內(nèi):將一個(gè)函數(shù)對(duì)象賦值給一個(gè)局部變量也是可以的。 例如:
# 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
現(xiàn)在 f
, g
和 h
都是 C
類的引用函數(shù)對(duì)象的屬性,因而它們就都是 C
的實(shí)例的方法 --- 其中 h
完全等同于 g
。 但請(qǐng)注意,本示例的做法通常只會(huì)令程序的閱讀者感到迷惑。
方法可以通過(guò)使用 self
參數(shù)的方法屬性調(diào)用其他方法:
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)
方法可以通過(guò)與普通函數(shù)相同的方式引用全局名稱。 與方法相關(guān)聯(lián)的全局作用域就是包含其定義的模塊。 (類永遠(yuǎn)不會(huì)被作為全局作用域。) 雖然我們很少會(huì)有充分的理由在方法中使用全局作用域,但全局作用域存在許多合理的使用場(chǎng)景:舉個(gè)例子,導(dǎo)入到全局作用域的函數(shù)和模塊可以被方法所使用,在其中定義的函數(shù)和類也一樣。 通常,包含該方法的類本身是在全局作用域中定義的,而在下一節(jié)中我們將會(huì)發(fā)現(xiàn)為何方法需要引用其所屬類的很好的理由。
每個(gè)值都是一個(gè)對(duì)象,因此具有 類 (也稱為 類型),并存儲(chǔ)為 object.__class__
。
9.5. 繼承?
當(dāng)然,如果不支持繼承,語(yǔ)言特性就不值得稱為“類”。派生類定義的語(yǔ)法如下所示:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
名稱 BaseClassName
必須定義于包含派生類定義的作用域中。 也允許用其他任意表達(dá)式代替基類名稱所在的位置。 這有時(shí)也可能會(huì)用得上,例如,當(dāng)基類定義在另一個(gè)模塊中的時(shí)候:
class DerivedClassName(modname.BaseClassName):
派生類定義的執(zhí)行過(guò)程與基類相同。 當(dāng)構(gòu)造類對(duì)象時(shí),基類會(huì)被記住。 此信息將被用來(lái)解析屬性引用:如果請(qǐng)求的屬性在類中找不到,搜索將轉(zhuǎn)往基類中進(jìn)行查找。 如果基類本身也派生自其他某個(gè)類,則此規(guī)則將被遞歸地應(yīng)用。
派生類的實(shí)例化沒有任何特殊之處: DerivedClassName()
會(huì)創(chuàng)建該類的一個(gè)新實(shí)例。 方法引用將按以下方式解析:搜索相應(yīng)的類屬性,如有必要將按基類繼承鏈逐步向下查找,如果產(chǎn)生了一個(gè)函數(shù)對(duì)象則方法引用就生效。
派生類可能會(huì)重寫其基類的方法。 因?yàn)榉椒ㄔ谡{(diào)用同一對(duì)象的其他方法時(shí)沒有特殊權(quán)限,所以調(diào)用同一基類中定義的另一方法的基類方法最終可能會(huì)調(diào)用覆蓋它的派生類的方法。 (對(duì) C++ 程序員的提示:Python 中所有的方法實(shí)際上都是 virtual
方法。)
在派生類中的重載方法實(shí)際上可能想要擴(kuò)展而非簡(jiǎn)單地替換同名的基類方法。 有一種方式可以簡(jiǎn)單地直接調(diào)用基類方法:即調(diào)用 BaseClassName.methodname(self, arguments)
。 有時(shí)這對(duì)客戶端來(lái)說(shuō)也是有用的。 (請(qǐng)注意僅當(dāng)此基類可在全局作用域中以 BaseClassName
的名稱被訪問(wèn)時(shí)方可使用此方式。)
Python有兩個(gè)內(nèi)置函數(shù)可被用于繼承機(jī)制:
使用
isinstance()
來(lái)檢查一個(gè)實(shí)例的類型:isinstance(obj, int)
僅會(huì)在obj.__class__
為int
或某個(gè)派生自int
的類時(shí)為True
。使用
issubclass()
來(lái)檢查類的繼承關(guān)系:issubclass(bool, int)
為True
,因?yàn)?bool
是int
的子類。 但是,issubclass(float, int)
為False
,因?yàn)?float
不是int
的子類。
9.5.1. 多重繼承?
Python 也支持一種多重繼承。 帶有多個(gè)基類的類定義語(yǔ)句如下所示:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
對(duì)于多數(shù)應(yīng)用來(lái)說(shuō),在最簡(jiǎn)單的情況下,你可以認(rèn)為搜索從父類所繼承屬性的操作是深度優(yōu)先、從左至右的,當(dāng)層次結(jié)構(gòu)中存在重疊時(shí)不會(huì)在同一個(gè)類中搜索兩次。 因此,如果某一屬性在 DerivedClassName
中未找到,則會(huì)到 Base1
中搜索它,然后(遞歸地)到 Base1
的基類中搜索,如果在那里未找到,再到 Base2
中搜索,依此類推。
真實(shí)情況比這個(gè)更復(fù)雜一些;方法解析順序會(huì)動(dòng)態(tài)改變以支持對(duì) super()
的協(xié)同調(diào)用。 這種方式在某些其他多重繼承型語(yǔ)言中被稱為后續(xù)方法調(diào)用,它比單繼承型語(yǔ)言中的 super 調(diào)用更強(qiáng)大。
動(dòng)態(tài)改變順序是有必要的,因?yàn)樗卸嘀乩^承的情況都會(huì)顯示出一個(gè)或更多的菱形關(guān)聯(lián)(即至少有一個(gè)父類可通過(guò)多條路徑被最底層類所訪問(wèn))。 例如,所有類都是繼承自 object
,因此任何多重繼承的情況都提供了一條以上的路徑可以通向 object
。 為了確?;惒粫?huì)被訪問(wèn)一次以上,動(dòng)態(tài)算法會(huì)用一種特殊方式將搜索順序線性化, 保留每個(gè)類所指定的從左至右的順序,只調(diào)用每個(gè)父類一次,并且保持單調(diào)(即一個(gè)類可以被子類化而不影響其父類的優(yōu)先順序)。 總而言之,這些特性使得設(shè)計(jì)具有多重繼承的可靠且可擴(kuò)展的類成為可能。 要了解更多細(xì)節(jié),請(qǐng)參閱 https://www.python.org/download/releases/2.3/mro/。
9.6. 私有變量?
那種僅限從一個(gè)對(duì)象內(nèi)部訪問(wèn)的“私有”實(shí)例變量在 Python 中并不存在。 但是,大多數(shù) Python 代碼都遵循這樣一個(gè)約定:帶有一個(gè)下劃線的名稱 (例如 _spam
) 應(yīng)該被當(dāng)作是 API 的非公有部分 (無(wú)論它是函數(shù)、方法或是數(shù)據(jù)成員)。 這應(yīng)當(dāng)被視為一個(gè)實(shí)現(xiàn)細(xì)節(jié),可能不經(jīng)通知即加以改變。
由于存在對(duì)于類私有成員的有效使用場(chǎng)景(例如避免名稱與子類所定義的名稱相沖突),因此存在對(duì)此種機(jī)制的有限支持,稱為 名稱改寫。 任何形式為 __spam
的標(biāo)識(shí)符(至少帶有兩個(gè)前綴下劃線,至多一個(gè)后綴下劃線)的文本將被替換為 _classname__spam
,其中 classname
為去除了前綴下劃線的當(dāng)前類名稱。 這種改寫不考慮標(biāo)識(shí)符的句法位置,只要它出現(xiàn)在類定義內(nèi)部就會(huì)進(jìn)行。
名稱改寫有助于讓子類重載方法而不破壞類內(nèi)方法調(diào)用。例如:
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
引入了一個(gè) __update
標(biāo)識(shí)符的情況下也不會(huì)出錯(cuò),因?yàn)樗鼤?huì)在 Mapping
類中被替換為 _Mapping__update
而在 MappingSubclass
類中被替換為 _MappingSubclass__update
。
請(qǐng)注意,改寫規(guī)則的設(shè)計(jì)主要是為了避免意外沖突;訪問(wèn)或修改被視為私有的變量仍然是可能的。這在特殊情況下甚至?xí)苡杏?,例如在調(diào)試器中。
請(qǐng)注意傳遞給 exec()
或 eval()
的代碼不會(huì)將發(fā)起調(diào)用類的類名視作當(dāng)前類;這類似于 global
語(yǔ)句的效果,因此這種效果僅限于同時(shí)經(jīng)過(guò)字節(jié)碼編譯的代碼。 同樣的限制也適用于 getattr()
, setattr()
和 delattr()
,以及對(duì)于 __dict__
的直接引用。
9.7. 雜項(xiàng)說(shuō)明?
有時(shí)會(huì)需要使用類似于 Pascal 的“record”或 C 的“struct”這樣的數(shù)據(jù)類型,將一些命名數(shù)據(jù)項(xiàng)捆綁在一起。 這種情況適合定義一個(gè)空類:
class Employee:
pass
john = Employee() # Create an empty employee record
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
一段需要特定抽象數(shù)據(jù)類型的 Python 代碼往往可以被傳入一個(gè)模擬了該數(shù)據(jù)類型的方法的類作為替代。 例如,如果你有一個(gè)基于文件對(duì)象來(lái)格式化某些數(shù)據(jù)的函數(shù),你可以定義一個(gè)帶有 read()
和 readline()
方法從字符串緩存獲取數(shù)據(jù)的類,并將其作為參數(shù)傳入。
實(shí)例方法對(duì)象也具有屬性: m.__self__
就是帶有 m()
方法的實(shí)例對(duì)象,而 m.__func__
則是該方法所對(duì)應(yīng)的函數(shù)對(duì)象。
9.8. 迭代器?
到目前為止,您可能已經(jīng)注意到大多數(shù)容器對(duì)象都可以使用 for
語(yǔ)句:
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='')
這種訪問(wèn)風(fēng)格清晰、簡(jiǎn)潔又方便。 迭代器的使用非常普遍并使得 Python 成為一個(gè)統(tǒng)一的整體。 在幕后,for
語(yǔ)句會(huì)在容器對(duì)象上調(diào)用 iter()
。 該函數(shù)返回一個(gè)定義了 __next__()
方法的迭代器對(duì)象,此方法將逐一訪問(wèn)容器中的元素。 當(dāng)元素用盡時(shí),__next__()
將引發(fā) StopIteration
異常來(lái)通知終止 for
循環(huán)。 你可以使用 next()
內(nèi)置函數(shù)來(lái)調(diào)用 __next__()
方法;這個(gè)例子顯示了它的運(yùn)作方式:
>>> 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
看過(guò)迭代器協(xié)議的幕后機(jī)制,給你的類添加迭代器行為就很容易了。 定義一個(gè) __iter__()
方法來(lái)返回一個(gè)帶有 __next__()
方法的對(duì)象。 如果類已定義了 __next__()
,則 __iter__()
可以簡(jiǎn)單地返回 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. 生成器?
生成器 是一個(gè)用于創(chuàng)建迭代器的簡(jiǎn)單而強(qiáng)大的工具。 它們的寫法類似于標(biāo)準(zhǔn)的函數(shù),但當(dāng)它們要返回?cái)?shù)據(jù)時(shí)會(huì)使用 yield
語(yǔ)句。 每次在生成器上調(diào)用 next()
時(shí),它會(huì)從上次離開的位置恢復(fù)執(zhí)行(它會(huì)記住上次執(zhí)行語(yǔ)句時(shí)的所有數(shù)據(jù)值)。 一個(gè)顯示如何非常容易地創(chuàng)建生成器的示例如下:
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
可以用生成器來(lái)完成的操作同樣可以用前一節(jié)所描述的基于類的迭代器來(lái)完成。 但生成器的寫法更為緊湊,因?yàn)樗鼤?huì)自動(dòng)創(chuàng)建 __iter__()
和 __next__()
方法。
另一個(gè)關(guān)鍵特性在于局部變量和執(zhí)行狀態(tài)會(huì)在每次調(diào)用之間自動(dòng)保存。 這使得該函數(shù)相比使用 self.index
和 self.data
這種實(shí)例變量的方式更易編寫且更為清晰。
除了會(huì)自動(dòng)創(chuàng)建方法和保存程序狀態(tài),當(dāng)生成器終結(jié)時(shí),它們還會(huì)自動(dòng)引發(fā) StopIteration
。 這些特性結(jié)合在一起,使得創(chuàng)建迭代器能與編寫常規(guī)函數(shù)一樣容易。
9.10. 生成器表達(dá)式?
某些簡(jiǎn)單的生成器可以寫成簡(jiǎn)潔的表達(dá)式代碼,所用語(yǔ)法類似列表推導(dǎo)式,但外層為圓括號(hào)而非方括號(hào)。 這種表達(dá)式被設(shè)計(jì)用于生成器將立即被外層函數(shù)所使用的情況。 生成器表達(dá)式相比完整的生成器更緊湊但較不靈活,相比等效的列表推導(dǎo)式則更為節(jié)省內(nèi)存。
示例:
>>> 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']
備注