python3測試工具開發快速入門教程5類
類將資料和功能捆綁在一起。建立新類時建立新型別的物件,允許建立該型別的新例項。每個類例項都可以附加屬性以保持其狀態。類例項也可以有方法(由類定義)來修改其狀態。
與其他程式語言相比,Python的類機制語法和語義最少。它是C ++和Modula-3中的的混合體。 Python類提供了面向物件程式設計的所有標準功能:類繼承機制支援多個基類,派生類可以重寫其基類方法,並且可以呼叫具有相同名稱的基類的方法。物件可以包含任意數量和種類的資料。與模組一樣,類也具有Python的動態特性:它們是在執行時建立的,並且可以在建立後修改。
在C ++術語中,常規類成員(包括資料成員)是公共的(除了見下面的私有變數),並且所有的成員函式都是虛擬的。和Modula-3一樣,從它的方法中引用物件的成員沒有簡短的方法:方法函式聲明瞭明確的第一引數,它代表物件,它由呼叫隱式提供。就像在Smalltalk中一樣,類本身就是物件。這為匯入和重新命名提供了語義。與C ++和Modula-3不同,內建型別可以用作基類,以便使用者進行擴充套件。此外,與C ++類似,可以為類例項重新定義大多數具有特殊語法(算術運算子,下標等)的內建運算子。
(由於缺乏普遍接受的術語來討論類,我偶爾會使用Smalltalk和C ++術語,因為它的面向物件語義與更接近,所以我會使用Modula-3術語,但很少有讀者聽說過。)
名稱和物件
物件具有特性,並且多個名稱(在多個作用域中)可以繫結在同一個物件上。在其它語言中被稱為別名。Python不可變基礎型別(數值,字串,元組)是傳值的,但是列表、字典這類可變物件,或者大多數程式外部型別(檔案,窗體等)描述實體時是傳地址的,像是指標。例如,你可以輕易的傳遞一個物件,因為通過繼承只是傳遞一個指標。
作用域和名稱空間
類的定義非常巧妙的運用了名稱空間。
名稱空間
是從命名到物件的對映。當前名稱空間主要是通過 Python 字典實現的,不過通常不關心具體的實現方式(除非出於效能考慮),以後也有可能會改變其實現方式。以下有一些名稱空間的例子:內建命名(像abs()
這樣的函式,以及內建異常名)集,模組中的全域性命名,函式呼叫中的區域性命名。某種意義上講物件的屬性集也是一個名稱空間。關於名稱空間需要了解的一件很重要的事就是不同名稱空間中的命名沒有任何聯絡,例如兩個不同的模組可能都會定義一個名為maximize
的函式而不會發生混淆-使用者必須以模組名為字首來引用它們。
順便提一句,我稱 Python 中任何一個“.”之後的命名為屬性
--例如,表示式z.real
中的real
是物件z
的一個屬性。嚴格來講,從模組中引用命名是引用屬性:表示式modname.funcname
中,modname
是一個模組物件,funcname
是它的一個屬性。因此,模組的屬性和模組中的全域性命名有直接的對映關係:它們共享同一名稱空間![1]
屬性可以是隻讀過或寫的。後一種情況下,可以對屬性賦值。你可以這樣作:modname.the_answer = 42
。可寫的屬性也可以用del
語句刪除。例如:del modname.the_answer
會從modname
物件中刪除the_answer
屬性。
不同的名稱空間在不同的時刻建立,有不同的生存期。包含內建命名的名稱空間在 Python 直譯器啟動時建立,會一直保留,不被刪除。模組的全域性名稱空間在模組定義被讀入時建立,通常,模組名稱空間也會一直儲存到直譯器退出。由直譯器在最高層呼叫執行的語句,不管它是從指令碼檔案中讀入還是來自互動式輸入,都是 main 模組的一部分,所以它們也擁有自己的名稱空間(內建命名也同樣被包含在一個模組中,它被稱作builtins )。
當呼叫函式時,就會為它建立一個區域性名稱空間,並且在函式返回或丟擲一個並沒有在函式內部處理的異常時被刪除。(實際上,用遺忘來形容到底發生了什麼更為貼切。)當然,每個遞迴呼叫都有自己的區域性名稱空間。
作用域 就是一個 Python 程式可以直接訪問名稱空間的正文區域。這裡的直接訪問意思是一個對名稱的錯誤引用會嘗試在名稱空間內查詢。儘管作用域是靜態定義,在使用時他們都是動態的。每次執行時,至少有三個名稱空間可以直接訪問的作用域巢狀在一起:
-
包含區域性命名的使用域在最裡面,首先被搜尋;其次搜尋的是中層的作用域,這裡包含了同級的函式;
最後搜尋最外面的作用域,它包含內建命名。
-
首先搜尋最內層的作用域,它包含區域性命名任意函式包含的作用域,是內層巢狀作用域搜尋起點,包含非區域性,但是也非全域性的命名
-
接下來的作用域包含當前模組的全域性命名
-
最外層的作用域(最後搜尋)是包含內建命名的名稱空間
如果一個命名宣告為全域性的,那麼對它的所有引用和賦值會直接搜尋包含這個模組全域性命名的作用域。如果要重新繫結最裡層作用域之外的變數,可以使用nonlocal 語句;如果不宣告為 nonlocal,這些變數將是隻讀的(對這樣的變數賦值會在最裡面的作用域建立一個新的區域性變數,外部具有相同命名的那個變數不會改變)。
通常,區域性作用域引用當前函式的命名。在函式之外,區域性作用域與全域性使用域引用同一名稱空間:模組名稱空間。類定義也是區域性作用域中的另一個名稱空間。
重要的是作用域決定於源程式的意義:一個定義於某模組中的函式的全域性作用域是該模組的名稱空間,而不是該函式的別名被定義或呼叫的位置,瞭解這一點非常重要。另一方面,命名的實際搜尋過程是動態的,在執行時確定的——然而,Python 語言也在不斷髮展,以後有可能會成為靜態的“編譯”時確定,所以不要依賴動態解析!(事實上,區域性變數已經是靜態確定了。)
Python 的一個特別之處在於:如果沒有使用global
語法,其賦值操作總是在最裡層的作用域。賦值不會複製資料,只是將命名繫結到物件。刪除也是如此:del x
只是從區域性作用域的名稱空間中刪除命名x
。事實上,所有引入新命名的操作都作用於區域性作用域。特別是import
語句和函式定義將模組名或函式綁定於區域性作用域(可以使用global
語句將變數引入到全域性作用域)。
global 語句用以指明某個特定的變數為全域性作用域,並重新繫結它。nonlocal 語句用以指明某個特定的變數為封閉作用域,並重新繫結它。
9.2.1. 作用域和名稱空間示例
以下是一個示例,演示瞭如何引用不同作用域和名稱空間,以及global 和nonlocal 如何影響變數繫結:
<pre>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)
</pre>
以上示例程式碼的輸出為:
<pre>After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
</pre>
注意:local 賦值語句是無法改變scope_test 的spam 繫結。nonlocal 賦值語句改變了scope_test 的spam 繫結,並且global 賦值語句從模組級改變了 spam 繫結。
你也可以看到在global 賦值語句之前對spam 是沒有預先繫結的。
9.3. 初識類
類引入了一些新語法:三種新的物件型別和一些新的語義。
9.3.1. 類定義語法
類定義最簡單的形式如下:
<pre>class ClassName:
<statement-1>
.
.
.
<statement-N>
</pre>
類的定義就像函式定義(def 語句),要先執行才能生效。(你當然可以把它放進if 語句的某一分支,或者一個函式的內部。)
習慣上,類定義語句的內容通常是函式定義,不過其它語句也可以,有時會很有用,後面我們再回過頭來討論。類中的函式定義通常包括了一個特殊形式的引數列表,用於方法呼叫約定——同樣我們在後面討論這些。
進入類定義部分後,會創建出一個新的名稱空間,作為區域性作用域。因此,所有的賦值成為這個新名稱空間的區域性變數。特別是函式定義在此綁定了新的命名。
類定義完成時(正常退出),就建立了一個類物件
。基本上它是對類定義建立的名稱空間進行了一個包裝;我們在下一節進一步學習類物件的知識。原始的區域性作用域(類定義引入之前生效的那個)得到恢復,類物件在這裡繫結到類定義頭部的類名(例子中是ClassName
)。
9.3.2. 類物件
類物件支援兩種操作:屬性引用和例項化。
屬性引用
使用和 Python 中所有的屬性引用一樣的標準語法:obj.name
。類物件建立後,類名稱空間中所有的命名都是有效屬性名。所以如果類定義是這樣:
<pre>class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
</pre>
那麼MyClass.i
和MyClass.f
是有效的屬性引用,分別返回一個整數和一個方法物件。也可以對類屬性賦值,你可以通過給MyClass.i
賦值來修改它。__doc__
也是一個有效的屬性,返回類的文件字串:"A simple example class"
。
類的例項化 使用函式符號。只要將類物件看作是一個返回新的類例項的無引數函式即可。例如(假設沿用前面的類):
<pre>x = MyClass()
</pre>
以上建立了一個新的類例項
並將該物件賦給區域性變數x
。
這個例項化操作(“呼叫”一個類物件)來建立一個空的物件。很多類都傾向於將物件建立為有初始狀態的。因此類可能會定義一個名為__init__()
的特殊方法,像下面這樣:
<pre>definit (self):
self.data = []
</pre>
類定義了__init__()
方法的話,類的例項化操作會自動為新建立的類例項呼叫__init__()
方法。所以在下例中,可以這樣建立一個新的例項:
<pre>x = MyClass()
</pre>
當然,出於彈性的需要,__init__()
方法可以有引數。事實上,引數通過__init__()
傳遞到類的例項化操作上。例如,
<pre>>>> class Complex:
...definit (self, realpart, imagpart):
...self.r = realpart
...self.i = imagpart
...
x = Complex(3.0, -4.5)x.r, x.i
(3.0, -4.5)
</pre>
9.3.3. 例項物件
現在我們可以用例項物件作什麼?例項物件唯一可用的操作就是屬性引用。有兩種有效的屬性名。
資料屬性
相當於 Smalltalk 中的“例項變數”或 C++ 中的“資料成員”。和區域性變數一樣,資料屬性不需要宣告,第一次使用時它們就會生成。例如,如果x
是前面建立的MyClass
例項,下面這段程式碼會打印出 16 而在堆疊中留下多餘的東西:
<pre>x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
</pre>
另一種為例項物件所接受的引用屬性是方法 。方法是“屬於”一個物件的函式。(在 Python 中,方法不止是類例項所獨有:其它型別的物件也可有方法。例如,連結串列物件有 append,insert,remove,sort 等等方法。然而,在後面的介紹中,除非特別說明,我們提到的方法特指類方法)
例項物件的有效名稱依賴於它的類。按照定義,類中所有(使用者定義)的函式物件對應它的例項中的方法。所以在我們的例子中,x.f
是一個有效的方法引用,因為MyClass.f
是一個函式。但x.i
不是,因為MyClass.i
不是函式。不過x.f
和MyClass.f
不同,它是一個方法物件
,不是一個函式物件。
9.3.4. 方法物件
通常,方法通過右繫結方式呼叫:
<pre>x.f()
</pre>
在MyClass
示例中,這會返回字串'hello world'
。然而,也不是一定要直接呼叫方法。x.f
是一個方法物件,它可以儲存起來以後呼叫。例如:
<pre>xf = x.f
while True:
print(xf())
</pre>
會不斷的列印hello world
。
呼叫方法時發生了什麼?你可能注意到呼叫x.f()
時沒有引用前面標出的變數,儘管在f()
的函式定義中指明瞭一個引數。這個引數怎麼了?事實上如果函式呼叫中缺少引數,Python 會丟擲異常--甚至這個引數實際上沒什麼用……
實際上,你可能已經猜到了答案:方法的特別之處在於例項物件作為函式的第一個引數傳給了函式。在我們的例子中,呼叫x.f()
相當於MyClass.f(x)
。通常,以n
個引數的列表去呼叫一個方法就相當於將方法的物件插入到引數列表的最前面後,以這個列表去呼叫相應的函式。
如果你還是不理解方法的工作原理,瞭解一下它的實現也許有幫助。引用非資料屬性的例項屬性時,會搜尋它的類。如果這個命名確認為一個有效的函式物件類屬性,就會將例項物件和函式物件封裝進一個抽象物件:這就是方法物件。以一個引數列表呼叫方法物件時,它被重新拆封,用例項物件和原始的引數列表構造一個新的引數列表,然後函式物件呼叫這個新的引數列表。
9.3.5. 類和例項變數
一般來說,例項變數用於對每一個例項都是唯一的資料,類變數用於類的所有例項共享的屬性和方法:
<pre>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'</pre>
正如在術語相關 討論的,可變 物件,例如列表和字典,的共享資料可能帶來意外的效果。例如,下面程式碼中的tricks 列表不應該用作類變數,因為所有的Dog 例項將共享同一個列表:
<pre>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']</pre>
這個類的正確設計應該使用一個例項變數:
<pre>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']</pre>
備註
資料屬性會覆蓋同名的方法屬性。為了避免意外的名稱衝突,這在大型程式中是極難發現的 Bug,使用一些約定來減少衝突的機會是明智的。可能的約定包括:大寫方法名稱的首字母,使用一個唯一的小字串(也許只是一個下劃線)作為資料屬性名稱的字首,或者方法使用動詞而資料屬性使用名詞。
資料屬性可以被方法引用,也可以由一個物件的普通使用者(客戶)使用。換句話說,類不能用來實現純淨的資料型別。事實上,Python 中不可能強制隱藏資料——一切基於約定(如果需要,使用 C 編寫的 Python 實現可以完全隱藏實現細節並控制物件的訪問。這可以用來通過 C 語言擴充套件 Python)。
客戶應該謹慎的使用資料屬性——客戶可能通過踐踏他們的資料屬性而使那些由方法維護的常量變得混亂。注意:只要能避免衝突,客戶可以向一個例項物件新增他們自己的資料屬性,而不會影響方法的正確性——再次強調,命名約定可以避免很多麻煩。
從方法內部引用資料屬性(或其他方法)並沒有快捷方式。我覺得這實際上增加了方法的可讀性:當瀏覽一個方法時,在區域性變數和例項變數之間不會出現令人費解的情況。
一般,方法的第一個引數被命名為self
。這僅僅是一個約定:對 Python 而言,名稱self
絕對沒有任何特殊含義。(但是請注意:如果不遵循這個約定,對其他的 Python 程式設計師而言你的程式碼可讀性就會變差,而且有些類檢視器
程式也可能是遵循此約定編寫的。)
類屬性的任何函式物件都為那個類的例項定義了一個方法。函式定義程式碼不一定非得定義在類中:也可以將一個函式物件賦值給類中的一個區域性變數。例如:
<pre># 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
</pre>
現在f
,g
和h
都是類C
的屬性,引用的都是函式物件,因此它們都是C
例項的方法--h
嚴格等於g
。要注意的是這種習慣通常只會迷惑程式的讀者。
通過self
引數的方法屬性,方法可以呼叫其它的方法:
<pre>class Bag:
definit (self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
</pre>
方法可以像引用普通的函式那樣引用全域性命名。與方法關聯的全域性作用域是包含類定義的模組。(類本身永遠不會做為全域性作用域使用。)儘管很少有好的理由在方法 中使用全域性資料,全域性作用域確有很多合法的用途:其一是方法可以呼叫匯入全域性作用域的函式和方法,也可以呼叫定義在其中的類和函式。通常,包含此方法的類也會定義在這個全域性作用域,在下一節我們會了解為何一個方法要引用自己的類。
每個值都是一個物件,因此每個值都有一個 類(class
) (也稱為它的 型別(type
) ),它儲存為object.__class__
。
繼承
派生類的定義如下所示:
#!python class DerivedClassName(BaseClassName): <statement-1> . . . <statement-N>
基類BaseClassName 必須與派生類定義在一個作用域內。還可以用表示式指定基類:
#!python class DerivedClassName(modname.BaseClassName):
派生類定義的執行和基類是一樣的。構造派生類物件時,就記住了基類。如果在類中找不到請求呼叫的屬性,就搜尋基類。如果基類是由別的類派生而來,繼續招上一級。
派生類的例項化沒有什麼特殊之處:DerivedClassName()建立新的類例項。方法引用按如下規則解析:搜尋對應的類屬性,必要時沿基類鏈逐級搜尋,如果找到了函式物件,這個方法引用就是合法的。
派生類可能會過載基類的方法(對於 C++ 程式設計師來說,Python 中的所有方法本質上都是virtual方法)
直接呼叫基類方法,BaseClassName.methodname(self, arguments)。
Python 有兩個用於繼承的內建函式:
-
函式isinstance() 用於檢查例項型別: isinstance(obj, int)只有在
obj.__class__
是int 或其它從int 繼承的型別的時候為True。 -
函式issubclass() 用於檢查類繼承:issubclass(bool, int)為 True ,因為bool 是int 的子類。 然而,issubclass(float, int) 為 False,因為float 不是int 的子類。
多繼承
Python 同樣有限的支援多繼承形式。多繼承的類定義形如下例:
#!python class DerivedClassName(Base1, Base2, Base3): <statement-1> . . . <statement-N>
在大多數簡單情況下,搜尋屬性深度優先,左到右,而不是搜尋兩次在同一個類層次結構中,其中有一個重疊。因此,如果在 DerivedClassName中沒有找到某個屬性,就會搜尋Base1,然後遞迴的搜尋其基類,如果最終沒有找到,就搜尋 Base2,以此類推。
實際上,super() 可以動態的改變解析順序。類似其它多繼承語言的 call-next-method,比單繼承語言中的 super 更強大 。
動態調整順序十分必要的,因為所有的多繼承會有一到多個菱形關係(指有至少一個祖先類可以從子類經由多個繼承路徑到達)。例如,所有的 new-style 類繼承自object ,所以任意的多繼承總是會有多於一條繼承路徑到達object 。為了防止重複訪問基類,通過動態的線性化演算法,每個類都按從左到右的順序特別指定了順序,每個祖先類只調用一次,這是單調的(意味著類被繼承時不會影響它祖先的次序)。這種方式使得設計可靠並且可擴充套件的多繼承類成為可能。更多的內容請參見http://www.python.org/download/releases/2.3/mro/ 。
私有變數
Python以下劃線開頭的命名(例如_spam)會被處理為 API 的非公開部分(無論它是函式、方法或資料成員)。
__spam(前面至少兩個下劃線,後面至多一個),被替代為_classname__spam 。
名稱重整是有助於子類重寫方法,而不會打破組內的方法呼叫。例如:
#!python 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)
名稱重整儘可能的避免衝突,私有的變數仍可訪問或修改。在特定的場合也是有用的,比如除錯。
要注意傳入exec()或eval()的程式碼時不考慮所呼叫的類的類名,視其為當前類,這類似於global 語句,已經按位元組編譯的部分也有同樣的限制。這也同樣作用於getattr(), setattr()和delattr(), 像直接引用dict 一樣。
其他
類可以實現Pascal 中record或 C 中struct的資料型別,它將一組已命名的資料項繫結在一起。一個空的類定義可以很好的實現它:
#!python 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
例項方法物件也有屬性:m.self 是帶m()的例項物件,而m.func 是這個方法對應的函式物件。
迭代器
多數容器物件都可以用for 遍歷:
#!python 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='')
這種風格的訪問清晰、簡潔、方便。迭代器的用法在 Python 中普遍而且統一。for 語句在容器物件中呼叫iter() 。該函式返回定義了 next () 方法的迭代器物件,它在容器中逐一訪問元素。沒有後續的元素時, next () 丟擲StopIteration 異常通知for 語句迴圈結束。你可以是用內建的next() 函式呼叫 next () 方法;示例:
#!python >>> 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:
#!python 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
生成器
Generator 是建立迭代器的簡單而強大的工具。它們像函式,但使用yield 語句返回資料。每次呼叫next() ,生成器從上次停頓的地方開始執行(它記錄了最後一次執行語句和所有的資料值)。
#!python 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
基於類的迭代器能作的生成器也能作到。因為自動建立了 iter () 和 next () 方法,生成器更簡潔。
另一個關鍵的功能在於兩次執行之間,區域性變數和執行狀態都自動的儲存下來。這使函式很容易寫,而且比使用self.index和self.data之類例項變數的方式更清晰。
除了建立和儲存程式狀態的自動方法,當發生器終止時,還會自動丟擲StopIteration 異常。
生成器表示式
簡單的生成器和列表推導式類似。
#!python >>> 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(wordfor line in pagefor 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']