9. 類(lèi)?

類(lèi)把數據與功能綁定在一起。創(chuàng )建新類(lèi)就是創(chuàng )建新的對象 類(lèi)型,從而創(chuàng )建該類(lèi)型的新 實(shí)例 。類(lèi)實(shí)例支持維持自身狀態(tài)的屬性,還支持(由類(lèi)定義的)修改自身狀態(tài)的方法。

和其他編程語(yǔ)言相比,Python 的類(lèi)只使用了很少的新語(yǔ)法和語(yǔ)義。Python 的類(lèi)有點(diǎn)類(lèi)似于 C++ 和 Modula-3 中類(lèi)的結合體,而且支持面向對象編程(OOP)的所有標準特性:類(lèi)的繼承機制支持多個(gè)基類(lèi)、派生的類(lèi)能覆蓋基類(lèi)的方法、類(lèi)的方法能調用基類(lèi)中的同名方法。對象可包含任意數量和類(lèi)型的數據。和模塊一樣,類(lèi)也支持 Python 動(dòng)態(tài)特性:在運行時(shí)創(chuàng )建,創(chuàng )建后還可以修改。

如果用 C++ 術(shù)語(yǔ)來(lái)描述的話(huà),類(lèi)成員(包括數據成員)通常為 public (例外的情況見(jiàn)下文 私有變量),所有成員函數都是 virtual。與在 Modula-3 中一樣,沒(méi)有用于從對象的方法中引用對象成員的簡(jiǎn)寫(xiě)形式:方法函數在聲明時(shí),有一個(gè)顯式的參數代表本對象,該參數由調用隱式提供。 與在 Smalltalk 中一樣,Python 的類(lèi)也是對象,這為導入和重命名提供了語(yǔ)義支持。與 C++ 和 Modula-3 不同,Python 的內置類(lèi)型可以用作基類(lèi),供用戶(hù)擴展。 此外,與 C++ 一樣,算術(shù)運算符、下標等具有特殊語(yǔ)法的內置運算符都可以為類(lèi)實(shí)例而重新定義。

由于缺乏關(guān)于類(lèi)的公認術(shù)語(yǔ),本章中偶爾會(huì )使用 Smalltalk 和 C++ 的術(shù)語(yǔ)。本章還會(huì )使用 Modula-3 的術(shù)語(yǔ),Modula-3 的面向對象語(yǔ)義比 C++ 更接近 Python,但估計聽(tīng)說(shuō)過(guò)這門(mén)語(yǔ)言的讀者很少。

9.1. 名稱(chēng)和對象?

對象之間相互獨立,多個(gè)名稱(chēng)(在多個(gè)作用域內)可以綁定到同一個(gè)對象。 其他語(yǔ)言稱(chēng)之為別名。Python 初學(xué)者通常不容易理解這個(gè)概念,處理數字、字符串、元組等不可變基本類(lèi)型時(shí),可以不必理會(huì )。 但是,對涉及可變對象,如列表、字典等大多數其他類(lèi)型的 Python 代碼的語(yǔ)義,別名可能會(huì )產(chǎn)生意料之外的效果。這樣做,通常是為了讓程序受益,因為別名在某些方面就像指針。例如,傳遞對象的代價(jià)很小,因為實(shí)現只傳遞一個(gè)指針;如果函數修改了作為參數傳遞的對象,調用者就可以看到更改 --- 無(wú)需 Pascal 用兩個(gè)不同參數的傳遞機制。

9.2. Python 作用域和命名空間?

在介紹類(lèi)前,首先要介紹 Python 的作用域規則。類(lèi)定義對命名空間有一些巧妙的技巧,了解作用域和命名空間的工作機制有利于加強對類(lèi)的理解。并且,即便對于高級 Python 程序員,這方面的知識也很有用。

接下來(lái),我們先了解一些定義。

