首頁 > Python教程 > Python基礎教程 > Python入門教程之類

Python入門教程之類

時間:2019-08-15    來源:互聯網

9. 類

Python 的類機制通過最小的新語法和語義在語言中實現了類。它是 C++ 或者 Modula-3 語言中類機制的混合。就像模塊一樣,Python 的類并沒有在用戶和定義之間設立絕對的屏障,而是依賴于用戶不去“強行闖入定義”的優雅。另一方面,類的大多數重要特性都被完整的保留下來:類繼承機制允許多重繼承,派生類可以覆蓋(override)基類中的任何方法或類,可以使用相同的方法名稱調用基類的方法。對象可以包含任意數量的私有數據。

用 C++ 術語來講,所有的類成員(包括數據成員)都是公有( public )的(其它情況見下文 私有變量),所有的成員函數都是虛( virtual )的。用 Modula-3 的術語來講,在成員方法中沒有簡便的方式引用對象的成員:方法函數在定義時需要以引用的對象做為第一個參數,調用時則會隱式引用對象。像在 Smalltalk 中一個,類也是對象。這就提供了導入和重命名語義。不像 C++ 和 Modula-3 中那樣,大多數帶有特殊語法的內置操作符(算法運算符、下標等)都可以針對類的需要重新定義。

在討論類時,沒有足夠的得到共識的術語,我會偶爾從 Smalltalk 和 C++ 借用一些。我比較喜歡用 Modula-3 的用語,因為比起 C++,Python 的面向對象語法更像它,但是我想很少有讀者聽過這個。

9.1. 術語相關

對象具有特性,并且多個名稱(在多個作用域中)可以綁定在同一個對象上。在其它語言中被稱為別名。在對 Python 的第一印象中這通常會被忽略,并且當處理不可變基礎類型(數字,字符串,元組)時可以被放心的忽略。但是,在調用列表、字典這類可變對象,或者大多數程序外部類型(文件,窗體等)描述實體時,別名對 Python 代碼的語義便具有(有意而為)影響。這通常有助于程序的優化,因為在某些方面別名表現的就像是指針。例如,你可以輕易的傳遞一個對象,因為通過繼承只是傳遞一個指針。并且如果一個方法修改了一個作為參數傳遞的對象,調用者可以接收這一變化——這消除了兩種不同的參數傳遞機制的需要,像 Pascal 語言。

9.2. Python 作用域和命名空間

在介紹類之前,我首先介紹一些有關 Python 作用域的規則。類的定義非常巧妙的運用了命名空間,要完全理解接下來的知識,需要先理解作用域和命名空間的工作原理。另外,這一切的知識對于任何高級 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 如何影響變量綁定:

 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 是沒有預先綁定的。

9.3. 初識類

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

9.3.1. 類定義語法

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

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

類的定義就像函數定義( def 語句),要先執行才能生效。(你當然可以把它放進 if 語句的某一分支,或者一個函數的內部。)

習慣上,類定義語句的內容通常是函數定義,不過其它語句也可以,有時會很有用,后面我們再回過頭來討論。類中的函數定義通常包括了一個特殊形式的參數列表,用于方法調用約定——同樣我們在后面討論這些。

進入類定義部分后,會創建出一個新的命名空間,作為局部作用域。因此,所有的賦值成為這個新命名空間的局部變量。特別是函數定義在此綁定了新的命名。

類定義完成時(正常退出),就創建了一個 類對象。基本上它是對類定義創建的命名空間進行了一個包裝;我們在下一節進一步學習類對象的知識。原始的局部作用域(類定義引入之前生效的那個)得到恢復,類對象在這里綁定到類定義頭部的類名(例子中是 ClassName )。

9.3.2. 類對象

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

屬性引用 使用和 Python 中所有的屬性引用一樣的標準語法:obj.name。類對象創建后,類命名空間中所有的命名都是有效屬性名。所以如果類定義是這樣:

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

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

