电竞比分网-中国电竞赛事及体育赛事平台

分享

9. 類

 xrobinson 2015-01-27

9. 類?

與其他編程語言相比,Python 的類機(jī)制用最少的語法和語義引入了類。它是 C++ 和 Modula-3 類機(jī)制的混合。Python 的類提供了面向?qū)ο缶幊痰乃袠?biāo)準(zhǔn)功能: 類繼承機(jī)制允許有多個(gè)基類,繼承的類可以覆蓋其基類或類的任何方法,方法能夠以相同的名稱調(diào)用基類中的方法。對象可以包含任意數(shù)量和種類的數(shù)據(jù)。和模塊一樣,類同樣具有 Python 的動(dòng)態(tài)性質(zhì):它們在運(yùn)行時(shí)創(chuàng)建,并可以在創(chuàng)建之后進(jìn)一步修改。

用 C++ 術(shù)語來講,通常情況下類成員(包括數(shù)據(jù)成員)是公有的(其它情況見下文私有變量),所有的成員函數(shù)都是 的。與 Modula-3 一樣,在成員方法中沒有簡便的方式引用對象的成員:方法函數(shù)的聲明用顯式的第一個(gè)參數(shù)表示對象本身,調(diào)用時(shí)會隱式地引用該對象。與 Smalltalk 一樣,類本身也是對象。這給導(dǎo)入類和重命名類提供了語義上的合理性。與 C++ 和 Modula-3 不同,用戶可以用內(nèi)置類型作為基類進(jìn)行擴(kuò)展。此外,像 C++ 一樣,類實(shí)例可以重定義大多數(shù)帶有特殊語法的內(nèi)置操作符(算術(shù)運(yùn)算符、 下標(biāo)等)。

(由于沒有統(tǒng)一的達(dá)成共識的術(shù)語,我會偶爾使用 SmallTalk 和 C++ 的術(shù)語。我比較喜歡用 Modula-3 的術(shù)語,因?yàn)楸绕?C++,Python 的面向?qū)ο笳Z法更像它,但是我想很少有讀者聽說過它。)

9.1. 名稱和對象?

對象是獨(dú)立的,多個(gè)名字(在多個(gè)作用域中)可以綁定到同一個(gè)對象。這在其他語言中稱為別名。第一次粗略瀏覽 Python 時(shí)經(jīng)常不會注意到這個(gè)特性,而且處理不可變的基本類型(數(shù)字,字符串,元組)時(shí)忽略這一點(diǎn)也沒什么問題。然而, 在Python 代碼涉及可變對象如列表、 字典和大多數(shù)其它類型時(shí),別名可能具有意想不到語義效果。這通用有助于優(yōu)化程序,因?yàn)閯e名的行為在某些方面類似指針。例如,傳遞一個(gè)對象的開銷是很小的,因?yàn)樵趯?shí)現(xiàn)上只是傳遞了一個(gè)指針;如果函數(shù)修改了參數(shù)傳遞的對象,調(diào)用者也將看到變化 —— 這就避免了類似 Pascal 中需要兩個(gè)不同參數(shù)的傳遞機(jī)制。

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

在介紹類之前,我首先要告訴你一些有關(guān) Python 作用域的的規(guī)則。類的定義非常巧妙的運(yùn)用了命名空間,要完全理解接下來的知識,需要先理解作用域和命名空間的工作原理。另外,這一切的知識對于任何高級 Python 程序員都非常有用。

讓我們從一些定義開始。

命名空間是從名稱到對象的映射。當(dāng)前命名空間主要是通過 Python 字典實(shí)現(xiàn)的,不過通常不會引起任何關(guān)注(除了性能方面),它以后也有可能會改變。以下有一些命名空間的例子:內(nèi)置名稱集(包括函數(shù)名列如abs()和內(nèi)置異常的名稱);模塊中的全局名稱;函數(shù)調(diào)用中的局部名稱。在某種意義上的一組對象的屬性也形成一個(gè)命名空間。關(guān)于命名空間需要知道的重要一點(diǎn)是不同命名空間的名稱絕對沒有任何關(guān)系;例如,兩個(gè)不同模塊可以都定義函數(shù)maximize而不會產(chǎn)生混淆 —— 模塊的使用者必須以模塊名為前綴引用它們。