namespace (命名空間)是映射到對象的名稱(chēng)?,F在,大多數命名空間都使用 Python 字典實(shí)現,但除非涉及到優(yōu)化性能,我們一般不會(huì )關(guān)注這方面的事情,而且將來(lái)也可能會(huì )改變這種方式。命名空間的幾個(gè)常見(jiàn)示例: abs() 函數、內置異常等的內置函數集合;模塊中的全局名稱(chēng);函數調用中的局部名稱(chēng)。對象的屬性集合也算是一種命名空間。關(guān)于命名空間的一個(gè)重要知識點(diǎn)是,不同命名空間中的名稱(chēng)之間絕對沒(méi)有關(guān)系;例如,兩個(gè)不同的模塊都可以定義 maximize 函數,且不會(huì )造成混淆。用戶(hù)使用函數時(shí)必須要在函數名前面附加上模塊名。

點(diǎn)號之后的名稱(chēng)是 屬性。例如,表達式 z.real 中,real 是對象 z 的屬性。嚴格來(lái)說(shuō),對模塊中名稱(chēng)的引用是屬性引用:表達式 modname.funcname 中,modname 是模塊對象,funcname 是模塊的屬性。模塊屬性和模塊中定義的全局名稱(chēng)之間存在直接的映射:它們共享相同的命名空間! 1

屬性可以是只讀或者可寫(xiě)的。如果可寫(xiě),則可對屬性賦值。模塊屬性是可寫(xiě)時(shí),可以使用 modname.the_answer = 42 。del 語(yǔ)句可以刪除可寫(xiě)屬性。例如, del modname.the_answer 會(huì )刪除 modname 對象中的 the_answer 屬性。

命名空間是在不同時(shí)刻創(chuàng )建的,且擁有不同的生命周期。內置名稱(chēng)的命名空間是在 Python 解釋器啟動(dòng)時(shí)創(chuàng )建的,永遠不會(huì )被刪除。模塊的全局命名空間在讀取模塊定義時(shí)創(chuàng )建;通常,模塊的命名空間也會(huì )持續到解釋器退出。從腳本文件讀取或交互式讀取的,由解釋器頂層調用執行的語(yǔ)句是 __main__ 模塊調用的一部分,也擁有自己的全局命名空間。內置名稱(chēng)實(shí)際上也在模塊里,即 builtins 。

函數的本地命名空間在調用該函數時(shí)創(chuàng )建,并在函數返回或拋出不在函數內部處理的錯誤時(shí)被刪除。 (實(shí)際上,用“遺忘”來(lái)描述實(shí)際發(fā)生的情況會(huì )更好一些。) 當然,每次遞歸調用都會(huì )有自己的本地命名空間。

作用域 是命名空間可直接訪(fǎng)問(wèn)的 Python 程序的文本區域。 “可直接訪(fǎng)問(wèn)” 的意思是,對名稱(chēng)的非限定引用會(huì )在命名空間中查找名稱(chēng)。

作用域雖然是靜態(tài)確定的,但會(huì )被動(dòng)態(tài)使用。執行期間的任何時(shí)刻,都會(huì )有 3 或 4 個(gè)命名空間可被直接訪(fǎng)問(wèn)的嵌套作用域:

  • 最內層作用域,包含局部名稱(chēng),并首先在其中進(jìn)行搜索

  • 封閉函數的作用域,包含非局部名稱(chēng)和非全局名稱(chēng),從最近的封閉作用域開(kāi)始搜索

  • 倒數第二個(gè)作用域,包含當前模塊的全局名稱(chēng)

  • 最外層的作用域,包含內置名稱(chēng)的命名空間,最后搜索

如果把名稱(chēng)聲明為全局變量,則所有引用和賦值將直接指向包含該模塊的全局名稱(chēng)的中間作用域。重新綁定在最內層作用域以外找到的變量,使用 nonlocal 語(yǔ)句把該變量聲明為非局部變量。未聲明為非局部變量的變量是只讀的,(寫(xiě)入只讀變量會(huì )在最內層作用域中創(chuàng )建一個(gè) 新的 局部變量,而同名的外部變量保持不變。)

通常,當前局部作用域將(按字面文本)引用當前函數的局部名稱(chēng)。在函數之外,局部作用域引用與全局作用域一致的命名空間:模塊的命名空間。 類(lèi)定義在局部命名空間內再放置另一個(gè)命名空間。