類的 實例化 使用函數符號。只要將類對象看作是一個返回新的類實例的無參數函數即可。例如(假設沿用前面的類):

 x = MyClass()  

以上創建了一個新的類 實例 并將該對象賦給局部變量 x。

這個實例化操作(“調用”一個類對象)來創建一個空的對象。很多類都傾向于將對象創建為有初始狀態的。因此類可能會定義一個名為 __init__() 的特殊方法,像下面這樣:

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

類定義了 __init__() 方法的話,類的實例化操作會自動為新創建的類實例調用 __init__() 方法。所以在下例中,可以這樣創建一個新的實例:

 x = MyClass()  

當然,出于彈性的需要,__init__() 方法可以有參數。事實上,參數通過 __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. 實例對象

現在我們可以用實例對象作什么?實例對象唯一可用的操作就是屬性引用。有兩種有效的屬性名。

數據屬性 相當于 Smalltalk 中的“實例變量”或 C++ 中的“數據成員”。和局部變量一樣,數據屬性不需要聲明,第一次使用時它們就會生成。例如,如果 x 是前面創建的 MyClass 實例,下面這段代碼會打印出 16 而在堆棧中留下多余的東西:

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

另一種為實例對象所接受的引用屬性是 方法。方法是“屬于”一個對象的函數。(在 Python 中,方法不止是類實例所獨有:其它類型的對象也可有方法。例如,鏈表對象有 append,insert,remove,sort 等等方法。然而,在后面的介紹中,除非特別說明,我們提到的方法特指類方法)

實例對象的有效名稱依賴于它的類。按照定義,類中所有(用戶定義)的函數對象對應它的實例中的方法。所以在我們的例子中,x.f 是一個有效的方法引用,因為 MyClass.f 是一個函數。但 x.i 不是,因為 MyClass.i 不是函數。不過 x.f 和 MyClass.f 不同,它是一個 方法對象 ,不是一個函數對象。

9.3.4. 方法對象

通常,方法通過右綁定方式調用:

 x.f()  

在 MyClass 示例中,這會返回字符串 'hello world'。然而,也不是一定要直接調用方法。 x.f 是一個方法對象,它可以存儲起來以后調用。例如:

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

會不斷的打印 hello world。

調用方法時發生了什么?你可能注意到調用 x.f() 時沒有引用前面標出的變量,盡管在 f() 的函數定義中指明了一個參數。這個參數怎么了?事實上如果函數調用中缺少參數,Python 會拋出異常--甚至這個參數實際上沒什么用……

實際上,你可能已經猜到了答案:方法的特別之處在于實例對象作為函數的第一個參數傳給了函數。在我們的例子中,調用 x.f() 相當于 MyClass.f(x) 。通常,以 n 個參數的列表去調用一個方法就相當于將方法的對象插入到參數列表的最前面后,以這個列表去調用相應的函數。

如果你還是不理解方法的工作原理,了解一下它的實現也許有幫助。引用非數據屬性的實例屬性時,會搜索它的類。如果這個命名確認為一個有效的函數對象類屬性,就會將實例對象和函數對象封裝進一個抽象對象:這就是方法對象。以一個參數列表調用方法對象時,它被重新拆封,用實例對象和原始的參數列表構造一個新的參數列表,然后函數對象調用這個新的參數列表。

9.3.5. 類和實例變量

一般來說,實例變量用于對每一個實例都是唯一的數據,類變量用于類的所有實例共享的屬性和方法:

 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'  

正如在 術語相關 討論的, 可變 對象,例如列表和字典,的共享數據可能帶來意外的效果。例如,下面代碼中的 tricks 列表不應該用作類變量,因為所有的 Dog 實例將共享同一個列表:

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

這個類的正確設計應該使用一個實例變量:

 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. 一些說明