順便說一句,我使用屬性 這個(gè)詞稱呼點(diǎn)后面的任何名稱 —— 例如,在表達(dá)式z.real中,realz對象的一個(gè)屬性。嚴(yán)格地說,對模塊中的名稱的引用是屬性引用:在表達(dá)式modname.funcname中, modname是一個(gè)模塊對象,funcname是它的一個(gè)屬性。在這種情況下,模塊的屬性和模塊中定義的全局名稱之間碰巧是直接的映射:它們共享同一命名空間 ![1]

屬性可以是只讀的也可以是可寫的。在后一種情況下,可以對屬性賦值。模塊的屬性都是可寫的:你可以這樣寫modname.the_answer = 42??蓪懙膶傩砸部梢杂?a href="http://python./python_341/tutorial/../reference/simple_stmts.html#del">del語句刪除。例如,del modname.the_answer將會刪除對象modname中的the_answer屬性。

各個(gè)命名空間創(chuàng)建的時(shí)刻是不一樣的,且有著不同的生命周期。包含內(nèi)置名稱的命名空間在 Python 解釋器啟動(dòng)時(shí)創(chuàng)建,永遠(yuǎn)不會被刪除。模塊的全局命名空間在讀入模塊定義時(shí)創(chuàng)建;通常情況下,模塊命名空間也會一直保存到解釋器退出。在解釋器最外層調(diào)用執(zhí)行的語句,不管是從腳本文件中讀入還是來自交互式輸入,都被當(dāng)作模塊__main__的一部分,所以它們有它們自己的全局命名空間。(內(nèi)置名稱實(shí)際上也存在于一個(gè)模塊中,這個(gè)模塊叫builtins。)

函數(shù)的局部命名空間在函數(shù)調(diào)用時(shí)創(chuàng)建,在函數(shù)返回或者引發(fā)了一個(gè)函數(shù)內(nèi)部沒有處理的異常時(shí)刪除。(實(shí)際上,用遺忘來形容到底發(fā)生了什么更為貼切。)當(dāng)然,每個(gè)遞歸調(diào)用有它們自己的局部命名空間。

作用域 是 Python 程序中可以直接訪問一個(gè)命名空間的代碼區(qū)域。這里的“直接訪問”的意思是用沒有前綴的引用在命名空間中找到的相應(yīng)的名稱。

雖然作用域的確定是靜態(tài)地,但它們的使用是動(dòng)態(tài)地。程序執(zhí)行過程中的任何時(shí)候,至少有三個(gè)嵌套的作用域,它們的命名空間是可以直接訪問的:

  • 首先搜索最里面包含局部命名的作用域
  • 其次搜索所有調(diào)用函數(shù)的作用域,從最內(nèi)層調(diào)用函數(shù)的作用域開始,它們包含非局部但也非全局的命名
  • 倒數(shù)第二個(gè)搜索的作用域是包含當(dāng)前模塊全局命名的作用域
  • 最后搜索的作用域是最外面包含內(nèi)置命名的命名空間

如果一個(gè)命名聲明為全局的,那么對它的所有引用和賦值會直接搜索包含這個(gè)模塊全局命名的作用域。如果要重新綁定最里層作用域之外的變量,可以使用nonlocal語句;如果不聲明為nonlocal,這些變量將是只讀的(對這樣的變量賦值會在最里面的作用域創(chuàng)建一個(gè) 的局部變量,外部具有相同命名的那個(gè)變量不會改變)。

