Python3.4 Tutorial9 - Classes (Part 1)

walkerMK發表於2015-09-21

9 類

    和其他語言相比,Python在建立類的機制(mechanism)上使用最小化的新語法和語義。它融合了C++和Modula-3的機制。Python的類提供了物件導向程式設計的所有標準特性:在類的繼承機制上允許有多個基類(多繼承),一個派生類可以重寫(Override)它所有基類的任何方法,並且一個方法可以呼叫基類的同名方法。物件(Objects)可以包含任意數量和種類的資料(data)。Python中模組和類都具有動態特性(Dynamic Nature):它們在執行時建立,並且能在建立後被修改。

    在C++術語中,通常類的成員(包括資料成員)是public的,並且所有的成員函式是virtual的。和Modula-3一樣,Python中沒有從物件方法中引用物件成員的簡寫方式(這裡是指預設傳入函式的this指標):方法(Method)函式(Function)在宣告時顯示的使用第一個引數代表它的物件,在方法被呼叫時該引數被隱式賦值。和Smalltalk類似,Python中的類本身就是物件。這提供了匯入和重新命名的語義。與C++和Modula-3不同的是,Python的內建(built-in)型別可以作為基類被使用者擴充。此外和C++相同的是,大多數使用特殊語法的內建操作符(算數運算子, 下標操作符等等)可為類的例項重定義。

9.1 名稱(Names)和物件(Objects)的簡述

    物件都具有個別性,多個名稱(在多個作用域下))可以被繫結到同一個物件上。這在其他語言中被稱作別名(Aliasing)。通常第一次接觸Python的人不會注意到這一點,並且在處理不可變的基礎型別(numbers, strings, tuples)時可以被安全的忽略。然而,在涉及到可變物件例如list、dictionary、和大多數其他型別時,別名可能會在Python語義上起到驚人的效果。通常別名對程式設計是有益的,因為在某些方面它表現的像指標。例如,傳遞一個物件的開銷是很小的,因為在實現上是隻傳遞了一個指標;如果函式修改了一個作為引數傳入的物件,呼叫函式的地方可以看到變化(注:函式內的修改是可以帶出函式的)——這樣就不需要Pascal中要傳入兩個不同引數的機制了。

9.2 Python的作用域(Scopes)和名稱空間(Namespaces)

    在介紹類之前,首先講一些Python中作用域的規則。類的定義非常巧妙的運用了名稱空間,為了能夠完全理解究竟發生了什麼,需要知道作用域和名稱空間是怎麼工作的(中間半句可能理解的不太對,給出原文:you need to know how scopes and namespaces work to fully understand whats going on)。順便一提,這方面的知識對任何高階Python程式設計師都是有用的。