數據屬性會覆蓋同名的方法屬性。為了避免意外的名稱沖突,這在大型程序中是極難發現的 Bug,使用一些約定來減少沖突的機會是明智的。可能的約定包括:大寫方法名稱的首字母,使用一個唯一的小字符串(也許只是一個下劃線)作為數據屬性名稱的前綴,或者方法使用動詞而數據屬性使用名詞。

數據屬性可以被方法引用,也可以由一個對象的普通用戶(客戶)使用。換句話說,類不能用來實現純凈的數據類型。事實上,Python 中不可能強制隱藏數據——一切基于約定(如果需要,使用 C 編寫的 Python 實現可以完全隱藏實現細節并控制對象的訪問。這可以用來通過 C 語言擴展 Python)。

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

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

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

類屬性的任何函數對象都為那個類的實例定義了一個方法。函數定義代碼不一定非得定義在類中:也可以將一個函數對象賦值給類中的一個局部變量。例如:

 # 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, g 和 h 都是類 C 的屬性,引用的都是函數對象,因此它們都是 C 實例的方法-- h嚴格等于 g 。要注意的是這種習慣通常只會迷惑程序的讀者。

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

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

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

9.5. 繼承

當然,如果一種語言不支持繼承就,“類”就沒有什么意義。派生類的定義如下所示:

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

命名 BaseClassName (示例中的基類名)必須與派生類定義在一個作用域內。除了類,還可以用表達式,基類定義在另一個模塊中時這一點非常有用:

 class DerivedClassName(modname.BaseClassName):  

派生類定義的執行過程和基類是一樣的。構造派生類對象時,就記住了基類。這在解析屬性引用的時候尤其有用:如果在類中找不到請求調用的屬性,就搜索基類。如果基類是由別的類派生而來,這個規則會遞歸的應用上去。

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

派生類可能會覆蓋其基類的方法。因為方法調用同一個對象中的其它方法時沒有特權,基類的方法調用同一個基類的方法時,可能實際上最終調用了派生類中的覆蓋方法。(對于 C++ 程序員來說,Python 中的所有方法本質上都是 虛 方法。)

派生類中的覆蓋方法可能是想要擴充而不是簡單的替代基類中的重名方法。有一個簡單的方法可以直接調用基類方法,只要調用: BaseClassName.methodname(self, arguments)。有時這對于客戶也很有用。(要注意只有 BaseClassName 在同一全局作用域定義或導入時才能這樣用。)

Python 有兩個用于繼承的函數:

  • 函數 isinstance() 用于檢查實例類型: isinstance(obj, int) 只有在 obj.__class__ 是 int 或其它從 int 繼承的類型

  • 函數 issubclass() 用于檢查類繼承: issubclass(bool, int) 為 True,因為 bool 是 int 的子類。

    然而, issubclass(float, int) 為 False,因為 float 不是 int 的子類。

9.5.1. 多繼承

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/ 。

9.6. 私有變量

只能從對像內部訪問的“私有”實例變量,在 Python 中不存在。然而,也有一個變通的訪問用于大多數 Python 代碼:以一個下劃線開頭的命名(例如 _spam )會被處理為 API 的非公開部分(無論它是一個函數、方法或數據成員)。它會被視為一個實現細節,無需公開。

因為有一個正當的類私有成員用途(即避免子類里定義的命名與之沖突),Python 提供了對這種結構的有限支持,稱為 name mangling (命名編碼) 。任何形如 __spam 的標識(前面至少兩個下劃線,后面至多一個),被替代為 _classname__spam ,去掉前導下劃線的 classname 即當前的類名。此語法不關注標識的位置,只要求在類定義內。

名稱重整是有助于子類重寫方法,而不會打破組內的方法調用。例如:

 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__ 一樣。

9.7. 補充