劃重點(diǎn),作用域是按字面文本確定的:模塊內定義的函數的全局作用域就是該模塊的命名空間,無(wú)論該函數從什么地方或以什么別名被調用。另一方面,實(shí)際的名稱(chēng)搜索是在運行時(shí)動(dòng)態(tài)完成的。但是,Python 正在朝著(zhù)“編譯時(shí)靜態(tài)名稱(chēng)解析”的方向發(fā)展,因此不要過(guò)于依賴(lài)動(dòng)態(tài)名稱(chēng)解析?。ň植孔兞恳呀?jīng)是被靜態(tài)確定了。)

Python 有一個(gè)特殊規定。如果不存在生效的 globalnonlocal 語(yǔ)句,則對名稱(chēng)的賦值總是會(huì )進(jìn)入最內層作用域。賦值不會(huì )復制數據,只是將名稱(chēng)綁定到對象。刪除也是如此:語(yǔ)句 del x 從局部作用域引用的命名空間中移除對 x 的綁定。所有引入新名稱(chēng)的操作都是使用局部作用域:尤其是 import 語(yǔ)句和函數定義會(huì )在局部作用域中綁定模塊或函數名稱(chēng)。

global 語(yǔ)句用于表明特定變量在全局作用域里,并應在全局作用域中重新綁定;nonlocal 語(yǔ)句表明特定變量在外層作用域中,并應在外層作用域中重新綁定。

9.2.1. 作用域和命名空間示例?

下例演示了如何引用不同作用域和名稱(chēng)空間,以及 globalnonlocal 對變量綁定的影響:

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

注意,局部 賦值(這是默認狀態(tài))不會(huì )改變 scope_testspam 的綁定。 nonlocal 賦值會(huì )改變 scope_testspam 的綁定,而 global 賦值會(huì )改變模塊層級的綁定。

而且,global 賦值前沒(méi)有 spam 的綁定。

9.3. 初探類(lèi)?

類(lèi)引入了一點(diǎn)新語(yǔ)法,三種新的對象類(lèi)型和一些新語(yǔ)義。

9.3.1. 類(lèi)定義語(yǔ)法?

最簡(jiǎn)單的類(lèi)定義形式如下:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

與函數定義 (def 語(yǔ)句) 一樣,類(lèi)定義必須先執行才能生效。把類(lèi)定義放在 if 語(yǔ)句的分支里或函數內部試試。

在實(shí)踐中,類(lèi)定義內的語(yǔ)句通常都是函數定義,但也可以是其他語(yǔ)句。這部分內容稍后再討論。類(lèi)里的函數定義一般是特殊的參數列表,這是由方法調用的約定規范所指明的 --- 同樣,稍后再解釋。

當進(jìn)入類(lèi)定義時(shí),將創(chuàng )建一個(gè)新的命名空間,并將其用作局部作用域 --- 因此,所有對局部變量的賦值都是在這個(gè)新命名空間之內。 特別的,函數定義會(huì )綁定到這里的新函數名稱(chēng)。

當(從結尾處)正常離開(kāi)類(lèi)定義時(shí),將創(chuàng )建一個(gè) 類(lèi)對象。 這基本上是一個(gè)包圍在類(lèi)定義所創(chuàng )建命名空間內容周?chē)陌b器;我們將在下一節了解有關(guān)類(lèi)對象的更多信息。 原始的(在進(jìn)入類(lèi)定義之前起作用的)局部作用域將重新生效,類(lèi)對象將在這里被綁定到類(lèi)定義頭所給出的類(lèi)名稱(chēng) (在這個(gè)示例中為 ClassName)。

9.3.2. Class 對象?

類(lèi)對象支持兩種操作:屬性引用和實(shí)例化。

屬性引用 使用 Python 中所有屬性引用所使用的標準語(yǔ)法: obj.name。 有效的屬性名稱(chēng)是類(lèi)對象被創(chuàng )建時(shí)存在于類(lèi)命名空間中的所有名稱(chēng)。 因此,如果類(lèi)定義是這樣的:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