通常情況下,局部作用域引用當(dāng)前函數(shù)的本地命名。函數(shù)之外,局部作用域引用的命名空間與全局作用域相同:模塊的命名空間。類定義在局部命名空間中創(chuàng)建了另一個(gè)命名空間。

認(rèn)識到作用域是由代碼確定的是非常重要的:函數(shù)的全局作用域是函數(shù)的定義所在的模塊的命名空間,與函數(shù)調(diào)用的位置或者別名無關(guān)。另一方面,命名的實(shí)際搜索過程是動(dòng)態(tài)的,在運(yùn)行時(shí)確定的——然而,Python 語言也在不斷發(fā)展,以后有可能會成為靜態(tài)的“編譯”時(shí)確定,所以不要依賴動(dòng)態(tài)解析?。ㄊ聦?shí)上,本地變量是已經(jīng)確定靜態(tài)。)

Python的一個(gè)特別之處在于——如果沒有使用global語法——其賦值操作總是在最里層的作用域。賦值不會復(fù)制數(shù)據(jù)——只是將命名綁定到對象。刪除也是如此:del x只是從局部作用域的命名空間中刪除命名x。事實(shí)上,所有引入新命名的操作都作用于局部作用域: 特別是import語句和函數(shù)定將模塊名或函數(shù)綁定于局部作用域。(可以使用   Global 語句將變量引入到全局作用域。)

global語句可以用來指明某個(gè)特定的表明位于全局作用域并且應(yīng)該在那里重新綁定;nonlocal語句表示特定的變量位于一個(gè)封閉的作用域并且應(yīng)該在那里重新綁定。

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

下面這個(gè)示例演示如何訪問不同作用域和命名空間,以及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 spams.
In global scope: global spam

注意,local賦值(默認(rèn)行為)沒有改變scope_testspam的綁定。nonlocal賦值改變了scope_testspam 的綁定,global賦值改變了模塊級別的綁定。

你也可以看到在global語句之前沒有對spam的綁定。

9.3. 初識類?

類引入了少量的新語法、三種新對象類型和一些新語義。

9.3.1. 類定義語法?

類定義的最簡單形式如下所示:

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

類的定義就像函數(shù)定義(def語句),要先執(zhí)行才能生效。(你當(dāng)然可以把它放進(jìn)if語句的某一分支,或者一個(gè)函數(shù)的內(nèi)部。)

實(shí)際應(yīng)用中,類定義包含的語句通常是函數(shù)定義,不過其它語句也是可以的而且有時(shí)還會很有用——后面我們會再回來討論。類中的函數(shù)定義通常有一個(gè)特殊形式的參數(shù)列表,這是由方法調(diào)用的協(xié)議決定的——同樣后面會解釋這些。

進(jìn)入類定義部分后,會創(chuàng)建出一個(gè)新的命名空間,作為局部作用域——因此,所有的賦值成為這個(gè)新命名空間的局部變量。特別是這里的函數(shù)定義會綁定新函數(shù)的名字。

類定義正常退出時(shí),一個(gè)類對象也就創(chuàng)建了?;旧纤菍︻惗x創(chuàng)建的命名空間進(jìn)行了一個(gè)包裝;我們在下一節(jié)將進(jìn)一步學(xué)習(xí)類對象的知識。原始的局部作用域(類定義引入之前生效的那個(gè))得到恢復(fù),類對象在這里綁定到類定義頭部的類名(例子中是ClassName)。

9.3.2. 類對象?

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

屬性引用使用的所有屬性引用在 Python 中使用的標(biāo)準(zhǔn)語法: obj.name。有效的屬性名稱是在該類的命名空間中的類對象被創(chuàng)建時(shí)的所有名稱。因此,如果類定義看起來像這樣:

class MyClass:
    """A simple example class"""
    i = 12345
    def f(self):
        return 'hello world'

