與其他編程語言相比,Python 的類機制通過最小的新語法和語義在語言中實現(xiàn)了類。 它是 C++ 或者 Modula-3 語言中類機制的混合。Python 類提供所有標準的面向?qū)ο缶幊坦δ?允許多個基類的類繼承機制,派生類可以覆蓋基類的任何方法或類,一個方法可以調(diào)用與基類方法名字相同的方法。對象可以包含任意數(shù)量和種類的數(shù)據(jù)。對于正確的模塊、類參加 Python 的動態(tài)特性在運行時創(chuàng)建。它們創(chuàng)建后可以進一步修改。
用 C++ 術(shù)語來講,所有的類成員(包括數(shù)據(jù)成員)都是公有(?public)的,所有的成員函數(shù)都是虛(?virtual?)的。用 Modula-3 的術(shù)語來講,在成員方法中沒有簡便的方式引用對象的成員:方法函數(shù)在定義時需要以引用的對象做為第一個參數(shù),調(diào)用時則會隱式引用對象。像在 Smalltalk 中一個,類也是對象。這就提供了導入和重命名語義。不像 C++ 和 Modula-3 中那樣,大多數(shù)帶有特殊語法的內(nèi)置操作符(算法運算符、下標等)都可以針對類的需要重新定義。
(在討論類時,沒有足夠的得到共識的術(shù)語,我會偶爾從 Smalltalk 和 C++ 借用一些。我比較喜歡用 Modula-3 的用語,因為比起 C++, Python 的面向?qū)ο笳Z法更像它,但是我想很少有讀者聽過這個。)
對象具有特性,并且多個名稱(在多個作用于中)可以綁定在同一個對象上。 這在其它語言中被稱為別名。 在對 Python 的第一印象中這通常會被忽略,并且當處理不可變基礎(chǔ)類型(數(shù)字,字符串,元組)時可以被放心的忽略。 但是,在調(diào)用列表、字典這類可變對象,或者大多數(shù)程序外部類型(文件,窗體等)描述實體時,別名對 Python 代碼的語義便具有(有意而為!)影響。 這通常有助于程序的優(yōu)化,因為在某些方面別名表現(xiàn)的就像是指針。 例如,你可以輕易的傳遞一個對象,因為通過繼承只是傳遞一個指針。 并且如果一個方法修改了一個作為參數(shù)傳遞的對象,調(diào)用者可以接收這一變化——這消除了兩種不同的參數(shù)傳遞機制的需要,像 Pascal 語言。
在介紹類之前,我首先介紹一些有關(guān) Python 作用域的規(guī)則。類的定義非常巧妙的運用了命名空間,要完全理解接下來的知識,需要先理解作用域和命名空間的工作原理。另外,這一切的知識對于任何高級 Python 程序員都非常有用。
讓我們從一些定義說起。
命名空間是從命名到對象的映射。當前命名空間主要是通過 Python 字典實現(xiàn)的,不過通常不關(guān)心具體的實現(xiàn)方式(除非出于性能考慮),以后也有可能會改變其實現(xiàn)方式。以下有一些命名空間的例子:內(nèi)置命名(像?abs()?這樣的函數(shù),以及內(nèi)置異常名)集,模塊中的全局命名,函數(shù)調(diào)用中的局部命名。某種意義上講對象的屬性集也是一個命名空間。關(guān)于命名空間需要了解的一件很重要的事就是不同命名空間中的命名沒有任何聯(lián)系,例如兩個不同的模塊可能都會定義一個名為?maximize?的函數(shù)而不會發(fā)生混淆--用戶必須以模塊名為前綴來引用它們。
順便提一句,我稱 Python 中任何一個“.”之后的命名為?屬性?--例如,表達式?z.real?中的?real?是對象?z?的一個屬性。嚴格來講,從模塊中引用命名是引用屬性:表達式?modname.funcname?中,?modname?是一個模塊對象,funcname?是它的一個屬性。因此,模塊的屬性和模塊中的全局命名有直接的映射關(guān)系:它們共享同一命名空間![1]
屬性可以是只讀過或?qū)懙?。后一種情況下,可以對屬性賦值。你可以這樣作:?modname.the_answer?=?42???蓪懙膶傩砸部梢杂?del?語句刪除。例如:?del?modname.the_answer?會從?modname?對象中刪除?the_answer?屬性。
不同的命名空間在不同的時刻創(chuàng)建,有不同的生存期。包含內(nèi)置命名的命名空間在 Python 解釋器啟動時創(chuàng)建,會一直保留,不被刪除。模塊的全局命名空間在模塊定義被讀入時創(chuàng)建,通常,模塊命名空間也會一直保存到解釋器退出。由解釋器在最高層調(diào)用執(zhí)行的語句,不管它是從腳本文件中讀入還是來自交互式輸入,都是?__main__?模塊的一部分,所以它們也擁有自己的命名空間。(內(nèi)置命名也同樣被包含在一個模塊中,它被稱作?__builtin__?。)
當調(diào)用函數(shù)時,就會為它創(chuàng)建一個局部命名空間,并且在函數(shù)返回或拋出一個并沒有在函數(shù)內(nèi)部處理的異常時被刪除。 (實際上,用遺忘來形容到底發(fā)生了什么更為貼切。) 當然,每個遞歸調(diào)用都有自己的局部命名空間。
作用域?就是一個 Python 程序可以直接訪問命名空間的正文區(qū)域。 這里的 直接訪問 意思是一個對名稱的錯誤引用會嘗試在命名空間內(nèi)查找。
盡管作用域是靜態(tài)定義,在使用時他們都是動態(tài)的。每次執(zhí)行時,至少有三個命名空間可以直接訪問的作用域嵌套在一起:
如果一個命名聲明為全局的,那么所有的賦值和引用都直接針對包含模全局命名的中級作用域。另外,從外部訪問到的所有內(nèi)層作用域的變量都是只讀的。(試圖寫這樣的變量只會在內(nèi)部作用域創(chuàng)建一個?新?局部變量,外部標示命名的那個變量不會改變)。
通常,局部作用域引用當前函數(shù)的命名。在函數(shù)之外,局部作用域與全局使用域引用同一命名空間:模塊命名空間。類定義也是局部作用域中的另一個命名空間。
重要的是作用域決定于源程序的意義:一個定義于某模塊中的函數(shù)的全局作用域是該模塊的命名空間,而不是該函數(shù)的別名被定義或調(diào)用的位置,了解這一點非常重要。另一方面,命名的實際搜索過程是動態(tài)的,在運行時確定的——然而,Python 語言也在不斷發(fā)展,以后有可能會成為靜態(tài)的“編譯”時確定,所以不要依賴動態(tài)解析?。ㄊ聦嵣希植孔兞恳呀?jīng)是靜態(tài)確定了。)
Python 的一個特別之處在于——如果沒有使用?global?語法——其賦值操作總是在最里層的作用域。賦值不會復制數(shù)據(jù)——只是將命名綁定到對象。刪除也是如此:?del?x?只是從局部作用域的命名空間中刪除命名 x?。事實上,所有引入新命名的操作都作用于局部作用域。特別是import?語句和函數(shù)定將模塊名或函數(shù)綁定于局部作用域。(可以使用 global?語句將變量引入到全局作用域。)
global?語句用以指明某個特定的變量為全局作用域,并重新綁定它。nonlocal?語句用以指明某個特定的變量為封閉作用域,并重新綁定它。
以下是一個示例,演示了如何引用不同作用域和命名空間,以及?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
注意:?local?賦值語句是無法改變?scope_test?的?spam?綁定。?nonlocal賦值語句改變了?scope_test?的?spam?綁定,并且?global?賦值語句從模塊級改變了 spam 綁定。
你也可以看到在?global?賦值語句之前對 spam 是沒有預先綁定的。
類引入了一些新語法:三種新的對象類型和一些新的語義。
類定義最簡單的形式如下:
class ClassName:
<statement-1>
.
.
.
<statement-N>
類的定義就像函數(shù)定義(?def?語句),要先執(zhí)行才能生效。(你當然可以把它放進?if?語句的某一分支,或者一個函數(shù)的內(nèi)部。)
習慣上,類定義語句的內(nèi)容通常是函數(shù)定義,不過其它語句也可以,有時會很有用——后面我們再回過頭來討論。類中的函數(shù)定義通常包括了一個特殊形式的參數(shù)列表,用于方法調(diào)用約定——同樣我們在后面討論這些。
進入類定義部分后,會創(chuàng)建出一個新的命名空間,作為局部作用域——因此,所有的賦值成為這個新命名空間的局部變量。特別是函數(shù)定義在此綁定了新的命名。
類定義完成時(正常退出),就創(chuàng)建了一個?類對象?。基本上它是對類定義創(chuàng)建的命名空間進行了一個包裝;我們在下一節(jié)進一步學習類對象的知識。原始的局部作用域(類定義引入之前生效的那個)得到恢復,類對象在這里綁定到類定義頭部的類名(例子中是?ClassName?)。
類對象支持兩種操作:屬性引用和實例化。
屬性引用?使用和 Python 中所有的屬性引用一樣的標準語法:obj.name。類對象創(chuàng)建后,類命名空間中所有的命名都是有效屬性名。所以如果類定義是這樣:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
那么?MyClass.i?和?MyClass.f?是有效的屬性引用,分別返回一個整數(shù)和一個方法對象。也可以對類屬性賦值,你可以通過給?MyClass.i?賦值來修改它。?__doc__?也是一個有效的屬性,返回類的文檔字符串:"A?simple?example?class"?。
類的實例化?使用函數(shù)符號。只要將類對象看作是一個返回新的類實例的無參數(shù)函數(shù)即可。例如(假設(shè)沿用前面的類):
x = MyClass()
以上創(chuàng)建了一個新的類?實例?并將該對象賦給局部變量?x?。
這個實例化操作(“調(diào)用”一個類對象)來創(chuàng)建一個空的對象。很多類都傾向于將對象創(chuàng)建為有初始狀態(tài)的。因此類可能會定義一個名為__init__()?的特殊方法,像下面這樣:
def __init__(self):
self.data = []
類定義了?__init__()?方法的話,類的實例化操作會自動為新創(chuàng)建的類實例調(diào)用?__init__()?方法。所以在下例中,可以這樣創(chuàng)建一個新的實例:
X = MyClass()
當然,出于彈性的需要,?__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)
現(xiàn)在我們可以用實例對象作什么?實例對象唯一可用的操作就是屬性引用。有兩種有效的屬性名。
數(shù)據(jù)屬性?相當于 Smalltalk 中的“實例變量”或 C++ 中的“數(shù)據(jù)成員”。和局部變量一樣,數(shù)據(jù)屬性不需要聲明,第一次使用時它們就會生成。例如,如果?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 中,方法不止是類實例所獨有:其它類型的對象也可有方法。例如,鏈表對象有 append,insert,remove,sort 等等方法。然而,在后面的介紹中,除非特別說明,我們提到的方法特指類方法)
實例對象的有效名稱依賴于它的類。按照定義,類中所有(用戶定義)的函數(shù)對象對應它的實例中的方法。所以在我們的例子中,x.f?是一個有效的方法引用,因為?MyClass.f?是一個函數(shù)。但?x.i?不是,因為MyClass.i?不是函數(shù)。不過?x.f?和?MyClass.f?不同--它是一個?方法對象?,不是一個函數(shù)對象。
通常,方法通過右綁定方式調(diào)用:
x.f()
在?MyClass?示例中,這會返回字符串?'hello?world'?。然而,也不是一定要直接調(diào)用方法。?x.f?是一個方法對象,它可以存儲起來以后調(diào)用。例如:
Xf = x.f
while True:
print(xf())
會不斷的打印?hello?world?。
調(diào)用方法時發(fā)生了什么?你可能注意到調(diào)用?x.f()?時沒有引用前面標出的變量,盡管在?f()?的函數(shù)定義中指明了一個參數(shù)。這個參數(shù)怎么了?事實上如果函數(shù)調(diào)用中缺少參數(shù),Python 會拋出異常--甚至這個參數(shù)實際上沒什么用……
實際上,你可能已經(jīng)猜到了答案:方法的特別之處在于實例對象作為函數(shù)的第一個參數(shù)傳給了函數(shù)。在我們的例子中,調(diào)用?x.f()?相當于 MyClass.f(x)?。通常,以?n?個參數(shù)的列表去調(diào)用一個方法就相當于將方法的對象插入到參數(shù)列表的最前面后,以這個列表去調(diào)用相應的函數(shù)。
如果你還是不理解方法的工作原理,了解一下它的實現(xiàn)也許有幫助。引用非數(shù)據(jù)屬性的實例屬性時,會搜索它的類。 如果這個命名確認為一個有效的函數(shù)對象類屬性,就會將實例對象和函數(shù)對象封裝進一個抽象對象:這就是方法對象。以一個參數(shù)列表調(diào)用方法對象時,它被重新拆 封,用實例對象和原始的參數(shù)列表構(gòu)造一個新的參數(shù)列表,然后函數(shù)對象調(diào)用這個新的參數(shù)列表。
一般來說,實例變量數(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'
正如一句話所討論的關(guān)于名稱和對象,共享數(shù)據(jù)可能會有驚人的效果伴隨著涉及的可變對象,比如列表和字典。例如,技巧列表下面的代碼不應只作為一個類變量,因為一個列表將由所有狗實例共享:
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è)計應該使用一個實例變量,而不是:
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']
數(shù)據(jù)屬性會覆蓋同名的方法屬性。 為了避免意外的名稱沖突,這在大型程序中是極難發(fā)現(xiàn)的 Bug,使用一些約定來減少沖突的機會是明智的。 可能的約定包括:大寫方法名稱的首字母,使用一個唯一的小字符串(也許只是一個下劃線)作為數(shù)據(jù)屬性名稱的前綴,或者方法使用動詞而數(shù)據(jù)屬性使用名詞。
數(shù)據(jù)屬性可以被方法引用,也可以由一個對象的普通用戶(客戶)使用。 換句話說,類不能用來實現(xiàn)純凈的數(shù)據(jù)類型。 事實上,Python 中不可能強制隱藏數(shù)據(jù)——一切基于約定。 (如果需要,使用 C 編寫的 Python 實現(xiàn)可以完全隱藏實現(xiàn)細節(jié)并控制對象的訪問。這可以用來通過 C 語言擴展 Python。)
客戶應該謹慎的使用數(shù)據(jù)屬性——客戶可能通過踐踏他們的數(shù)據(jù)屬性而使那些由方法維護的常量變得混亂。 注意:只要能避免沖突,客戶可以向一個實例對象添加他們自己的數(shù)據(jù)屬性,而不會影響方法的正確性——再次強調(diào),命名約定可以避免很多麻煩。
從方法內(nèi)部引用數(shù)據(jù)屬性(或其他方法)并沒有快捷方式。我覺得這實際上增加了方法的可讀性:當瀏覽一個方法時,在局部變量和實例變量之間不會出現(xiàn)令人費解的情況。
一般,方法的第一個參數(shù)被命名為 self 。 這僅僅是一個約定:對 Python 而言,名稱 self 絕對沒有任何特殊含義。(但是請注意:如果不遵循這個約定,對其他的 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?都是類?C?的屬性,引用的都是函數(shù)對象,因此它們都是?C?實例的方法--?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)的全局作用域是包含類定義的模塊。(類本身永遠不會做為全局作用域使用。)盡管很少有好的理由在方法 中使用全局數(shù)據(jù),全局作用域確有很多合法的用途:其一是方法可以調(diào)用導入全局作用域的函數(shù)和方法,也可以調(diào)用定義在其中的類和函數(shù)。通常,包含此方法的類也會定義在這個全局作用域,在下一節(jié)我們會了解為何一個方法要引用自己的類。
每個值都是一個對象,因此每個值都有一個 類(?class?) (也稱為它的 類型(?type?) ),它存儲為?object.__class__?。
當然,如果一種語言不支持繼承就,“類”就沒有什么意義。派生類的定義如下所示:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
命名?BaseClassName?(示例中的基類名)必須與派生類定義在一個作用域內(nèi)。除了類,還可以用表達式,基類定義在另一個模塊中時這一點非常有用:
class DerivedClassName(modname.BaseClassName):
派生類定義的執(zhí)行過程和基類是一樣的。構(gòu)造派生類對象時,就記住了基類。這在解析屬性引用的時候尤其有用:如果在類中找不到請求調(diào)用的屬性,就搜索基類。如果基類是由別的類派生而來,這個規(guī)則會遞歸的應用上去。
派生類的實例化沒有什么特殊之處:?DerivedClassName()?(示列中的派生類)創(chuàng)建一個新的類實例。方法引用按如下規(guī)則解析:搜索對應的類屬性,必要時沿基類鏈逐級搜索,如果找到了函數(shù)對象這個方法引用就是合法的。
派生類可能會覆蓋其基類的方法。因為方法調(diào)用同一個對象中的其它方法時沒有特權(quán),基類的方法調(diào)用同一個基類的方法時,可能實際上最終調(diào)用了派生類中的覆蓋方法。(對于 C++ 程序員來說,Python 中的所有方法本質(zhì)上都是?虛?方法。)
派生類中的覆蓋方法可能是想要擴充而不是簡單的替代基類中的重名方法。有一個簡單的方法可以直接調(diào)用基類方法,只要調(diào)用:BaseClassName.methodname(self,?arguments)。有時這對于客戶也很有用。(要注意只有?BaseClassName?在同一全局作用域定義或?qū)霑r才能這樣用。)
Python 有兩個用于繼承的函數(shù):
函數(shù)?isinstance()?用于檢查實例類型:?isinstance(obj,?int)?只有在obj.__class__?是?int?或其它從?int?繼承的類型
issubclass()?用于檢查類繼承:?issubclass(bool,?int)?為?True?,因為?bool?是 int 的子類。但是,?issubclass(unicode,?str)?是?False?,因為?unicode?不是?str?的子類(它們只是共享一個通用祖先類basestring?)。 Python 同樣有限的支持多繼承形式。多繼承的類定義形如下例:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
在大多數(shù)情況下,在最簡單的情況下,你能想到的搜索屬性從父類繼承的深度優(yōu)先,左到右,而不是搜索兩次在同一個類層次結(jié)構(gòu)中,其中有一個重疊。因此,如果在?DerivedClassName?(示例中的派生類)中沒有找到某個屬性,就會搜索?Base1?,然后(遞歸的)搜索其基類,如果最終沒有找到,就搜索?Base2?,以此類推。
實際上,super()?可以動態(tài)的改變解析順序。這個方式可見于其它的一些多繼承語言,類似 call-next-method,比單繼承語言中的 super 更強大 。
動態(tài)調(diào)整順序十分必要的,因為所有的多繼承會有一到多個菱形關(guān)系(指有至少一個祖先類可以從子類經(jīng)由多個繼承路徑到達)。例如,所有的 new-style 類繼承自?object?,所以任意的多繼承總是會有多于一條繼承路徑到達?object?。
為了防止重復訪問基類,通過動態(tài)的線性化算法,每個類都按從左到右的順序特別指定了順序,每個祖先類只調(diào)用一次,這是單調(diào)的(意味著一個類被繼承時不會影響它祖先的次序)??偹憧梢酝ㄟ^這種方式使得設(shè)計一個可靠并且可擴展的多繼承類成為可能。進一步的內(nèi)容請參見http://www.python.org/download/releases/2.3/mro/?。
只能從對像內(nèi)部訪問的“私有”實例變量,在 Python 中不存在。然而,也有一個變通的訪問用于大多數(shù) Python 代碼:以一個下劃線開頭的命名(例如?_spam?)會被處理為 API 的非公開部分(無論它是一個函數(shù)、方法或數(shù)據(jù)成員)。它會被視為一個實現(xiàn)細節(jié),無需公開。
因為有一個正當?shù)念愃接谐蓡T用途(即避免子類里定義的命名與之沖突),Python 提供了對這種結(jié)構(gòu)的有限支持,稱為?name mangling(命名編碼) 。任何形如?__spam?的標識(前面至少兩個下劃線,后面至多一個),被替代為?_classname__spam?,去掉前導下劃線的?classname 即當前的類名。此語法不關(guān)注標識的位置,只要求在類定義內(nèi)。
名稱重整是有助于子類重寫方法,而不會打破組內(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è)計為盡可能的避免沖突,被認作為私有的變量仍然有可能被訪問或修改。在特定的場合它也是有用的,比如調(diào)試的時候。
要注意的是代碼傳入?exec?,?eval()?或?execfile()?時不考慮所調(diào)用的類的類名,視其為當前類,這類似于?global?語句的效應,已經(jīng)按字節(jié)編譯的部分也有同樣的限制。這也同樣作用于?getattr()?,?setattr()?和 delattr()?,像直接引用?__dict__?一樣。
有時類似于 Pascal 中“記錄( record )”或 C 中“結(jié)構(gòu)( struct )”的數(shù)據(jù)類型很有用,它將一組已命名的數(shù)據(jù)項綁定在一起。一個空的類定義可以很好的實現(xiàn)它:
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 代碼需要一個特殊的抽象數(shù)據(jù)結(jié)構(gòu)的話,通??梢詡魅胍粋€類,事實上這模仿了該類的方法。例如,如果你有一個用于從文件對象中格式化數(shù)據(jù)的函數(shù),你可以定義一個帶有?read()?和?readline()方法的類,以此從字符串緩沖讀取數(shù)據(jù),然后將該類的對象作為參數(shù)傳入前述的函數(shù)。
實例方法對象也有屬性:m.im_self是一個實例方法所屬的對象,而 m.im_func?是這個方法對應的函數(shù)對象。
用戶自定義異常也可以是類。利用這個機制可以創(chuàng)建可擴展的異常體系。 以下是兩種新的,有效的(語義上的)異常拋出形式,使用?raise?語句:
raise Class
raise Instance
第一種形式中,?instance?必須是?Class?或其派生類的一個實例。第二種形式是以下形式的簡寫:
raise Class()
發(fā)生的異常其類型如果是?except?子句中列出的類,或者是其派生類,那么它們就是相符的(反過來說--發(fā)生的異常其類型如果是異常子句中列出的類的基類,它們就不相符)。例如,以下代碼會按順序打印 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")
要注意的是如果異常子句的順序顛倒過來(?execpt?B?在最前),它就會打印 B,B,B--第一個匹配的異常被觸發(fā)。
打印一個異常類的錯誤信息時,先打印類名,然后是一個空格、一個冒號,然后是用內(nèi)置函數(shù)?str()?將類轉(zhuǎn)換得到的完整字符串。
現(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='')
這種形式的訪問清晰、簡潔、方便。迭代器的用法在 Python 中普遍而且統(tǒng)一。在后臺,?for?語句在容器對象中調(diào)用?iter()?。 該函數(shù)返回一個定義了?next()?方法的迭代器對象,它在容器中逐一訪問元素。沒有后續(xù)的元素時,?next()?拋出一個?StopIteration?異常通知?for?語句循環(huán)結(jié)束。以下是其工作原理的示例:
>>> 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é)議的后臺機制,就可以很容易的給自己的類添加迭代器行為。定義一個?__iter__()?方法,使其返回一個帶有?next()?方法的對象。如果這個類已經(jīng)定義了?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
Generator?是創(chuàng)建迭代器的簡單而強大的工具。它們寫起來就像是正規(guī)的函數(shù),需要返回數(shù)據(jù)的時候使用?yield?語句。每次?next()?被調(diào)用時,生成器回復它脫離的位置(它記憶語句最后一次執(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é)中描述了基于類的迭代器,它能作的每一件事生成器也能作到。因為自動創(chuàng)建了__iter__()?和?next()?方法,生成器顯得如此簡潔。
另一個關(guān)鍵的功能在于兩次執(zhí)行之間,局部變量和執(zhí)行狀態(tài)都自動的保存下來。這使函數(shù)很容易寫,而且比使用?self.index?和?self.data?之類的方式更清晰。
除了創(chuàng)建和保存程序狀態(tài)的自動方法,當發(fā)生器終結(jié)時,還會自動拋出 StopIteration?異常。綜上所述,這些功能使得編寫一個正規(guī)函數(shù)成為創(chuàng)建迭代器的最簡單方法。
有時簡單的生成器可以用簡潔的方式調(diào)用,就像不帶中括號的鏈表推導式。這些表達式是為函數(shù)調(diào)用生成器而設(shè)計的。生成器表達式比完整的生成器定義更簡潔,但是沒有那么多變,而且通常比等價的鏈表推導式更容易記。
例如:
>>> 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] 有一個例外。模塊對象有一個隱秘的只讀對象,名為?__dict__?,它返回用于實現(xiàn)模塊命名空間的字典,命名?__dict__?是一個屬性而非全局命名。顯然,使用它違反了命名空間實現(xiàn)的抽象原則,應該被嚴格限制于調(diào)試中。