那么 MyClass.iMyClass.f 就是有效的屬性引用,將分別返回一個(gè)整數和一個(gè)函數對象。 類(lèi)屬性也可以被賦值,因此可以通過(guò)賦值來(lái)更改 MyClass.i 的值。 __doc__ 也是一個(gè)有效的屬性,將返回所屬類(lèi)的文檔字符串: "A simple example class"。

類(lèi)的 實(shí)例化 使用函數表示法。 可以把類(lèi)對象視為是返回該類(lèi)的一個(gè)新實(shí)例的不帶參數的函數。 舉例來(lái)說(shuō)(假設使用上述的類(lèi)):

x = MyClass()

創(chuàng )建類(lèi)的新 實(shí)例 并將此對象分配給局部變量 x。

實(shí)例化操作(“調用”類(lèi)對象)會(huì )創(chuàng )建一個(gè)空對象。 許多類(lèi)喜歡創(chuàng )建帶有特定初始狀態(tài)的自定義實(shí)例。 為此類(lèi)定義可能包含一個(gè)名為 __init__() 的特殊方法,就像這樣:

def __init__(self):
    self.data = []

當一個(gè)類(lèi)定義了 __init__() 方法時(shí),類(lèi)的實(shí)例化操作會(huì )自動(dòng)為新創(chuàng )建的類(lèi)實(shí)例發(fā)起調用 __init__()。 因此在這個(gè)示例中,可以通過(guò)以下語(yǔ)句獲得一個(gè)經(jīng)初始化的新實(shí)例:

x = MyClass()

當然,__init__() 方法還可以有額外參數以實(shí)現更高靈活性。 在這種情況下,提供給類(lèi)實(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í)例對象?

現在我們能用實(shí)例對象做什么? 實(shí)例對象所能理解的唯一操作是屬性引用。 有兩種有效的屬性名稱(chēng):數據屬性和方法。

數據屬性 對應于 Smalltalk 中的“實(shí)例變量”,以及 C++ 中的“數據成員”。 數據屬性不需要聲明;像局部變量一樣,它們將在第一次被賦值時(shí)產(chǎn)生。 例如,如果 x 是上面創(chuàng )建的 MyClass 的實(shí)例,則以下代碼段將打印數值 16,且不保留任何追蹤信息:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

另一類(lèi)實(shí)例屬性引用稱(chēng)為 方法。 方法是“從屬于”對象的函數。 (在 Python 中,方法這個(gè)術(shù)語(yǔ)并不是類(lèi)實(shí)例所特有的:其他對象也可以有方法。 例如,列表對象具有 append, insert, remove, sort 等方法。 然而,在以下討論中,我們使用方法一詞將專(zhuān)指類(lèi)實(shí)例對象的方法,除非另外顯式地說(shuō)明。)

實(shí)例對象的有效方法名稱(chēng)依賴(lài)于其所屬的類(lèi)。 根據定義,一個(gè)類(lèi)中所有是函數對象的屬性都是定義了其實(shí)例的相應方法。 因此在我們的示例中,x.f 是有效的方法引用,因為 MyClass.f 是一個(gè)函數,而 x.i 不是方法,因為 MyClass.i 不是函數。 但是 x.fMyClass.f 并不是一回事 --- 它是一個(gè) 方法對象,不是函數對象。

9.3.4. 方法對象?

通常,方法在綁定后立即被調用:

x.f()

MyClass 示例中,這將返回字符串 'hello world'。 但是,立即調用一個(gè)方法并不是必須的: x.f 是一個(gè)方法對象,它可以被保存起來(lái)以后再調用。 例如:

xf = x.f
while True:
    print(xf())

將持續打印 hello world,直到結束。

當一個(gè)方法被調用時(shí)到底發(fā)生了什么? 你可能已經(jīng)注意到上面調用 x.f() 時(shí)并沒(méi)有帶參數,雖然 f() 的函數定義指定了一個(gè)參數。 這個(gè)參數發(fā)生了什么事? 當不帶參數地調用一個(gè)需要參數的函數時(shí) Python 肯定會(huì )引發(fā)異常 --- 即使參數實(shí)際未被使用...