那么 MyClass.iMyClass.f 是有效的屬性引用,分別返回一個(gè)整數(shù)和一個(gè)方法對象。也可以對類屬性賦值,你可以通過給 MyClass.i 賦值來修改它。__doc__ 也是一個(gè)有效的屬性,返回類的文檔字符串: "A simple example class"。

類的實(shí)例化 使用函數(shù)的符號??梢约僭O(shè)類對象是一個(gè)不帶參數(shù)的函數(shù),該函數(shù)返回這個(gè)類的一個(gè)新的實(shí)例。例如(假設(shè)沿用上面的類):

x = MyClass()

創(chuàng)建這個(gè)類的一個(gè)新實(shí)例,并將該對象賦給局部變量x。

實(shí)例化操作(“調(diào)用”一個(gè)類對象)將創(chuàng)建一個(gè)空對象。很多類希望創(chuàng)建的對象可以自定義一個(gè)初始狀態(tài)。因此類可以定義一個(gè)名為__init__()的特殊方法,像下面這樣:

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

當(dāng)類定義了__init__()方法,類的實(shí)例化會為新創(chuàng)建的類實(shí)例自動(dòng)調(diào)用__init__()。所以在下面的示例中,可以獲得一個(gè)新的、已初始化的實(shí)例:

x = MyClass()

當(dāng)然,__init__()方法可以帶有參數(shù),這將帶來更大的靈活性。在這種情況下,類實(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. 實(shí)例對象?

現(xiàn)在我們可以用實(shí)例對象做什么?實(shí)例對象唯一可用的操作就是屬性引用。有兩種有效的屬性名:數(shù)據(jù)屬性和方法。

數(shù)據(jù)屬性相當(dāng)于 Smalltalk 中的"實(shí)例變量"或 C++ 中的"數(shù)據(jù)成員"。數(shù)據(jù)屬性不需要聲明;和局部變量一樣,它們會在第一次給它們賦值時(shí)生成。例如,如果x是上面創(chuàng)建的MyClass的實(shí)例,下面的代碼段將打印出值16而不會出現(xiàn)錯(cuò)誤:

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

實(shí)例屬性引用的另一種類型是方法。方法是"屬于"一個(gè)對象的函數(shù)。(在 Python,方法這個(gè)術(shù)語不只針對類實(shí)例:其他對象類型也可以具有方法。例如,列表對象有 append、insert、remove、sort 方法等等。但是在后面的討論中,除非明確說明,我們提到的方法特指類實(shí)例對象的方法。)

實(shí)例對象的方法的有效名稱依賴于它的類。根據(jù)定義,類中所有函數(shù)對象的屬性定義了其實(shí)例中相應(yīng)的方法。所以在我們的示例中, x.f是一個(gè)有效的方法的引用,因?yàn)?tt>MyClass.f是一個(gè)函數(shù),但x.i不是,因?yàn)?tt>MyClass.i不是一個(gè)函數(shù)。但x.fMyClass.f也不是一回事 —— 它是一個(gè)方法對象,不是一個(gè)函數(shù)對象。

9.3.4. 方法對象?

通常情況下,方法在綁定之后被直接調(diào)用:

x.f()

MyClass的示例中,這將返回字符串'hello world'。然而,也不是一定要直接調(diào)用方法: x.f是一個(gè)方法對象,可以存儲起來以后調(diào)用。例如:

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

會不斷地打印hello world。

調(diào)用方法時(shí)到底發(fā)生了什么?你可能已經(jīng)注意到,上面x.f()的調(diào)用沒有參數(shù),即使f ()函數(shù)的定義指定了一個(gè)參數(shù)。該參數(shù)發(fā)生了什么問題?當(dāng)然如果函數(shù)調(diào)用中缺少參數(shù) Python 會拋出異常——即使這個(gè)參數(shù)實(shí)際上沒有使用……

