寫在前面
本篇文章是《The Python Tutorial》(3.6.1),第九章,類的譯文。
9. Classes
與其他編程語言相比腻暮,Python
的類機(jī)制定義類時,最小化了新的語法和語義的引入毯侦。Python
類機(jī)制是C++
和Modula-3
的混合體哭靖。Python
類支持所有面向?qū)ο缶幊痰奶匦裕?/p>
類繼承機(jī)制允許多繼承,子類可以覆蓋其父類們的任何方法侈离,方法可以使用相同的名字調(diào)用父類中的方法试幽。對象可以包含任意數(shù)量和類型的數(shù)據(jù)。
跟模塊相似卦碾,Python
類也具有Python
的動態(tài)性質(zhì):
類在運行時被創(chuàng)建铺坞,在創(chuàng)建之后可被修改
用在C++
的術(shù)語來說,在Python
中洲胖,通常類成員(包括數(shù)據(jù)成員)是public(公有)的(除了見下文的Private Variables
)济榨,并且所有成員方法是virtual(虛擬)的。跟Modual-3
一樣绿映,從對象的方法中引用其成員沒有直接的方法:方法成員聲明時帶有一個顯式的表示對象自身的參數(shù)擒滑,這個參數(shù)在參數(shù)列表第一位腐晾,并且在調(diào)用時隱式提供。與Smalltalk
語言類似丐一,類自身也是對象藻糖。這為importing
(導(dǎo)入)和renaming
(重命名)提供了語義支持。與C++
和Modula-3
不同的是库车,built-in
(內(nèi)建)類型可以被程序員用作父類來擴(kuò)展颖御。當(dāng)然,與C++
類似凝颇,大多數(shù)帶有特殊語義的內(nèi)建運算符(比如算數(shù)運算符,下標(biāo)運算符)可以被重定義為類實例疹鳄。
(由于缺乏關(guān)于類的普遍接受的通用術(shù)語拧略,作者會偶爾使用C++
和Smalltalk
的相關(guān)項。由于Modula-3
的面相對象語義與Python
更接近瘪弓,作者會使用Modula-3
的一些東西垫蛆,但是恐怕少數(shù)讀者聽說過。)
9.1. A Word About Names and Objects
對象具有其特性腺怯,不同作用域的各種名字或者相同作用域的不同名字都可以綁定到同一個對象上袱饭。在其他語言中這被稱作別名。這種特性在初識Python
時不易理解呛占,而且在處理不可變基本類型(如數(shù)字虑乖,字符串,元組)時可以被安全的忽視晾虑。然而疹味,當(dāng)涉及到可變對象時,比如列表帜篇,字典和其他大多數(shù)類型笙隙,別名可能會對Python
的語義產(chǎn)生意想不到的影響签钩。由于別名在某些方面就像指針一樣假消,通常在一些地方會使程序受益臼予。比如窄锅,傳遞一個對象很容易,因為在實現(xiàn)上只是傳遞了一個指針疏之;如果一個函數(shù)修改了參數(shù)對象锋爪,修改對調(diào)用者可見(譯注:可變對象)——這消除了在Pascal
中需要兩個不同參數(shù)的傳參機(jī)制的需求。
9.2. Python Scopes and Namespaces
在介紹類之前拯爽,先介紹Python
的作用域
的相關(guān)規(guī)則惰瓜。類定義和命名空間
之間有一些巧妙的聯(lián)系,理解了作用域
以及命名空間
的機(jī)制之后才能完全理解類的一些行為曲尸。此外,這個主題的知識對高級Python
程序員也是有幫助的。
我們以一些定義開始:
namespace(命名空間)
是從名字到對象的映射薯嗤。目前大多數(shù)的命名空間
都是由Python
的字典實現(xiàn)的,但是實現(xiàn)方式通常都是不重要的玻褪,并且將來或許會改變命名空間
的實現(xiàn)方式稚矿。命名空間
的一些例子:built-in
名字的集合(包括如abs()
的函數(shù)以及built-in
的異常名字)昧识;模塊中的global
(全局)名字;函數(shù)調(diào)用中的local
局部名字等池户。在某種意義上,對象的屬性集合同樣構(gòu)成了一個命名空間
毙沾。關(guān)于命名空間
比較重要的特征是:不同命名空間
中的名字絕對沒有任何聯(lián)系;比如,兩個不同模塊可以定義同名函數(shù)maximize
汤徽,不會有任何沖突——模塊的使用者必須使用模塊名字為前綴來調(diào)用它們狱掂。
順便說一下讯嫂,我使用attribute
(屬性)這個詞來描述任何跟在.
后面的名字曲楚,例如,在表達(dá)式z.real
中褥符,real
是對象z
的屬性龙誊。嚴(yán)格來說,在模塊中引用名字是屬性的引用:在表達(dá)式modname.funcname
中modname
是一個模塊對象喷楣,funcname
是模塊對象的一個屬性趟大。因此,模塊的屬性和定義在模塊中的全局名字之間有直接的映射關(guān)系:它們共享同一個命名空間
[1]铣焊。
屬性也許是只讀或者可寫的护昧。在后面的例子中,對屬性賦值是可行的粗截。模塊的屬性是可寫的:可以寫出modname.the_answer = 42
這樣的語句〉肪妫可寫屬性也可以使用del
語句來刪除熊昌。例如,del modname.the_answer
會從名字為modname
的對象中移除the_answer
屬性湿酸。
命名空間
在不同的時刻被創(chuàng)建婿屹,并且具有不同的生命周期。包含built-in
(內(nèi)建)名字的命名空間
在Python
解釋器啟動時創(chuàng)建推溃,并且不會被刪除昂利。模塊的全局命名空間
在模塊定義讀入時創(chuàng)建;通常來說铁坎,模塊的命名空間
也是在解釋器退出時銷毀蜂奸。在解釋器最高層調(diào)用執(zhí)行的語句,無論是從腳本文件讀入還是交互輸入的硬萍,被認(rèn)為是一個叫做__main__
模塊的一部分扩所,因此這些語句有自己的全局命名空間
(實際上內(nèi)建名字也在一個模塊中,這個模塊名字叫做builtins
)
函數(shù)的局部namespace
在函數(shù)調(diào)用時創(chuàng)建朴乖,函數(shù)返回或者拋出未在函數(shù)中處理的異常時銷毀祖屏。(實際上助赞,忽略掉函數(shù)的namespace
能更好理解函數(shù)調(diào)用實際發(fā)生的事情) 當(dāng)然,每一次遞歸調(diào)用都有自己的局部namespace
袁勺。
scope(作用域)是一個可以直接訪問命名空間的Python
程序文本區(qū)域雹食。這里的"直接訪問"的意思是對一個名字的無限定引用會嘗試在命名空間中查找這個名字 (譯注:限定引用是通過對象.屬性
的方式,無限定引用是直接寫名字引用的方式)
雖然定義作用域是靜態(tài)的期丰,但是被動態(tài)的使用群叶。在程序執(zhí)行的任何時間內(nèi),至少有三個命名空間可以被直接訪問的嵌套的作用域:
- 首先搜索咐汞,包含局部名字的
最內(nèi)層作用域
- 根據(jù)嵌套層次從內(nèi)到外搜索盖呼,包含非局部也非全局名字的任意
封閉函數(shù)的作用域
- 倒數(shù)第二次被搜索,
包含當(dāng)前模塊全局名字的作用域
- 最后被搜索化撕,包含內(nèi)建名字的
最外層作用域
(譯注:這就是所謂LEGB - local, enclosing, global, buit-in
搜索規(guī)則几晤,這四個指的是作用域而非命名空間
)
如果名字是全局的,那么對名字所有的引用和賦值操作都會到包含這個模塊全局名字的作用域搜索植阴。若要重新綁定在最內(nèi)層作用域
之外變量蟹瘾,可以使用nonlocal
語句;如果該變量未聲明為nonlocal
掠手,那么變量只讀(對變量寫操作會在最內(nèi)層作用域
創(chuàng)建一個新的局部變量憾朴,外部的變量不會有任何改變)。
通常喷鸽,局部作用域
引用函數(shù)的局部名字(譯注:局部作用域引用函數(shù)的命名空間)众雷。在函數(shù)之外,局部作用域
和全局作用域
引用相同的命名空間
:模塊命名空間
做祝。然而砾省,類定義將命名空間
關(guān)聯(lián)到局部作用域中。
(譯注:類的定義創(chuàng)建一個命名空間混槐,并把它關(guān)聯(lián)到局部作用域上编兄,這一點非常重要)
作用域源于程序文本上的意義:無論函數(shù)從哪兒或者以何種別名被調(diào)用,定義在模塊中的函數(shù)的全局作用域
是該模塊的命名空間
声登,這一點非常重要狠鸳!另一方面,實際的名字搜索是在動態(tài)執(zhí)行的悯嗓,在運行時確定件舵;然而,語言定義不斷朝著靜態(tài)名字解決方案發(fā)展脯厨,未來可能在編譯時確定芦圾。因此不要依賴名字的動態(tài)搜索。(實際上俄认,局部變量已經(jīng)靜態(tài)確定了个少。)
Python
有一個特別的地方:如果沒有使用global
語句聲明變量洪乍,那么對于其的賦值語句會影響最內(nèi)層作用域
(譯注:沒用使用global
語句聲明該名字是全局名字時,在局部作用域
對其進(jìn)行賦值操作夜焦,會在局部作用域
對應(yīng)的命名空間
創(chuàng)建一個同名的變量壳澳。) 賦值操作從不拷貝數(shù)據(jù),只是簡單的將名字綁定到對象上茫经。對于刪除操作也一樣:語句del x
移除局部作用域
的命名空間
中x
的綁定關(guān)系巷波。實際上,所有引入新的名字的操作都是用的是局部作用域
:特別地卸伞,import
語句以及函數(shù)定義也將模塊或者函數(shù)名字綁定到局部作用域
抹镊。(譯注:這就是使用導(dǎo)入其他模塊后,不能直接引用導(dǎo)入模塊成員的原因荤傲,而要使用模塊.屬性
的方式)
可以使用global
語句來表明特定的變量屬于全局作用域
垮耳,并且應(yīng)該重新綁定;nonlocal
語句表明特定的變量是enclosing
作用域遂黍,并且應(yīng)該重新綁定终佛。
9.2.1. Scopes and Namespaces Example
以示例說明了如何引用不同作用域
和命名空間
,以及global
和nonlocal
如何印象變量綁定:
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. A First Look at Classes
類引入了少量新的語法,三個新的對象類型以及一些新的語義敬飒。
9.3.1. Class Definition Syntax
最簡單的類定義如下:
class ClassName:
<statement-1>
.
.
.
<statement-N>
類定義與函數(shù)定義(def
語句)相似邪铲,必須在生效之前執(zhí)行。(可以將類定義放在if
的分支或者函數(shù)中來證明這個問題驶拱。)
在實踐中,類中的語句通常都是函數(shù)定義晶衷,但是其他語句也是允許的蓝纲,有時候也是有用的——我們后面介紹。類中的函數(shù)定義通常有一個特殊形式的參數(shù)列表晌纫,參數(shù)受方法約定調(diào)用的影響——當(dāng)然税迷,這些后面會解釋。
類定義開始后锹漱,會創(chuàng)建一個作為局部作用域
的新命名空間
——因此箭养,所有局部變量的賦值操作都在這個命名空間
中。特別地哥牍,函數(shù)定義在這個命名空間
中綁定了新的名字毕泌。
類定義完成后喝检,創(chuàng)建了一個類對象
。這個類對象基本上就是對類定義創(chuàng)建的命名空間
內(nèi)容的包裝撼泛;我們在接下來的章節(jié)中會深入學(xué)習(xí)挠说。類定義完成后,原始的局部作用域
(類定義進(jìn)入之前生效的局部作用域
)得到恢復(fù)愿题,類對象綁定到了類定義頭部的名字(示例中是ClassName
)
(譯注:
在一個模塊中定義類损俭,從書寫下 class ClassName:
開始,類定義開始潘酗,此時創(chuàng)建一個新的命名空間
作為局部作用域
杆兵,注意作用域
只是程序文本上的意義。接下來的所有賦值語句仔夺,函數(shù)定義都綁定在這個新的命名空間
琐脏。按照Python
一切皆對象的哲學(xué),類也不例外囚灼。類定義結(jié)束后骆膝,一個類對象被創(chuàng)建,這個對象被綁定到值為類名的名字上灶体,即有了類名->類對象
這一個映射關(guān)系阅签,這個映射關(guān)系綁定在原始的命名空間
中。類對象是對類定義時創(chuàng)建的那個命名空間
的包裝蝎抽。類定義結(jié)束后政钟,原來的局部作用域
就恢復(fù)了。
)
9.3.2. Class Objects
類對象支持兩種操作:屬性引用以及實例化樟结。
對象屬性引用 使用Python
廣泛使用的引用屬性的標(biāo)準(zhǔn)語法:obj.name
养交。類對象創(chuàng)建時,類命名空間
中所有的名字都是有效的對象屬性
名字瓢宦。因此碎连,如果類定義是這樣的:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那么,MyClass.i
和MyClass.f
都是有效的屬性引用驮履,各自返回一個整數(shù)和一個函數(shù)對象鱼辙。類屬性也可以被賦值,因此可以使用賦值語句改變MyClass.i
的值玫镐。__doc__
也是有效的屬性倒戏,返回屬于該類的文檔描述:"A simple example class"
。
類實例化使用函數(shù)符號法恐似《捧危可以將類對象當(dāng)成返回類新實例的無參函數(shù)。例如(假設(shè)是上述類):
x = MyClass()
創(chuàng)建類的新實例,并且將這個實例對象綁定到局部變量x
實例化操作("調(diào)用"一個類對象)創(chuàng)建一個空的對象葛闷。許多類都有可以創(chuàng)建自定義初始狀態(tài)實例的需求憋槐。因此類可能定義一個叫做__init__()
的方法,像這樣:
def __init__(self):
self.data = []
當(dāng)類定義了__init__()
方法后孵运,類實例化時會自動調(diào)用__init__()
方法秦陋。因此在這個例子中,一個新的實例化實例可以通過下面的方法獲得:
x = MyClass()
當(dāng)然治笨,為了更好的擴(kuò)展驳概,__init__()
方法可以帶一些參數(shù)。這種情況下旷赖,給類實例化操作的參數(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. Instance Objects
現(xiàn)在我們可以使用實例對象做什么?實例對象接受的唯一操作就是屬性引用等孵。有兩種類型的有效屬性名字稚照,數(shù)據(jù)屬性和方法(譯注:這里的方法與函數(shù)是不一樣的)
數(shù)據(jù)屬性相當(dāng)于Smalltalk
中的"實例變量",以及C++
中的"數(shù)據(jù)成員"俯萌。數(shù)據(jù)屬性無需聲明果录;像局部變量一樣,它們在第一次賦值時就會綁定到相應(yīng)對象咐熙。例如弱恒,如果x
是上面創(chuàng)建的MyClass
的實例,下面的代碼段會打印16
棋恼,不會拋出異常:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
另一種實例屬性引用是 方法返弹。方法是“屬于”對象的函數(shù)。(Python
中爪飘,術(shù)語方法不是類實例獨有的:其他對象類型也可以有方法义起。例如,list
對象有append
, insert
, remove
, sort
等方法师崎。然而默终,在接下來的討論中,我們使用術(shù)語方法特指類實例的方法,除非另有明確說明。)
實例對象有效的方法名取決于它的類丹莲。按照定義,類中所有的函數(shù)對象對應(yīng)類實例的方法肴熏。因此在例子中鬼雀,x.f
是有效的方法引用顷窒,因為MyClass.f
是一個函數(shù),但是x.i
不是有效方法引用,因為MyClass.i
不是函數(shù)鞋吉。但是x.f
與MyClass.f
是不一樣的鸦做,它是一個方法對象,而不是函數(shù)對象谓着。(譯注:Python
中一切皆對象泼诱,有對象就有對象對應(yīng)的類,方法和函數(shù)是對應(yīng)不同的類)
9.3.4. Method Objects
通常赊锚,方法通過寫在其綁定名字右邊的方式調(diào)用:
x.f()
在MyClass
的例子中治筒,這個調(diào)用會返回字符串'hello world'。然而舷蒲,有時候并不需要立即調(diào)用:x.f
是一個方法對象耸袜,可以被存儲起來以后調(diào)用。例如:
xf = x.f
while True:
print(xf())
會持續(xù)打印'hello world'直到程序退出牲平。
方法調(diào)用時堤框,到底發(fā)生了什么?也許你注意到了上面調(diào)用x.f()
時沒有傳遞任何參數(shù)纵柿,即使f()
的函數(shù)定義指定了一個參數(shù)蜈抓。參數(shù)發(fā)生了什么?當(dāng)不傳遞任何參數(shù)調(diào)用一個需要參數(shù)的函數(shù)時昂儒,Python
必然會拋出異彻凳梗——即使參數(shù)實際上沒有使用...
實際上,或許你猜到了答案:方法特殊之處在于實例對象被作為函數(shù)的第一個參數(shù)傳遞荆忍。在例子中格带,調(diào)用x.f()
恰好等于MyClass.f(x)
。通常刹枉,調(diào)用一個有n
個參數(shù)的方法叽唱,等同于重新構(gòu)造參數(shù)列表調(diào)用方法對應(yīng)的函數(shù),新的參數(shù)列表在原來的參數(shù)列表的第一位插入方法所屬實例對象微宝。
如果你仍然不理解方法是如何工作的棺亭,了解其實現(xiàn)原理也許能澄清原因。引用非數(shù)據(jù)屬性的實例屬性時蟋软,會搜索它對應(yīng)的類镶摘。如果名字是一個有效的函數(shù)對象,Python
會將實例對象連同函數(shù)對象打包到一個抽象的對象中并且依據(jù)這個對象創(chuàng)建方法對象:這就是被調(diào)用的方法對象岳守。當(dāng)使用參數(shù)列表調(diào)用方法對象時凄敢,會使用實例對象以及原有參數(shù)列表構(gòu)建新的參數(shù)列表,并且使用新的參數(shù)列表調(diào)用函數(shù)對象湿痢。
9.3.5. Class and Instance Variables
類變量以及實例變量
通常來說涝缝,實例變量是對于每個實例都獨有的數(shù)據(jù)扑庞,而類變量是該類所有實例共享的屬性和方法:
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'
正如在A Word About Names and Objects中討論的那樣,涉及到如list
列表和dict
字典之類的可變對象時拒逮,共享的數(shù)據(jù)或許有一些意想不到的驚人影響罐氨。例如,下面代碼中的trick
列表不應(yīng)該作為類變量使用滩援,因為所有的Dog
實例會共享同一個列表:
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è)計方式應(yīng)該是使用實例變量代替類變量:
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. Random Remarks
數(shù)據(jù)屬性會覆蓋同名方法屬性栅隐;為避免意外的名字沖突,這些沖突或許會造成大型程序中極難排除的Bug玩徊,使用一些約定來減少沖突的幾率是非常明智的租悄。可能的約定包括大寫方法名恩袱,給數(shù)據(jù)屬性的名字建一個短小的唯一字符串(或許只是一個下劃線)前綴恰矩,或者使用動詞作為方法名,名詞作為數(shù)據(jù)屬性名憎蛤。
數(shù)據(jù)屬性可能被方法和對象的普通用戶引用外傅。換句話說,類不能用來實現(xiàn)純粹的抽象數(shù)據(jù)類型俩檬。實際上萎胰,Python
并沒有強(qiáng)制數(shù)據(jù)隱藏的機(jī)制——一切都基于約定(另一方面,用C
寫的Python
實現(xiàn)了完全隱藏實現(xiàn)細(xì)節(jié)并且在需要的情況下控制對象的訪問權(quán)限棚辽;C
寫的Python
擴(kuò)展可以是使用這些特性)
客戶應(yīng)該謹(jǐn)慎地使用數(shù)據(jù)屬性——客戶在實例對象中加入自己的數(shù)據(jù)屬性時技竟,或許會把由方法維持的不變量搞糟。值得注意的是屈藐,只有避免了名字沖突榔组,客戶或許會在實例對象中加入自己的數(shù)據(jù)成員,而不影響方法的有效性联逻。再次搓扯,命名約定可以解決許多頭疼的問題。
在方法中沒有通過名字直接引用數(shù)據(jù)屬性(或者其他方法)的途徑包归。(譯注:直接途徑是指只通過名字引用锨推,如name
,self.name
不是直接引用公壤。) 我認(rèn)為這實際上增加了方法的可讀性:當(dāng)閱讀方法時换可,不會混淆局部變量和實例變量。
通常厦幅,方法的第一個參數(shù)名叫self
沾鳄。這僅僅是一個約定:名字self
對Python
絕沒有任何特殊含義。然而值得注意的是确憨,若不遵循約定译荞,你的代碼對于其他Python
程序員的可讀性會降低套媚,并且有的類查看程序或許會依賴該約定也是有可能的。
任何類屬性的函數(shù)對象都為該類的實例對象定義了一個方法磁椒。函數(shù)定義的代碼不必要寫在類定義中:將一個函數(shù)對象分配給類中的一個局部變量也是可行的。例如:
# 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
都是指向函數(shù)對象玫芦,都是類C
的屬性浆熔,因此他們都是類C
實例對象的方法——h
等于g
。注意這種寫法通常只會迷惑程序的讀者桥帆。
方法可以使用參數(shù)self
來調(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)
與使用普通函數(shù)一樣医增,方法也可以以相同的方式引用全局名字。與方法關(guān)聯(lián)的全局作用域
是包含方法定義的模塊老虫。(類從來不會作為全局作用域
使用叶骨。) 雖然在方法中使用全局?jǐn)?shù)據(jù)不是一個明智的選擇,但是全局作用域
確實有許多合理的使用場景:首先祈匙,導(dǎo)入到全局作用域
的函數(shù)和模塊可以被方法忽刽,函數(shù)和定義在其中的類使用。通常夺欲,包含該方法的類也會自定義在這個全局作用域
中跪帝,在后面的章節(jié)我們會介紹方法為什么會需要引用自己的類。
9.5. Inheritance
有類
這一特性的語言些阅,如果不支持繼承伞剑,那類
這個特性就沒有什么意義了。派生類定義的語法如下:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
名字BaseClassName
必須與派生類定義在同一個作用域
中市埋。在基類類名的位置黎泣,還可以使用其他表達(dá)式。當(dāng)基類定義在其他模塊中時缤谎,這一點非常有用:
class DerivedClassName(modname.BaseClassName):
派生類定義的執(zhí)行過程與基類一樣抒倚。派生類對象構(gòu)造完成時,基類也被記住了坷澡。這被用來解析屬性引用:如果被請求的屬性在該類中沒有找到衡便,搜索繼續(xù)在基類尋找。當(dāng)基類本身派生于其他類時洋访,會遞歸應(yīng)用這個規(guī)則镣陕。
派生類的實例化并沒有什么特殊之處:DrivedClassName()
創(chuàng)建了一個新的類實例。方法引用按照如下解析:搜索對應(yīng)類屬性姻政,必要時沿著類繼承鏈向下搜索呆抑,如果找到對應(yīng)函數(shù)對象,那這個方法引用就是有效的汁展。
派生類可以重寫其基類的方法鹊碍。因為方法在調(diào)用相同對象的其他方法時沒有特權(quán)厌殉,基類的一個方法調(diào)用基類的另一個方法時,可能最終會調(diào)用其派生類中被重寫的方法侈咕。(對于C++
程序員來說公罕,Python
中所有的方法實質(zhì)上都是虛
的。)
派生類中重寫基類方法時耀销,可能會擴(kuò)展基類方法而不是簡單的替換掉基類的同名方法楼眷。一個簡單的方式可以直接調(diào)用基類方法:使用BaseClassName.methodname(self, arguments)
。這對客戶有時候也很有用熊尉。(注意僅當(dāng)基類BaseClassName
在全局作用域
中可訪問時才生效罐柳。)
Python
有兩個可以判斷繼承關(guān)系的內(nèi)建函數(shù):
- 使用
isinstance()
檢查實例的類型:isinstance(obj, int)
,當(dāng)且僅當(dāng)obj.__class__
是int
或者派生與int
的類時狰住,返回True
- 使用
issubclass()
檢查類的繼承關(guān)系:issubclass(bool, int)
返回True
张吉,因為bool
是int
的子類。然而issubclass(float, int)
返回False
催植,因為float
不是int
的子類肮蛹。
9.5.1. Multiple Inheritance
Python
也支持多繼承。多繼承的類定義如下:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
在大多數(shù)情況下创南,在最簡單的情況中蔗崎,可以認(rèn)為搜索從父類繼承的屬性是深度優(yōu)先,從左到右的扰藕,而不是繼承有重疊時在同一個類中搜索兩次缓苛。因此,如果一個屬性在DerivedClassName
中沒有找到邓深,接下來會在Base1
中搜索未桥,遞歸的在Base1
的基類中搜索,如果還沒有找到芥备,會在Base2
中搜索冬耿,以此類推。
實際上的搜索方式會比上述稍微復(fù)雜一些萌壳;為了支持super()
亦镶,方法的解析順序會動態(tài)變化。在其他一些多繼承語言中袱瓮,這種方式被稱為call-next-method
缤骨,這種方式比單繼承語言中的super
調(diào)用更加強(qiáng)大。
因為所有的多繼承都存在一個或者多個菱形關(guān)系(至少一個父類可以通過從最底層類開始的多條路徑達(dá)到)尺借,動態(tài)排序是必須的绊起。比如,所有的類都繼承自object
燎斩,因此任何多繼承都存在不止一條可達(dá)object
的路徑虱歪。為避免基類被重復(fù)訪問蜂绎,動態(tài)算法線性化了搜索順序,這個算法維持在每個類中從左到右的順序笋鄙,每個父類只調(diào)用一次师枣,并且是單調(diào)的(這意味著一個類被繼承時,不會影響其父類的優(yōu)先順序萧落。) 這些特性綜合起來践美,使得設(shè)計出可靠且可擴(kuò)展的多繼承成為可能。更多詳細(xì)信息铐尚,請參考:https://www.python.org/download/releases/2.3/mro/。
9.6. Private Variables
Python
中不存在只能在對象內(nèi)部訪問哆姻,而不能在外部訪問的“私有”實例變量宣增。然而,大多數(shù)Python
代碼都遵循了這樣一個約定:以一個下劃線為前綴的名字(如_spam
)應(yīng)該被當(dāng)做是API
非公有的部分(無論是函數(shù)矛缨,方法還是數(shù)據(jù)成員)爹脾。這個約定是實現(xiàn)細(xì)節(jié),更改后不會通知箕昭。
由于類的私有成員有一個有效的用例(即為避免基類名字和子類名字的沖突)灵妨,Python
只對這個機(jī)制做了有限的支持,叫做名字改編
落竹。任意形如__spam
(至少兩個前置下劃線泌霍,至多一個后置下劃線)的標(biāo)識符被替換為文本_classname__spam
,classname
是當(dāng)前類的名字加上一個前置下劃線述召。不論標(biāo)識符的句法位置在哪兒朱转,只要這種標(biāo)識符出現(xiàn)在類定義中,形如__spam
的名字都會被改編积暖。
名字改編使得子類可以重寫父類方法藤为,而不會影響父類中的方法內(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)
需要注意的是名字改編規(guī)則是為了避免意外而設(shè)計的:訪問或者改變被認(rèn)為是私有屬性的值是可行的夺刑。甚至這在一些特定的環(huán)境下是可行的缅疟,比如在debugger
時。
需要注意傳遞給exec()
或者eval()
的代碼并不為認(rèn)為用來調(diào)用類的類名是當(dāng)前類遍愿;類似于global
語句影響存淫,字節(jié)編譯的代碼也受同樣的限制。這同樣作用于getattr()
, setattr()
以及delattr()
以及直接引用__dict__
沼填。
9.7. Odds and Ends
有時候Pascal
的“record”以及C
中的“struct”是有用的纫雁,這兩種結(jié)構(gòu)將一些數(shù)據(jù)項綁定在一起。在Python
中空的類定義也可以優(yōu)雅做到這一點:
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ù)類型的代碼段倾哺,通吃埃可以傳入一個模擬該數(shù)據(jù)類型方法的類來代替它刽脖。例如,如果有一個格式化文件對象數(shù)據(jù)的函數(shù)忌愚,可以新建一個有能從數(shù)據(jù)緩沖讀取數(shù)據(jù)的read()
和readline()
方法的對象來代替文件對象曲管,把新的類實例作為參數(shù)傳遞到這個函數(shù)即可。(譯注:由于Python
是動態(tài)語言硕糊,實現(xiàn)多態(tài)的方法不如Java
等靜態(tài)語言嚴(yán)格)
實例方法對象也擁有屬性:m.__self__
指向持有這個方法m()
的實例對象院水,m.__func__
指向?qū)?yīng)于方法的函數(shù)對象。
9.8. Iterators
到現(xiàn)在你也許注意到了大多數(shù)基本對象可以使用for
語句來循環(huán)遍歷:
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='')
這種訪問風(fēng)格清楚简十,簡潔以及方便檬某。迭代器的使用遍及Python
,風(fēng)格統(tǒng)一螟蝙。實現(xiàn)上恢恼,for
語句調(diào)用了容器對象的iter()
函數(shù)。這個函數(shù)返回定義了遍歷容器元素的__next()__
方法的迭代器對象胰默。當(dāng)沒有剩余元素遍歷時场斑,__next()__
方法拋出一個指示for
循環(huán)終止的StopIteration
異常∏J穑可以使用內(nèi)建函數(shù)next()
來調(diào)用__next()__
方法漏隐。以下是示例:
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> 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
了解了迭代的原理,就可以讓自定義的類可迭代了奴迅。定義一個名為__iter()__
方法青责,該方法可返回具有__next()__
方法的對象。如果類本身定義了__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)
...
m
a
p
s
9.9. Generators
生成器是創(chuàng)建迭代器簡單而強(qiáng)大的工具爽柒。它們像普通函數(shù)一樣定義,但當(dāng)需要返回數(shù)據(jù)時使用yield
者填。每次使用next()
調(diào)用生成器時浩村,生成器從上次的斷點恢復(fù)執(zhí)行(生成器記錄了所有數(shù)據(jù)以及下一條執(zhí)行的語句)。以下示例說明了生成器可以很容易的創(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
生成器可做的占哟,前面一節(jié)中的基于類的迭代器也可以做到心墅。__iter()__
和__next()__
方法的自動創(chuàng)建使得生成器更加的緊湊。
另一個重要的特色是調(diào)用之間的局部變量和執(zhí)行狀態(tài)自動保存起來榨乎。相較于使用如self.index
和self.data
之類的實例變量怎燥,這個自動機(jī)制使得生成器更加容易書寫,更加清楚蜜暑。
除了自動方法創(chuàng)建和程序狀態(tài)保存之外铐姚,當(dāng)生成器終止時,自動拋出StopIteration
異常。結(jié)合這些特性隐绵,創(chuàng)建生成器比起創(chuàng)建常規(guī)方法來更簡單之众。
9.10. Generator Expressions
使用類似于列表推導(dǎo)式的語法,將中括號[]
換成括號()
依许,可以簡潔的寫出一些簡單生成器棺禾。這些表達(dá)式是為恰好使用生成器的封閉函數(shù)的情形設(shè)計的。比起完成的生成器定義峭跳,生成器表達(dá)式更加緊湊膘婶,更加容易記憶,語法與列表推導(dǎo)式近似蛀醉,但不多變悬襟。
示例:
>>> 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
>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}
>>> 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']
Footnotes
[1] 除了一點,模塊對象有一個叫做__dict__
的私密屬性拯刁,這個屬性返回用來實現(xiàn)模塊命名空間
的字典脊岳;名字__dict__
是屬性而不是全局名字。顯而易見筛璧,這違反了名字空間的抽象原則逸绎,應(yīng)該嚴(yán)格地限制在調(diào)試使用中惹恃。