實(shí)際上,你可能已經(jīng)猜到了答案:方法的特殊之處就在于實(shí)例對象會(huì )作為函數的第一個(gè)參數被傳入。 在我們的示例中,調用 x.f() 其實(shí)就相當于 MyClass.f(x)。 總之,調用一個(gè)具有 n 個(gè)參數的方法就相當于調用再多一個(gè)參數的對應函數,這個(gè)參數值為方法所屬實(shí)例對象,位置在其他參數之前。

如果你仍然無(wú)法理解方法的運作原理,那么查看實(shí)現細節可能會(huì )弄清楚問(wèn)題。 當一個(gè)實(shí)例的非數據屬性被引用時(shí),將搜索實(shí)例所屬的類(lèi)。 如果被引用的屬性名稱(chēng)表示一個(gè)有效的類(lèi)屬性中的函數對象,會(huì )通過(guò)打包(指向)查找到的實(shí)例對象和函數對象到一個(gè)抽象對象的方式來(lái)創(chuàng )建方法對象:這個(gè)抽象對象就是方法對象。 當附帶參數列表調用方法對象時(shí),將基于實(shí)例對象和參數列表構建一個(gè)新的參數列表,并使用這個(gè)新參數列表調用相應的函數對象。

9.3.5. 類(lèi)和實(shí)例變量?