實(shí)際上,你可能已經(jīng)猜到了答案:方法的特別之處在于實(shí)例對象被作為函數(shù)的第一個(gè)參數(shù)傳給了函數(shù)。在我們的示例中,調(diào)用x.f()完全等同于MyClass.f(x)。一般情況下,以n 個(gè)參數(shù)的列表調(diào)用一個(gè)方法就相當(dāng)于將方法所屬的對象插入到列表的第一個(gè)參數(shù)的前面,然后以新的列表調(diào)用相應(yīng)的函數(shù)。

如果你還是不明白方法的工作原理,了解一下它的實(shí)現(xiàn)或許有幫助。引用非數(shù)據(jù)屬性的實(shí)例屬性時(shí),會搜索它的類。如果這個(gè)命名確認(rèn)為一個(gè)有效的函數(shù)對象類屬性,就會將實(shí)例對象和函數(shù)對象封裝進(jìn)一個(gè)抽象對象:這就是方法對象。以一個(gè)參數(shù)列表調(diào)用方法對象時(shí),它被重新拆封,用實(shí)例對象和原始的參數(shù)列表構(gòu)造一個(gè)新的參數(shù)列表,然后函數(shù)對象調(diào)用這個(gè)新的參數(shù)列表。

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

一般來說,實(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'

正如在名稱和對象討論的,可變對象,例如列表和字典,的共享數(shù)據(jù)可能帶來意外的效果。例如,下面代碼中的tricks 列表不應(yīng)該用作類變量,因?yàn)樗械?em>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']

這個(gè)類的正確設(shè)計(jì)應(yīng)該使用一個(gè)實(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ǔ)充說明?

數(shù)據(jù)屬性會覆蓋同名的方法屬性;為了避免意外的命名沖突,這在大型程序中可能帶來極難發(fā)現(xiàn)的 bug,使用一些約定來減少?zèng)_突的機(jī)會是明智的??赡艿募s定包括大寫方法名稱的首字母,使用一個(gè)唯一的小寫的字符串(也許只是一個(gè)下劃線)作為數(shù)據(jù)屬性名稱的前綴,或者方法使用動(dòng)詞而數(shù)據(jù)屬性使用名詞。

數(shù)據(jù)屬性可以被方法引用,也可以由一個(gè)對象的普通用戶(“客戶端”)使用。換句話說,類是不能用來實(shí)現(xiàn)純抽象數(shù)據(jù)類型。事實(shí)上,Python 中不可能強(qiáng)制隱藏?cái)?shù)據(jù)——一切基于約定。(另一方面,如果需要,使用 C 編寫的 Python 實(shí)現(xiàn)可以完全隱藏實(shí)現(xiàn)細(xì)節(jié)并控制對象的訪問;這可以用來通過 C 語言擴(kuò)展 Python。)

客戶應(yīng)該謹(jǐn)慎的使用數(shù)據(jù)屬性——客戶可能通過踐踏他們的數(shù)據(jù)屬性而使那些由方法維護(hù)的常量變得混亂。注意:只要能避免沖突,客戶可以向一個(gè)實(shí)例對象添加他們自己的數(shù)據(jù)屬性,而不會影響方法的正確性——再次強(qiáng)調(diào),命名約定可以避免很多麻煩。

從方法內(nèi)部引用數(shù)據(jù)屬性(或其他方法)并沒有快捷方式。我覺得這實(shí)際上增加了方法的可讀性:當(dāng)瀏覽一個(gè)方法時(shí),在局部變量和實(shí)例變量之間不會出現(xiàn)令人費(fèi)解的情況。

通常,方法的第一個(gè)參數(shù)稱為self。這僅僅是一個(gè)約定:名字self對 Python 而言絕對沒有任何特殊含義。但是請注意:如果不遵循這個(gè)約定,對其他的 Python 程序員而言你的代碼可讀性就會變差,而且有些類 查看 器程序也可能是遵循此約定編寫的。