接下來看一些定義。

    名稱空間是從名字到物件的對映。目前大多數的名稱空間都是Python中的字典實現的,但是通常都不會注意這些的(除非出於效能考慮),並且將來可能會改變實現方式。一些名稱空間的例子:內建名稱集(包括abs()這樣的函式,還有一些內建的異常名字);模組中的所有全域性名稱;函式呼叫中的所有區域性(local)名稱。從某種意義上講,物件的屬性集也是一個名稱空間。關於名稱空間要了解的重點是,不同名稱空間中的名字是沒有任何關係的;例如,兩個不同的模組中可能都定義一個名為maximize的函式但是不會有衝突——模組的使用者必須使用模組名作為字首來引用。

    順帶一提,我使用屬性(attribute)這個詞來指代所有跟在點(dot)後面的名字——例如在表示式z.real中,real是物件z的一個屬性。嚴格來說,引用模組中的一個名字是對屬性的引用:在表示式modname.funcname 中,modname是一個模組物件,funcname是它的一個屬性。因此模組的屬性和模組中定義的全域性名稱間,擁有直接的對映關係:它們共享同一個名稱空間!

    屬性可能是隻讀的,也可能是可寫的。在下面的例子中,給屬性賦值是可行的。當模組屬性是可寫時:你可以寫modname.the_answer = 42。可寫的屬性也可以被del表示式刪除。例如,del modname.the_answer會將the_answer從名為modname的物件中刪除。

    名稱空間在不同的時刻建立,並且擁有不同的生命週期。包含內建名稱的名稱空間在Python直譯器啟動的時候被建立,並且不會被刪除。某個模組的全域性名稱空間當模組的定義被讀入的時候建立;通常,模組的也是直到直譯器退出的時候刪除。被直譯器頂層呼叫執行的語句,不管是從指令碼檔案中讀入的還是從互動模式中輸入的,都會被當作__main__模組的一部分,所以它們擁有自己的全域性名稱空間。(內建名稱實際上也存在於一個名為builtins的模組中。)

    一個函式的區域性名稱空間在這個函式呼叫的時候被建立,並且在函式返回(return)或引發了一個在函式內不會處理的異常時被刪除。(實際上,比起描述它實際發生了什麼,忽略這個問題是更好的方式。)當然,每個遞迴呼叫(recursive invocation)都有它自己的本地名稱空間。

    作用域是可以直接訪問名稱空間的一個Python程式上的結構區域。“直接訪問”在這裡意味著,沒有字首的引用可以在名稱空間中找到指定的名字。(這裡翻譯的不好,主要意思是不需modname.target這樣的點表示式,可以直接使用target訪問一個名稱,原文是:an unqualified reference to a name attempts to find the name in the namespace.)

    雖然作用域的定義是靜態的,但它們都被動態的使用。在程式執行的任何時刻,至少有三層巢狀的作用域,它們可以直接訪問同一個名稱空間:

  • 最內層的作用域,包含了區域性名稱,也是最先被搜尋的
  • 任意閉包函式作用域,包含了非區域性(non-local)但也非全域性(non-global)的名稱,它從最近的閉包範圍開始被搜尋
  • 倒數第二層(也是中層)作用域,包含當前模組的全域性名稱
  • 最外層作用域(最後被搜尋),包含內建名稱的名稱空間

    如果一個名稱被宣告為全域性的,那麼所有的引用和賦值都直接針對包含模組全域性名稱的中層作用域。為了重新繫結最內層作用域之外的變數,可以使用nonlocal語句;沒有宣告為nonlocal的那些(外部)變數是隻讀的(如果嘗試給這樣的變數賦值,只會簡單的建立一個最內層作用域中的變數,而外層的同名變數不會改變)。

    通常,區域性作用域引用當前函式的區域性名稱。在函式之外,區域性作用域和全域性作用域引用相同的名稱空間:模組的名稱空間。類的定義在區域性作用域的另一個名稱空間中。

    意識到作用域由程式文字結構決定是很重要的:定義在一個模組中的函式的全域性作用域是這個模組的名稱空間,無論函式是在哪裡或以什麼樣的別名被呼叫。另一方面,名稱的實際搜尋過程是執行時動態完成的——然而,語言的定義在向靜態名稱解決方案的方向發展,在“編譯”時完成名稱的搜尋,這樣就不再依賴於動態名稱解決方案了!(事實上,區域性變數已經是靜態決定的了。)

    Python有一個特別的“怪癖”——如果沒有global語句在起作用——給一個名稱賦值,總會進入最內層作用域。賦值不會拷貝資料——它只會將名稱繫結到物件上。刪除操作也是一樣:del x語句從被區域性作用域引用的名稱空間中移除x上的繫結。實際上,所有產生一個新名稱的操作都是使用的區域性作用域:特別是import語句和函式定義將模組或函式名繫結到區域性作用域。

    global語句可以被用來索引全域性作用域中的個別變數,並且將其重新繫結到本地;nonlocal語句索引閉包作用域中的個別變數,並將其重新繫結到本地。

9.2.1 作用域和名稱空間的例子

    用一個例子來演示怎樣引用不同的作用域和名稱空間,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

    注意,區域性(local)賦值(預設情況)不會改變scope_test繫結的spamnonlocal賦值改變了scope_test繫結的spam;並且global賦值改變了模組級別的繫結。

    可以觀察到,在global賦值之前沒有繫結spam

相關文章