一般來(lái)說(shuō),實(shí)例變量用于每個(gè)實(shí)例的唯一數據,而類(lèi)變量用于類(lèi)的所有實(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'

正如 名稱(chēng)和對象 中已討論過(guò)的,共享數據可能在涉及 mutable 對象例如列表和字典的時(shí)候導致令人驚訝的結果。 例如以下代碼中的 tricks 列表不應該被用作類(lèi)變量,因為所有的 Dog 實(shí)例將只共享一個(gè)單獨的列表:

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']

正確的類(lèi)設計應該使用實(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. 補充說(shuō)明?

如果同樣的屬性名稱(chēng)同時(shí)出現在實(shí)例和類(lèi)中,則屬性查找會(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

數據屬性可以被方法以及一個(gè)對象的普通用戶(hù)(“客戶(hù)端”)所引用。 換句話(huà)說(shuō),類(lèi)不能用于實(shí)現純抽象數據類(lèi)型。 實(shí)際上,在 Python 中沒(méi)有任何東西能強制隱藏數據 --- 它是完全基于約定的。 (而在另一方面,用 C 語(yǔ)言編寫(xiě)的 Python 實(shí)現則可以完全隱藏實(shí)現細節,并在必要時(shí)控制對象的訪(fǎng)問(wèn);此特性可以通過(guò)用 C 編寫(xiě) Python 擴展來(lái)使用。)

客戶(hù)端應當謹慎地使用數據屬性 --- 客戶(hù)端可能通過(guò)直接操作數據屬性的方式破壞由方法所維護的固定變量。 請注意客戶(hù)端可以向一個(gè)實(shí)例對象添加他們自己的數據屬性而不會(huì )影響方法的可用性,只要保證避免名稱(chēng)沖突 --- 再次提醒,在此使用命名約定可以省去許多令人頭痛的麻煩。

在方法內部引用數據屬性(或其他方法?。┎](méi)有簡(jiǎn)便方式。 我發(fā)現這實(shí)際上提升了方法的可讀性:當瀏覽一個(gè)方法代碼時(shí),不會(huì )存在混淆局部變量和實(shí)例變量的機會(huì )。

方法的第一個(gè)參數常常被命名為 self。 這也不過(guò)就是一個(gè)約定: self 這一名稱(chēng)在 Python 中絕對沒(méi)有特殊含義。 但是要注意,不遵循此約定會(huì )使得你的代碼對其他 Python 程序員來(lái)說(shuō)缺乏可讀性,而且也可以想像一個(gè) 類(lèi)瀏覽器 程序的編寫(xiě)可能會(huì )依賴(lài)于這樣的約定。

任何一個(gè)作為類(lèi)屬性的函數都為該類(lèi)的實(shí)例定義了一個(gè)相應方法。 函數定義的文本并非必須包含于類(lèi)定義之內:將一個(gè)函數對象賦值給一個(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

現在 f, gh 都是 C 類(lèi)的引用函數對象的屬性,因而它們就都是 C 的實(shí)例的方法 --- 其中 h 完全等同于 g。 但請注意,本示例的做法通常只會(huì )令程序的閱讀者感到迷惑。

方法可以通過(guò)使用 self 參數的方法屬性調用其他方法:

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ò)與普通函數相同的方式引用全局名稱(chēng)。 與方法相關(guān)聯(lián)的全局作用域就是包含其定義的模塊。 (類(lèi)永遠不會(huì )被作為全局作用域。) 雖然我們很少會(huì )有充分的理由在方法中使用全局作用域,但全局作用域存在許多合理的使用場(chǎng)景:舉個(gè)例子,導入到全局作用域的函數和模塊可以被方法所使用,在其中定義的函數和類(lèi)也一樣。 通常,包含該方法的類(lèi)本身是在全局作用域中定義的,而在下一節中我們將會(huì )發(fā)現為何方法需要引用其所屬類(lèi)的很好的理由。

每個(gè)值都是一個(gè)對象,因此具有 類(lèi) (也稱(chēng)為 類(lèi)型),并存儲為 object.__class__ 。

9.5. 繼承?

當然,如果不支持繼承,語(yǔ)言特性就不值得稱(chēng)為“類(lèi)”。派生類(lèi)定義的語(yǔ)法如下所示:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

名稱(chēng) BaseClassName 必須定義于包含派生類(lèi)定義的作用域中。 也允許用其他任意表達式代替基類(lèi)名稱(chēng)所在的位置。 這有時(shí)也可能會(huì )用得上,例如,當基類(lèi)定義在另一個(gè)模塊中的時(shí)候:

class DerivedClassName(modname.BaseClassName):

派生類(lèi)定義的執行過(guò)程與基類(lèi)相同。 當構造類(lèi)對象時(shí),基類(lèi)會(huì )被記住。 此信息將被用來(lái)解析屬性引用:如果請求的屬性在類(lèi)中找不到,搜索將轉往基類(lèi)中進(jìn)行查找。 如果基類(lèi)本身也派生自其他某個(gè)類(lèi),則此規則將被遞歸地應用。

派生類(lèi)的實(shí)例化沒(méi)有任何特殊之處: DerivedClassName() 會(huì )創(chuàng )建該類(lèi)的一個(gè)新實(shí)例。 方法引用將按以下方式解析:搜索相應的類(lèi)屬性,如有必要將按基類(lèi)繼承鏈逐步向下查找,如果產(chǎn)生了一個(gè)函數對象則方法引用就生效。

派生類(lèi)可能會(huì )重寫(xiě)其基類(lèi)的方法。 因為方法在調用同一對象的其他方法時(shí)沒(méi)有特殊權限,所以調用同一基類(lèi)中定義的另一方法的基類(lèi)方法最終可能會(huì )調用覆蓋它的派生類(lèi)的方法。 (對 C++ 程序員的提示:Python 中所有的方法實(shí)際上都是 virtual 方法。)

在派生類(lèi)中的重載方法實(shí)際上可能想要擴展而非簡(jiǎn)單地替換同名的基類(lèi)方法。 有一種方式可以簡(jiǎn)單地直接調用基類(lèi)方法:即調用 BaseClassName.methodname(self, arguments)。 有時(shí)這對客戶(hù)端來(lái)說(shuō)也是有用的。 (請注意僅當此基類(lèi)可在全局作用域中以 BaseClassName 的名稱(chēng)被訪(fǎng)問(wèn)時(shí)方可使用此方式。)

Python有兩個(gè)內置函數可被用于繼承機制:

  • 使用 isinstance() 來(lái)檢查一個(gè)實(shí)例的類(lèi)型: isinstance(obj, int) 僅會(huì )在 obj.__class__int 或某個(gè)派生自 int 的類(lèi)時(shí)為 True。

  • 使用 issubclass() 來(lái)檢查類(lèi)的繼承關(guān)系: issubclass(bool, int)True,因為 boolint 的子類(lèi)。 但是,issubclass(float, int)False,因為 float 不是 int 的子類(lèi)。

9.5.1. 多重繼承?

Python 也支持一種多重繼承。 帶有多個(gè)基類(lèi)的類(lèi)定義語(yǔ)句如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

對于多數應用來(lái)說(shuō),在最簡(jiǎn)單的情況下,你可以認為搜索從父類(lèi)所繼承屬性的操作是深度優(yōu)先、從左至右的,當層次結構中存在重疊時(shí)不會(huì )在同一個(gè)類(lèi)中搜索兩次。 因此,如果某一屬性在 DerivedClassName 中未找到,則會(huì )到 Base1 中搜索它,然后(遞歸地)到 Base1 的基類(lèi)中搜索,如果在那里未找到,再到 Base2 中搜索,依此類(lèi)推。

真實(shí)情況比這個(gè)更復雜一些;方法解析順序會(huì )動(dòng)態(tài)改變以支持對 super() 的協(xié)同調用。 這種方式在某些其他多重繼承型語(yǔ)言中被稱(chēng)為后續方法調用,它比單繼承型語(yǔ)言中的 super 調用更強大。

動(dòng)態(tài)改變順序是有必要的,因為所有多重繼承的情況都會(huì )顯示出一個(gè)或更多的菱形關(guān)聯(lián)(即至少有一個(gè)父類(lèi)可通過(guò)多條路徑被最底層類(lèi)所訪(fǎng)問(wèn))。 例如,所有類(lèi)都是繼承自 object,因此任何多重繼承的情況都提供了一條以上的路徑可以通向 object。 為了確?;?lèi)不會(huì )被訪(fǎng)問(wèn)一次以上,動(dòng)態(tài)算法會(huì )用一種特殊方式將搜索順序線(xiàn)性化, 保留每個(gè)類(lèi)所指定的從左至右的順序,只調用每個(gè)父類(lèi)一次,并且保持單調(即一個(gè)類(lèi)可以被子類(lèi)化而不影響其父類(lèi)的優(yōu)先順序)。 總而言之,這些特性使得設計具有多重繼承的可靠且可擴展的類(lèi)成為可能。 要了解更多細節,請參閱 https://www.python.org/download/releases/2.3/mro/。

9.6. 私有變量?

那種僅限從一個(gè)對象內部訪(fǎng)問(wèn)的“私有”實(shí)例變量在 Python 中并不存在。 但是,大多數 Python 代碼都遵循這樣一個(gè)約定:帶有一個(gè)下劃線(xiàn)的名稱(chēng) (例如 _spam) 應該被當作是 API 的非公有部分 (無(wú)論它是函數、方法或是數據成員)。 這應當被視為一個(gè)實(shí)現細節,可能不經(jīng)通知即加以改變。

由于存在對于類(lèi)私有成員的有效使用場(chǎng)景(例如避免名稱(chēng)與子類(lèi)所定義的名稱(chēng)相沖突),因此存在對此種機制的有限支持,稱(chēng)為 名稱(chēng)改寫(xiě)。 任何形式為 __spam 的標識符(至少帶有兩個(gè)前綴下劃線(xiàn),至多一個(gè)后綴下劃線(xiàn))的文本將被替換為 _classname__spam,其中 classname 為去除了前綴下劃線(xiàn)的當前類(lèi)名稱(chēng)。 這種改寫(xiě)不考慮標識符的句法位置,只要它出現在類(lèi)定義內部就會(huì )進(jìn)行。

名稱(chēng)改寫(xiě)有助于讓子類(lèi)重載方法而不破壞類(lèi)內方法調用。例如:

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 標識符的情況下也不會(huì )出錯,因為它會(huì )在 Mapping 類(lèi)中被替換為 _Mapping__update 而在 MappingSubclass 類(lèi)中被替換為 _MappingSubclass__update。

請注意,改寫(xiě)規則的設計主要是為了避免意外沖突;訪(fǎng)問(wèn)或修改被視為私有的變量仍然是可能的。這在特殊情況下甚至會(huì )很有用,例如在調試器中。

請注意傳遞給 exec()eval() 的代碼不會(huì )將發(fā)起調用類(lèi)的類(lèi)名視作當前類(lèi);這類(lèi)似于 global 語(yǔ)句的效果,因此這種效果僅限于同時(shí)經(jīng)過(guò)字節碼編譯的代碼。 同樣的限制也適用于 getattr(), setattr()delattr(),以及對于 __dict__ 的直接引用。

9.7. 雜項說(shuō)明?

有時(shí)會(huì )需要使用類(lèi)似于 Pascal 的“record”或 C 的“struct”這樣的數據類(lèi)型,將一些命名數據項捆綁在一起。 這種情況適合定義一個(gè)空類(lèi):

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

一段需要特定抽象數據類(lèi)型的 Python 代碼往往可以被傳入一個(gè)模擬了該數據類(lèi)型的方法的類(lèi)作為替代。 例如,如果你有一個(gè)基于文件對象來(lái)格式化某些數據的函數,你可以定義一個(gè)帶有 read()readline() 方法從字符串緩存獲取數據的類(lèi),并將其作為參數傳入。

實(shí)例方法對象也具有屬性: m.__self__ 就是帶有 m() 方法的實(shí)例對象,而 m.__func__ 則是該方法所對應的函數對象。

9.8. 迭代器?

到目前為止,您可能已經(jīng)注意到大多數容器對象都可以使用 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='')

這種訪(fǎng)問(wèn)風(fēng)格清晰、簡(jiǎn)潔又方便。 迭代器的使用非常普遍并使得 Python 成為一個(gè)統一的整體。 在幕后,for 語(yǔ)句會(huì )在容器對象上調用 iter()。 該函數返回一個(gè)定義了 __next__() 方法的迭代器對象,此方法將逐一訪(fǎng)問(wèn)容器中的元素。 當元素用盡時(shí),__next__() 將引發(fā) StopIteration 異常來(lái)通知終止 for 循環(huán)。 你可以使用 next() 內置函數來(lái)調用 __next__() 方法;這個(gè)例子顯示了它的運作方式:

>>>
>>> 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é)議的幕后機制,給你的類(lèi)添加迭代器行為就很容易了。 定義一個(gè) __iter__() 方法來(lái)返回一個(gè)帶有 __next__() 方法的對象。 如果類(lèi)已定義了 __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)單而強大的工具。 它們的寫(xiě)法類(lèi)似于標準的函數,但當它們要返回數據時(shí)會(huì )使用 yield 語(yǔ)句。 每次在生成器上調用 next() 時(shí),它會(huì )從上次離開(kāi)的位置恢復執行(它會(huì )記住上次執行語(yǔ)句時(shí)的所有數據值)。 一個(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)完成的操作同樣可以用前一節所描述的基于類(lèi)的迭代器來(lái)完成。 但生成器的寫(xiě)法更為緊湊,因為它會(huì )自動(dòng)創(chuàng )建 __iter__()__next__() 方法。

另一個(gè)關(guān)鍵特性在于局部變量和執行狀態(tài)會(huì )在每次調用之間自動(dòng)保存。 這使得該函數相比使用 self.indexself.data 這種實(shí)例變量的方式更易編寫(xiě)且更為清晰。

除了會(huì )自動(dòng)創(chuàng )建方法和保存程序狀態(tài),當生成器終結時(shí),它們還會(huì )自動(dòng)引發(fā) StopIteration。 這些特性結合在一起,使得創(chuàng )建迭代器能與編寫(xiě)常規函數一樣容易。

9.10. 生成器表達式?

某些簡(jiǎn)單的生成器可以寫(xiě)成簡(jiǎn)潔的表達式代碼,所用語(yǔ)法類(lè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']

備注

1

存在一個(gè)例外。 模塊對象有一個(gè)秘密的只讀屬性 __dict__,它返回用于實(shí)現模塊命名空間的字典;__dict__ 是屬性但不是全局名稱(chēng)。 顯然,使用這個(gè)將違反命名空間實(shí)現的抽象,應當僅被用于事后調試器之類(lèi)的場(chǎng)合。