類屬性的任何函數(shù)對象都為那個(gè)類的實(shí)例定義了一個(gè)方法。函數(shù)定義代碼不一定非得定義在類中:也可以將一個(gè)函數(shù)對象賦值給類中的一個(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、 gh都是類C中引用函數(shù)對象的屬性,因此它們都是c的實(shí)例的方法 —— h完全等同于g。請注意,這種做法通常只會混淆程序的讀者。

通過使用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)

方法可以像普通函數(shù)那樣引用全局命名。與方法關(guān)聯(lián)的全局作用域是包含類定義的模塊。(類本身永遠(yuǎn)不會做為全局作用域使用。)盡管很少有好的理由在方法中使用全局?jǐn)?shù)據(jù),全局作用域確有很多合法的用途:其一是方法可以調(diào)用導(dǎo)入全局作用域的函數(shù)和方法,也可以調(diào)用定義在其中的類和函數(shù)。通常,包含此方法的類也會定義在這個(gè)全局作用域,在下一節(jié)我們會了解為何一個(gè)方法要引用自己的類。

每個(gè)值是都一個(gè)對象,因此每個(gè)值都有一個(gè)(也稱它的類型)。它存儲為object.__class__。

9.5. 繼承?

當(dāng)然,一個(gè)語言特性不支持繼承是配不上“類”這個(gè)名字的。派生類定義的語法如下所示:

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

BaseClassName必須與派生類定義在一個(gè)作用域內(nèi)。用其他任意表達(dá)式代替基類的名稱也是允許的。這可以是有用的,例如,當(dāng)基類定義在另一個(gè)模塊中時(shí):

class DerivedClassName(modname.BaseClassName):

派生類定義的執(zhí)行過程和基類是相同的。類對象創(chuàng)建后,基類會被保存。這用于解析屬性的引用:如果在類中找不到請求的屬性,搜索會在基類中繼續(xù)。如果基類本身是由別的類派生而來,這個(gè)規(guī)則會遞歸應(yīng)用。

派生類的實(shí)例化沒有什么特殊之處:DerivedClassName()創(chuàng)建類的一個(gè)新的實(shí)例。方法的引用按如下規(guī)則解析: 搜索對應(yīng)的類的屬性,必要時(shí)沿基類鏈逐級搜索,如果找到了函數(shù)對象這個(gè)方法引用就是合法的。

派生的類可能重寫其基類的方法。因?yàn)榉椒ㄕ{(diào)用同一個(gè)對象中的其它方法時(shí)沒有特權(quán),基類的方法調(diào)用同一個(gè)基類的方法時(shí),可能實(shí)際上最終調(diào)用了派生類中的覆蓋方法。(對于 C++ 程序員:Python 中的所有方法實(shí)際上都是的。)

派生類中的覆蓋方法可能是想要擴(kuò)充而不是簡單的替代基類中的重名方法。有一個(gè)簡單的方法可以直接調(diào)用基類方法:只要調(diào)用BaseClassName.methodname(self, arguments)。有時(shí)這對于客戶端也很有用。(要注意只有BaseClassName在同一全局作用域定義或?qū)霑r(shí)才能這樣用。)

Python 有兩個(gè)用于繼承的函數(shù):

  • 使用isinstance()來檢查實(shí)例類型:isinstance(obj, int)只有obj.__class__int或者是從int派生的類時(shí)才為True。
  • 使用issubclass()來檢查類的繼承: issubclas(bool, int)True因?yàn)?a href="http://python./python_341/tutorial/../library/functions.html#bool" title="bool">boolint的子類。然而,issubclass(float, int)False,因?yàn)?a href="http://python./python_341/tutorial/../library/functions.html#float" title="float">float不是int的子類。

9.5.1. 多繼承?

Python 也支持一種形式的多繼承。具有多個(gè)基類的類定義如下所示:

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