有時類似于 Pascal 中“記錄(record)”或 C 中“結構(struct)”的數據類型很有用,它將一組已命名的數據項綁定在一起。一個空的類定義可以很好的實現它:

 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 代碼需要一個特殊的抽象數據結構的話,通常可以傳入一個類,事實上這模仿了該類的方法。例如,如果你有一個用于從文件對象中格式化數據的函數,你可以定義一個帶有 read() 和 readline() 方法的類,以此從字符串緩沖讀取數據,然后將該類的對象作為參數傳入前述的函數。

實例方法對象也有屬性:m.__self__ 是一個實例方法所屬的對象,而 m.__func__ 是這個方法對應的函數對象。

9.8. 異常也是類

用戶自定義異常也可以是類。利用這個機制可以創建可擴展的異常體系。

以下是兩種新的,有效的(語義上的)異常拋出形式,使用 raise 語句:

 raise Class    raise Instance  

第一種形式中,Class 必須是 type 或其派生類的一個實例。第二種形式是以下形式的簡寫:

 raise Class()  

發生的異常其類型如果是 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")  

要注意的是如果異常子句的順序顛倒過來( execpt B 在最前),它就會打印 B,B,B--第一個匹配的異常被觸發。

打印一個異常類的錯誤信息時,先打印類名,然后是一個空格、一個冒號,然后是用內置函數 str() 將類轉換得到的完整字符串。

9.9. 迭代器

現在你可能注意到大多數容器對象都可以用 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 中普遍而且統一。在后臺, for 語句在容器對象中調用 iter() 。該函數返回一個定義了 __next__() 方法的迭代器對象,它在容器中逐一訪問元素。沒有后續的元素時, __next__() 拋出一個 StopIteration 異常通知 for 語句循環結束。你可以是用內建的 next() 函數調用 __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  

了解了迭代器協議的后臺機制,就可以很容易的給自己的類添加迭代器行為。定義一個 __iter__() 方法,使其返回一個帶有 __next__() 方法的對象。如果這個類已經定義了 __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. 生成器

Generator 是創建迭代器的簡單而強大的工具。它們寫起來就像是正規的函數,需要返回數據的時候使用 yield 語句。每次 next() 被調用時,生成器回復它脫離的位置(它記憶語句最后一次執行的位置和所有的數據值)。以下示例演示了生成器可以很簡單的創建出來:

 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 異常。綜上所述,這些功能使得編寫一個正規函數成為創建迭代器的最簡單方法。

9.11. 生成器表達式

有時簡單的生成器可以用簡潔的方式調用,就像不帶中括號的鏈表推導式。這些表達式是為函數調用生成器而設計的。生成器表達式比完整的生成器定義更簡潔,但是沒有那么多變,而且通常比等價的鏈表推導式更容易記。

例如:

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

Footnotes

[1] 有一個例外。模塊對象有一個隱秘的只讀對象,名為 __dict__ ,它返回用于實現模塊命名空間的字典,命名 __dict__ 是一個屬性而非全局命名。顯然,使用它違反了命名空間實現的抽象原則,應該被嚴格限制于調試中。

 

相關推薦
Python入門簡介 Python能做什么?
調用Python 解釋器 什么是Python 解釋器?
Python3 簡介
Python入門教程之流程控制
Python入門教程之數據結構
Python入門教程之模塊
Python入門教程之輸入和輸出
Python入門教程之錯誤和異常處理
Python入門教程之標準庫概覽
Python入門教程之標準庫瀏覽 – Part II
Python入門教程之虛擬環境和包
Python入門教程之浮點數算法:爭議和限制

精彩推薦

熱門教程

忍者法宝试玩
体球网nba 森林龙江麻将五常版下载 众彩网河南11选5走势图 手机棋牌赌博的骗局 3d直选复式计算器 湖北快三最大遗漏665 排五综合走势图 后三万能99%中奖率 内蒙古快三预测视频 比分直播500 迅盈篮球nba比分直播 重庆时时彩骗局 龙虎合 甘肃彩票快3玩法表 足球直播比分网 快乐十分计划免费版 明日之后9级庄园采集职业赚钱