對于大多數(shù)用途,在最簡單的情況下,你可以認(rèn)為繼承自父類的屬性搜索是從左到右的深度優(yōu)先搜索,不會在同一個(gè)類中搜索兩次,即使層次會有重疊。因此,如果在DerivedClassName中找不到屬性,它搜索Base1,然后(遞歸)基類中的Base1,如果沒有找到,它會搜索base2,依此類推。

事實(shí)要稍微復(fù)雜一些;為了支持合作調(diào)用super(),方法解析的順序會動(dòng)態(tài)改變。這種方法在某些其它多繼承的語言中也有并叫做call-next-method,它比單繼承語言中的super調(diào)用更強(qiáng)大。

動(dòng)態(tài)調(diào)整順序是必要的,因?yàn)樗械亩嗬^承都會有一個(gè)或多個(gè)菱形關(guān)系(從最底部的類向上,至少會有一個(gè)父類可以通過多條路徑訪問到)。例如,所有的類都繼承自object,所以任何多繼承都會有多條路徑到達(dá)object。為了防止基類被重復(fù)訪問,動(dòng)態(tài)算法線性化搜索順序,每個(gè)類都按從左到右的順序特別指定了順序,每個(gè)父類只調(diào)用一次,這是單調(diào)的(也就是說一個(gè)類被繼承時(shí)不會影響它祖先的次序)。所有這些特性使得設(shè)計(jì)可靠并且可擴(kuò)展的多繼承類成為可能。有關(guān)詳細(xì)信息,請參閱http://www./download/releases/2.3/mro/

9.6. 私有變量?

在 Python 中不存在只能從對像內(nèi)部訪問的“私有”實(shí)例變量。然而,有一項(xiàng)大多數(shù) Python 代碼都遵循的公約:帶有下劃線(例如_spam)前綴的名稱應(yīng)被視為非公開的 API 的一部分(無論是函數(shù)、 方法還是數(shù)據(jù)成員)。它應(yīng)該被當(dāng)做一個(gè)實(shí)現(xiàn)細(xì)節(jié),將來如果有變化恕不另行通知。

因?yàn)橛幸粋€(gè)合理的類私有成員的使用場景(即為了避免名稱與子類定義的名稱沖突),Python 對這種機(jī)制有簡單的支持,叫做name mangling。__spam 形式的任何標(biāo)識符(前面至少兩個(gè)下劃線,后面至多一個(gè)下劃線)將被替換為_classname__spamclassname是當(dāng)前類的名字。此重整是做而不考慮該標(biāo)識符的句法位置,只要它出現(xiàn)在類的定義的范圍內(nèi)。

Name mangling 有利于子類重寫父類的方法而不會破壞類內(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)

請注意 mangling 規(guī)則的目的主要是避免發(fā)生意外;訪問或者修改私有變量仍然是可能的。這在特殊情況下,例如調(diào)試的時(shí)候,還是有用的。

注意傳遞給execeval()的代碼沒有考慮要將調(diào)用類的類名當(dāng)作當(dāng)前類;這類似于global語句的效果,影響只限于一起進(jìn)行字節(jié)編譯的代碼。相同的限制適用于getattr() setattr()delattr(),以及直接引用__dict__時(shí)。

9.7. 零碎的說明?

有時(shí)候類似于Pascal 的"record" 或 C 的"struct"的數(shù)據(jù)類型很有用,它們把幾個(gè)已命名的數(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

某一段 Python 代碼需要一個(gè)特殊的抽象數(shù)據(jù)結(jié)構(gòu)的話,通??梢詡魅胍粋€(gè)類來模擬該數(shù)據(jù)類型的方法。例如,如果你有一個(gè)用于從文件對象中格式化數(shù)據(jù)的函數(shù),你可以定義一個(gè)帶有read ()readline () 方法的類,以此從字符串緩沖讀取數(shù)據(jù),然后將該類的對象作為參數(shù)傳入前述的函數(shù)。

實(shí)例的方法對象也有屬性:m.__self__是具有方法m()的實(shí)例對象,m.__func__是方法的函數(shù)對象。

9.8. 異常也是類?

用戶定義的異常類也由類標(biāo)識。利用這個(gè)機(jī)制可以創(chuàng)建可擴(kuò)展的異常層次。

raise語句有兩種新的有效的(語義上的)形式:

raise Class

raise Instance

第一種形式中,Class 必須是type或者它的子類的一個(gè)實(shí)例。第一種形式是一種簡寫:

raise Class()

except子句中的類如果與異常是同一個(gè)類或者是其基類,那么它們就是相容的(但是反過來是不行的——except子句列出的子類與基類是不相容的)。例如,下面的代碼將按該順序打印 B、 C、 D:

class B(Exception):
    pass
class C(B):
    pass
class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

請注意,如果except 子句的順序倒過來 (excpet B在最前面),它就會打印B,B,B —— 第一個(gè)匹配的異常被觸發(fā)。

打印一個(gè)異常類的錯(cuò)誤信息時(shí),先打印類名,然后是一個(gè)空格、一個(gè)冒號,然后是用內(nèi)置函數(shù)str()將類轉(zhuǎn)換得到的完整字符串。

9.9. 迭代器?

現(xiàn)在你可能注意到大多數(shù)容器對象都可以用for遍歷:

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 中普遍而且統(tǒng)一。在后臺, for語句調(diào)用容器對象上的iter() 。該函數(shù)返回一個(gè)定義了__next__()方法的迭代器對象,它在容器中逐一訪問元素。沒有后續(xù)的元素時(shí),__next__()會引發(fā)StopIteration異常,告訴for循環(huán)終止。你可以是用內(nèi)建的next()函數(shù)調(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 ?
    next(it)
StopIteration

看過迭代器協(xié)議背后的機(jī)制后,將很容易將迭代器的行為添加到你的類中。定義一個(gè)__iter__()方法,它使用__next__()方法返回一個(gè)對象。如果類定義了__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.10. 生成器?

生成器是簡單且功能強(qiáng)大的工具,用于創(chuàng)建迭代器。它們寫起來就像是正規(guī)的函數(shù),需要返回?cái)?shù)據(jù)的時(shí)候使用yield語句。每次在它上面調(diào)用next()時(shí),生成器回復(fù)它脫離的位置(它記憶語句最后一次執(zhí)行的位置和所有的數(shù)據(jù)值)。以下示例演示了生成器可以非常簡單地創(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é)所述的基于類的迭代器也能做到。生成器這么簡潔是因?yàn)?a href="http://python./python_341/tutorial/../reference/datamodel.html#object.__iter__" title="object.__iter__">__iter__()__next__()方法是自動(dòng)創(chuàng)建的。

另一個(gè)關(guān)鍵功能是調(diào)用之間自動(dòng)保存的本地變量和執(zhí)行狀態(tài)。這使得該函數(shù)更容易寫,很多更清楚地比使用實(shí)例變量,如self.indexself.data方法。

除了自動(dòng)創(chuàng)建方法和保存程序的狀態(tài),當(dāng)生成器終止時(shí),它們會自動(dòng)引發(fā)StopIteration。在組合中,這些功能可以容易地創(chuàng)建迭代器具有比編寫正則函數(shù)沒有更多的努力。

9.11. 生成器表達(dá)式?

一些簡單的生成器可以作為表達(dá)式使用的語法類似簡潔地編碼列表中體會到,但用而不是括號括號。這些表達(dá)式用于生成器由封閉的功能馬上使用的情況。生成器表達(dá)式更緊湊但比完整生成器定義較不通用,傾向于更多的內(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

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

腳注

[1]Except for one thing. Module objects have a secret read-only attribute called __dict__ which returns the dictionary used to implement the module’s namespace; the name __dict__ is an attribute but not a global name. Obviously, using this violates the abstraction of namespace implementation, and should be restricted to things like post-mortem debuggers.

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多