<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 第九章 魔法方法、屬性和迭代器 > 來源:http://www.cnblogs.com/Marlowes/p/5437223.html > 作者:Marlowes 在Python中,有的名稱會在前面和后面都加上兩個下劃線,這種寫法很特別。前面幾章中已經出現過一些這樣的名稱(如`__future__`),這種拼寫表示名字有特殊含義,所以絕不要在自己的程序中使用這樣的名字。在Python中,由這些名字組成的集合所包含的方法稱為魔法(或特殊)方法。如果對象實現了這些方法中的某一個,那么這個方法會在特殊的情況下(確切地說是根據名字)被Python調用。而幾乎沒有直接調用它們的必要。 本章會詳細討論一些重要的魔法方法(最重要的是`__init__`方法和一些處理對象訪問的方法,這些方法允許你創建自己的序列或者映射)。本章還會講兩個相關的主題:屬性(在以前版本的Python中通過魔法方法來處理,現在通過`property`函數)和迭代器(使用魔法方法`__iter__`來允許迭代器在`for`循環中使用),本章最后還有一個相關的示例,其中有些知識點已經介紹過,可以用于處理一些相對復雜的問題。 ## 9.1 準備工作 很久以前(Python2.2中),對象的工作方式就有了很大的改變。這種改變產生了一些影響,對于剛開始使用Python的人來說,大多數改變都不是很重要(在Alex Martelli所著的《Python技術手冊》(_Python in a Nutshell_)的第八章有關與舊式和新式類之間區別的深入討論)。值得注意的是,盡管可能使用的是新版的Python,但一些特性(比如屬性和`super`函數)不會在舊式的類上起作用。為了確保類是新式的,應該把賦值語句`__metaclass__ = type`放在你的模塊的最開始(第七章提到過),或者(直接或者間接)子類化內建類(實際上是類型)`object`(或其他一些新式類)。考慮下面的兩個類。 ``` class NewStyle(object): more_code_here class OldStyle: more_code_here ``` 在這兩個類中,`NewStyle`是新式的類,`OldStyle`是舊式的類。如果文件以`__metaclass__ = type`開始,那么兩個類都是新式類。 _注:除此之外,還可以在自己的類的作用域中對`__metaclass__`變量賦值。這樣只會為這個類設定元類。元類就是其他類(或類型)的類——這是個更高及的主題。要了解關于元類的更多信息,請參見Guido van Rossum的技術性文章[Unifying types and classes in Python 2.2](http://python.org/2.2/descrintro.html)。或者在互聯網上搜索術語python metaclasses。_ 在本書所有示例代碼中都沒有顯式地設定元類(或者子類化`object`)。如果要沒有兼容之前舊版本Python的需要,建議你將所有類寫為新式的類,并使用`super`函數(稍后會在9.2.3節討論)這樣的特性。 _注:在Python3.0中沒有“舊式”的類,也不需要顯式地子類化`object`或者將元類設為`type`。所有的類都會隱式地成為`object`的子類——如果沒有明確超類的話,就會直接子類化;否則會間接子類化。_ ## 9.2 構造方法 首先要討論的第一個魔法方法是構造方法。如果讀者以前沒有聽過_構造方法_這個詞,那么說明一下:構造方法是一個很奇特的名字,它代表著類似于以前例子中使用過的那種名為`init`的初始化方法。但構造方法和其他普通方法不同的地方在于,當一個對象被創建后,會立即調用構造方法。因此,剛才我做的那些工作現在就不用做了: ``` >>> f = FooBar() >>> f.init() # 構造方法能讓它簡化成如下形式: >>> f = FooBar() ``` 在Python中創建一個構造方法很容易。只要把`init`方法的名字從簡單的`init`修改為魔法版本`__init__`即可: ``` >>> class FooBar: ... def __init__(self): ... self.somevar = 19 ... >>> f = FooBar() >>> f.somevar 19 ``` 現在一切都很好。但如果給構造方法傳幾個參數的話,會有什么情況發生呢?看看下面的代碼: ``` class FooBar: def __init__(self, value=19): self.somevar = value ``` 你認為可以怎樣使用它呢?因為參數是可選的,所以你可以繼續,,當什么事也沒發生。但如果要使用參數(或者不讓參數是可選的)的時候會發生什么?我相信你已經猜到了,一起來看看結果吧: ``` >>> f = FooBar("This is a constructor argument") >>> f.somevar 'This is a constructor argument' ``` 在Python所有的魔法方法中,`__init__`是使用最多的一個。 _注:Python中有一個魔法方法叫做`__del__`,也就是析構方法。它在對象就要被垃圾回收之前調用。但發生調用的具體時間是不可知的。所以建議讀者盡力避免使用`__del__`函數。_ ### 9.2.1 重寫一般方法和特殊的構造方法 第七章中介紹了繼承的知識。每個類都可能擁有一個或多個超類,它們從超類那里繼承行為方式。如果一個方法在B類的一個實例中被調用(或一個屬性被訪問),但在B類中沒有找到該方法,那么就會去它的超類A里面找。考慮下面的兩個類: ``` class A: def Hello(self): print "Hello, I'm A." class B(A): pass ``` A類定義了一個叫做`Hello`的方法,被B類繼承。下面是一個說明類是如何工作的例子: ``` >>> a = A() >>> b = B() >>> a.hello() Hello, I'm A. >>> b.hello() Hello, I'm A. ``` 因為B類沒有定義自己的`Hello`方法,所以當`hello`被調用時,原始的信息就被打印出來。 在子類中增加功能的最基本的方法就是增加方法。但是也可以重寫一些超類的方法來自定義繼承的行為。B類也能重寫這個方法。比如下面的例子中B類的定義就被修改了。 ``` class B(A): def hello(self): print "Hello, I'm B." ``` 使用這個定義,`b.hello()`能產生一個不同的結果。 ``` >>> b = B() >>> b.hello() Hello, I'm B. ``` 重寫是繼承機制中的一個重要內容,對于構造方法尤其重要。構造方法用來初始化新創建對象的狀態,大多數子類不僅要擁有自己的初始化代碼,還要擁有超類的初始化代碼。雖然重寫的機制對于所有方法來說都是一樣的,但是當處理構造方法比重寫方法時,更可能遇到特別的問題:如果一個類的構造方法被重寫,那么就需要調用超類(你所繼承的類)的構造方法,否則對象可能不會被正確地初始化。 考慮下面的`Bird`類: ``` class Bird: def __init__(self): self.hungry = True def eat(self): if self.hungry: print "Aaaah..." self.hungry = False else: print "No, thanks!" ``` 這個類定義所有的鳥都具有的一些最基本的能力:吃。下面就是這個類的用法示例: ``` >>> b = Bird() >>> b.eat() Aaaah... >>> b.eat() No, thanks! ``` 就像能在這個例子中看到的,鳥吃過了以后,它就不再饑餓。現在考慮子類`SongBird`,它添加了唱歌的行為: ``` class SongBird(Bird): def __init__(self): self.sound = "Squawk!" def sing(self): print self.sound ``` `SongBird`類和`Bird`類一樣容易使用: ``` >>> sb = SongBird() >>> sb.sing() Squawk! ``` 因為`SongBird`和`Bird`的一個子類,它繼承了`eat`方法,但如果調用`eat`方法,就會產生一個問題: ``` >>> sb.eat() Traceback (most recent call last): File "<stdin>", line 1, in ? File "Magic_Methods.py", line 27, in eat if self.hungry: AttributeError: SongBird instance has no attribute 'hungry' ``` 異常很清楚地說明了錯誤:`SongBIrd`沒有`hungry`特性。原因是這樣的:在`SongBird`中,構造方法被重寫,但新的構造方法沒有任何關于初始化`hungry`特性的代碼。為了達到預期的效果,`SongBird`的構造方法必須調用其超類`Bird`的構造方法來確保進行基本的初始化。有兩種方法能達到這個目的:調用超類構造方法的未綁定版本,或者使用super函數。后面兩節會介紹這兩種技術。 ### 9.2.2 調用未綁定的超類構造方法 本節所介紹的內容只要是歷史遺留問題。在目前版本的Python中,使用`super`函數(下一節會介紹)會更為簡單明了(在Python3.0中更是如此)。但是很多遺留的代碼還會使用本節介紹的方法,所以讀者還是要了解一些。而且它很有用,這是一個了解綁定和未綁定方法之間區別的好例子。 那么下面進入實際內容。不要被本節的標題嚇到,放松。其實調用超類的構造方法很容易(也很有用)。下面我先給出在上一節末尾提出的問題的解決方法。 ``` class SongBird(Bird): def __init__(self): Bird.__init__(self) self.sound = "Squawk!" def sing(self): print self.sound ``` `SongBird`類中至添加了一行代碼——`Bird.__init__(self)`。在解釋這句話真正的含義之前,首先來演示一下執行效果: ``` >>> sb = SongBird() >>> sb.sing() Squawk! >>> sb.eat() Aaaah... >>> sb.eat() No, thanks! ``` 為什么會有這樣的結果?在調用一個實例的方法時,該方法的`self`參數會被自動綁定到實例上(這稱為綁定方法)。前面已經給出幾個類似的例子了。但是如果直接調用類的方法(比如`Bird.__init__`),那么就沒有實例會被綁定。這樣就可以自由地提供需要的`self`參數。這樣的方法稱為未綁定(unbound)方法,也就是這節標題中所提到的。 通過將當前的實例作為`self`參數提供給未綁定方法,`SongBird`就能夠使用其超類構造方法的所有實現,也就是說屬性`hungry`能被設置。 9.2.3 使用`super`函數 如果讀者不想堅守舊版本的Python陣營,那么就應該使用`super`函數。它只能在新式類中使用,不管怎么樣,你都應該盡量使用新式類。當前的類和對象可以作為`super`函數的參數使用,調用函數返回的對象的任何方法都是調用超類的方法,而不是當前類的方法。那么就可以不用在SongBird構造方法中使用`Bird`,而直接調用`super(SongBird, self`)。除此之外,`__init__`方法能以一個普通的(綁定)方式被調用。 _注:在Python3.0中,`super`函數可以不帶任何參數進行調用,功能依然具有“魔力”。_ 下面的例子是對`Bird`(注意`Bird`是`object`的子類,這樣它就成為了一個新式類)例子的更新: ``` # super函數只在新式類中起作用 __metaclass__ = type class Bird: def __init__(self): self.hungry = True def eat(self): if self.hungry: print "Aaaah..." self.hungry = False else: print "No, thanks!" class SongBird(Bird): def __init__(self): super(SongBird, self).__init__() self.sound = "Squawk!" def sing(self): print self.sound ``` 這個新式的版本的運行結果和舊式版本的一樣: ``` >>> sb = SongBird() >>> sb.sing() Squawk! >>> sb.eat() Aaaah... >>> sb.eat() No, thanks! ``` **為什么`super`函數這么超級** 在我看來,`super`函數比在超類中直接調用未綁定方法更直觀。但這并不是它的唯一優點。`super`函數實際上是很智能的,因此即使類已經繼承多個超類,它也只需要使用一次`super`函數(但要確保所有的超類的構造方法都使用了`super`函數)。在一些含糊的情況下使用舊式類會很別扭(比如兩個超類共同繼承一個超類),但能被新式類和`super`函數自動處理。內部的具體工作原理不用理解,但必須清楚地知道:在大多數情況下,使用新式類和super函數是比調用超類的未綁定的構造方法(或者其他的方法)更好的選擇。 那么,`super`函數到底返回什么?一般來說讀者不用擔心這個問題,就假裝它返回的所需的超類好了。實際上它返回了一個`super`對象,這個對象負責進行方法解析。當對其特性進行訪問時,它會查找所有的超類(以及超類的超類),直到找到所需的特性為止(或者引發一個`AttributeError`異常)。 ## 9.3 成員訪問 盡管`__init__`是目前為止提到的最重要的特殊方法,但還有一些其他的方法提供的作用也很重要。本節會討論常見的魔法方法的集合,它可以創建行為類似于序列或映射的對象。 基本的序列和映射的規則很簡單,但如果要實現它們全部功能就需要實現很多魔法函數。幸好,還是有一些捷徑的,下面馬上會說到。 _注:規則(protocol)這個詞在Python中會經常使用,用來描述管理某種形式的行為的規則。這與第七章中提到的接口的概念有點類似。規則說明了應該實現何種方法和這些方法應該做什么。因為Python中的多態性是基于對象的行為的(而不是基于祖先,例如它所屬的類或超類,等等)。這是一個重要的概念:在其他的語言中對象可能被要求屬于某一個類,或者被要求實現某個接口,但Python中只是簡單地要求它遵循幾個給定的規則。因此成為了一個序列,你所需要做的只是遵循序列的規則。_ ### 9.3.1 基本的序列和映射規則 序列和映射是對象的集合。為了實現它們基本的行為(規則),如果對象是不可變的,那么就需要使用兩個魔法方法,如果是可變的則需要四個。 ? `__len__(self)`:這個方法應該返回級和中所含項目的數量。對于序列來說,這就是元素的個數;對于映射來說,則是鍵-值對的數量。如果`__len__`返回0(并且沒有實現重寫該行為的`__nozero__`),對象會被當作一個布爾變量中的假值(空的列表、元組、字符串和字典也一樣)進行處理。 ? `__getitem__(self, key)`:這個方法返回與所給的鍵對應的值。對于一個序列,鍵應該是一個0~n-1的整數(或者像后面所說的負數),n是序列的長度;對于映射來說,可以使用任何種類的鍵。 ? `__setitem__(self, key, value)`:這個方法應該按一定的方式存儲和key相關的value,該值隨后可使用`__getitem__`來獲取。當然,只能為可以修改的對象定義這個方法。 ? `__delitem__(self, key)`:這個方法在對一部分對象使用del語句時被調用,同時必須刪除和鍵相關的鍵。這個方法也是為可修改的對象定義的(并不是刪除全部的對象,而只刪除一些需要移除的元素)。 對于這些方法的附加要求如下。 ? 對于一個序列來說,如果鍵是負整數,那么要從末尾開始計數。換句話說就是`x[-n]`和`x[len(x)-n]`是一樣的。 ? 如果鍵是不合適的類型(例如,對序列使用字符串作為鍵),會引發一個`TypeError`異常。 ? 如果序列的索引是正確的類型,但超出了范圍,應該引發一個`IndexError`異常。 讓我們實踐一下,看看如果創建一個無窮序列,會發生什么: ``` __metaclass__ = type def checkindex(key): """ 所給的鍵是能接受的索引嗎? 為了能被接受,鍵應該是一個非負的整數。如果它不是一個整數,會引發TypeError; 如果它是負數,則會引發IndexError(因為序列是無限長的)。 """ if not isinstance(key, (int, long)): raise TypeError if key < 0: raise IndexError class ArithmeticSequence: def __init__(self, start=0, step=1): """ 初始化算術序列 起始值——序列中的第一個值 步長——兩個相鄰值之間的差別 改變——用戶修改的值的字典 """ self.start = start # 保存開始值 self.step = step # 保存步長值 self.changed = {} # 沒有項被修改 def __getitem__(self, key): """ Get an item from the arithmetic sequence. """ checkindex(key) try: # 修改了嗎? return self.changed[key] except KeyError: # 否則······ # ······計算值 return self.start + key * self.step def __setitem__(self, key, value): """ 修改算術序列中的一個項 """ checkindex(key) # 保存更改后的值 self.changed[key] = value ``` 這里實現的是一個_算術序列_,該序列中的每個元素都比它前面的元素大一個常數。第一個值是由構造方法參數`start`(默認為0)給出的,而值與值之間的步長是由`step`設定的(默認為1).用戶能將特例規則保存在名為`changed`的字典中,從而修改一些元素的值,如果元素沒有被修改,那就計算`self.start + key * self.steo`的值。 下面是如何使用這個類的例子: ``` >>> s = ArithmeticSequence(1, 2) >>> s[4] 9 >>> s[4] = 2 >>> s[4] 2 >>> s[5] 11 ``` 注意,沒有實現`__del__`方法的原因是我希望刪除元素是非法的: ``` >>> del s[4] Traceback (most recent call last): File "ArithmeticSequence.py", line 66, in <module> del s[4] AttributeError: __delitem__ ``` 這個類沒有`__len__`方法,因為它是無限長的。 如果使用了一個非法類型的索引,就會引發`TypeError`異常,如果索引的類型是正確的但超出了范圍(在本例中為負數),則會引發`IndexError`異常: ``` >>> s["four"] Traceback (most recent call last): File "/home/marlowes/Program/Py_Project/ArithmeticSequence.py", line 66, in <module> s["four"] File "/home/marlowes/Program/Py_Project/ArithmeticSequence.py", line 44, in __getitem__ checkindex(key) File "/home/marlowes/Program/Py_Project/ArithmeticSequence.py", line 18, in checkindex raise TypeError TypeError >>> s[-42] Traceback (most recent call last): File "/home/marlowes/Program/Py_Project/ArithmeticSequence.py", line 66, in <module> s[-42] File "/home/marlowes/Program/Py_Project/ArithmeticSequence.py", line 44, in __getitem__ checkindex(key) File "/home/marlowes/Program/Py_Project/ArithmeticSequence.py", line 21, in checkindex raise IndexError IndexError ``` 索引檢查是通過用戶自定義的`checkindex`函數實現的。 有一件關于`checkindex`函數的事情可能會讓人吃驚,即`isinstance`函數的使用(這個函數應盡量避免使用,因為類或類型檢查和Python中多態的目標背道而馳)。因為Python的語言規范上明確指出索引必須是整數(包括長整數),所以上面的代碼才會如此使用。遵守標準是使用類型檢查的(很少的)正當理由之一。 注:分片操作也是可以模擬的。當對支持`__getitem__`方法的實例進行分片操作時,分片對象作為鍵提供。分片對象在[Python庫參考](http://python.org/doc/lib)的2.1節中slice函數部分有介紹。Python2.5有一個更加專門的方法叫做`__index__`,它允許你在分片中使用非整形限制。只要你想處理基本序列規則之外的事情,那么這個方法尤其有用。 ### 9.3.2 子類化列表,字典和字符串 到目前為止本書已經介紹了基本的序列/映射規則的4個方法,官方語言規范也推薦實現其他的特殊方法和普通方法(參見[Python參考手冊的3.4.5節] http://www.python.org/doc/ref/sequence-types.html),包括9.6節描述的`__iter\__`方法在內。實現所有的這些方法(為了讓自己的對象和列表或者字典一樣具有多態性)是一項繁重的工作,并且很難做好。如果只想在一個操作中自定義行為,那么其他的方法就不用實現。這就是程序員的懶惰(也是常識)。 那么應該怎么做呢?關鍵詞是_繼承_。能繼承的時候為什么還要全部實現呢?標準庫有3個關于序列和映射規則(`UserList`、`UserString`和`UserDict`)可以立即使用的實現,在較新版本的Python中,可以子類化內建類型(注意,如果類的行為和默認的行為很接近這就很有用,如果需要重新實現大部分方法,那么還不如重新寫一個類)。 因此,如果希望實現一個和內建列表行為相似的序列,可以子類化`list`來實現。 _注:當子類化一個內建類型——比如`list`的時候,也就間接地將`object`子類化了。因此該類就自動成為新式類,這就意味著可以使用像`super`函數這樣的特性了。_ 下面看看例子,帶有訪問計數的列表。 ``` __metaclass__ = type class CounterList(list): def __init__(self, *args): super(CounterList, self).__init__(*args) self.counter = 0 def __getitem__(self, index): self.counter += 1 return super(CounterList, self).__getitem__(index) ``` `CounterList`類嚴重依賴于它的超類(`list`)的行為。`CounterList`類沒有重寫的任何方法(和`append`、`extend`、`index`一樣)都能被直接使用。在兩個被重寫的方法中,`super`方法被用來調用相應的超類的方法,只在`__init__`中添加了所需的初始化`counter`特性的行為,并在`__getitem__`中更新了`counter`特性。 _注:重寫`__getitem__`并非獲取用戶訪問的萬全之策,因為還有其他訪問列表內容的途徑,比如通過`pop`方法。_ 這里是`CounterList`如何使用的例子: ``` >>> cl = CounterList(range(10)) >>> cl [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> cl.reverse() >>> cl [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] >>> del cl[3:6] >>> cl [9, 8, 7, 3, 2, 1, 0] >>> cl.counter 0 >>> cl[4] + cl[2] 9 >>> cl.counter 2 ``` 正如看到的,`CounterList`在很多方面和列表的作用一樣,但它有一個`counter`特性(被初始化為0),每次列表元素被訪問時,它都會自增,所以在執行加法`cl[4] + cl[2]`后,這個值自增兩次,變為2。 ## 9.4 更多魔力 魔法名稱的用途有很多,我目前所演示的只是所有用途中的一小部分。大部分的特殊方法都是為高級的用法準備的,所有我不會在這里詳細討論。單是如果感興趣,可以模擬數字,讓對象像函數那樣被調用,影響對象的比較,等等。關于特殊函數的更多內容請參考[《Python參考手冊》中的3.4節](http://www.python.org/doc/ref/specialnames.html)。 ## 9.5 屬性 第七章曾經提到過訪問器方法。訪問器是一個簡單的方法,它能夠使用`getHeight`、`setHeight`這樣的名字來得到或者重綁定一些特性(可能是類的私有屬性——具體內容請參見第七章的“再論私有化”的部分)。如果在訪問給定的特性時必須要采取一些行動,那么像這樣的封裝狀態變量(特性)就很重要。比如下面例子中的`Rectangle`類: ``` __metaclass__ = type class Rectangle: def __init__(self): self.width = 0 self.height = 0 def setSize(self, size): self.width, self.height = size def getSize(self): return self.width, self.height ``` 下面的例子演示如何使用這個類: ``` >>> r = Rectangle() >>> r.width = 10 >>> r.height = 5 >>> r.getSize() (10, 5) >>> r.setSize((150, 100)) >>> r.width 150 ``` 在上面的例子中,`getSize`和`setSize`方法是一個名為`size`的假想特性的訪問器方法,`size`是由`width`和`height`構成的元組。讀者可以隨意使用一些更有趣的方法替換這里的函數,例如計算面積或者對角線的長度。這些代碼沒錯,但卻有缺陷。當程序員使用這個類時不應該還要考慮它是怎么實現的(封裝)。如果有一天要改變類的實現,將`size`變成一個真正的特性,這樣`width`和`height`就可以動態算出,那么就要把它們放到一個訪問器方法中去,并且任何使用這個類的程序都必須重寫。客戶代碼(使用代碼的代碼)應該能夠用同樣的方式對待所有特性。 那么怎么解決呢?把所有的屬性都放到訪問器方法中?這當然沒問題。但如果有很多簡單的特性,那么就很不現實了(有點笨)。如果那樣做就得寫很多訪問器方法,它們除了返回或者設置特性就不做任何事了。_復制加粘貼式_或_切割代碼式_的編程方式顯然是很糟糕的(盡管是在一些語言中針對這樣的特殊問題很普遍)。幸好,Python能隱藏訪問器方法,讓所有特性看起來一樣。這些通過訪問器定義的特性被稱為_屬性_。 實際上在Python中有兩種創建屬性的機制。我主要討論新的機制——只在新式類中使用的property函數,然后我會簡單地說明一下如何使用特殊方法實現屬性。 ### 9.5.1 `property`函數 property函數的使用很簡單。如果已經編寫了一個像上節的`Rectangle`那樣的類,那么只要增加一行代碼(除了要子類化`object`,或者使用`__metaclass__ = type`語句以外): ``` __metaclass__ = type class Rectangle: def __init__(self): self.width = 0 self.height = 0 def setSize(self, size): self.width, self.height = size def getSize(self): return self.width, self.height size = property(getSize, setSize) ``` 在這個新版的`Rectangle`中,`property`函數創建了一個屬性,其中訪問器函數被用作參數(先是取值,然后是賦值),這個屬性命名為`size`。這樣一來就不再需要擔心是怎么實現的了,可以用同樣的方式處理`width`、`height`和`size`。 ``` >>> r = Rectangle() >>> r.width = 10 >>> r.height = 5 >>> r.size (10, 5) >>> r.size = 150, 100 >>> r.width 150 ``` 很明顯,`size`特性仍然取決于`getSize`和`setSize`中的計算。但它們看起來就像普通的屬性一樣。 _注:如果屬性的行為很奇怪,那么要確保你所使用的類為新式類(通過直接或間接子類化`object`,或直接設置元類);如果不是的話,雖然屬性的取值部分還是可以工作,但賦值部分就不一定了(取決于Python的版本),這很讓人困惑。_ 實際上,`property`函數可以用0、1、3或者4個參數來調用。如果沒有參數,產生的屬性既不可讀,也不可寫。如果只使用一個參數調用(一個取值方法),產生的屬性是只讀的。第3個參數(可選)是一個用于_刪除_特性的方法(它不要參數)。第4個參數(可選)是一個文檔字符串。`property`的四個參數分別被叫做`fget`、`fset`、`fdel`和`doc`,如果想要第一個屬性是只寫的,并且有一個文檔字符串,可以使用關鍵字參數的方式來實現。 盡管這一節很短(只是對`property`函數的簡單說明),但它卻十分的重要。理論上說,在新式類中應該使用`property`函數而不是訪問器方法。 **它是如何工作的** 有的讀者很想知道`property`函數是如何實現它的功能的,那么在這里解釋一下,不感興趣的讀者可以跳過。實際上,`property`函數不是一個真正的函數,它是其實例擁有很多特殊方法的類,也正是那些方法完成了所有的工作。涉及的方法是`__get__`、`__set__`和`__delete__`。這3個方法和在一起,就定義了描述符規則。實現了其中任何一個方法的對象就叫描述符(descriptor)。描述符的特殊之處在于它們使如何被訪問的。比如,程序讀取一個特性時(尤其是在實例中訪問該特性,但該特性在類中定義時),如果該特性被綁定到了實現了`__get__`方法的對象上,那么就會調用`__get__`方法(結果值也會被返回),而不只是簡單地返回對象。實際上這就是屬性的機制,即綁定方法,靜態方法和類成員方法(下一節會介紹更多的信息)還有`super`函數。[《Python參考手冊》](http://python.org/doc/ref/descriptors.html)包括有關描述符規則的簡單說明。一個更全面的信息源是[Raymond Hettinger的How To Guide for Descriptors](http://users.rcn.com/python/download/Descriptor.html) ### 9.5.2 靜態方法和類成員方法 在討論實現屬性的舊方法前,先讓我們繞道而行,看看另一對實現方法和新式屬性的實現方法類似的特征。靜態方法和類成員方法分別在創建時分別被裝入`staticmethod`類型和`classmethod`類型對象中。靜態方法的定義沒有`self`參數,且能夠被類本身直接調用。類方法在定義時需要名為`cls`的類似于`self`的參數,類成員方法可以直接用類的具體對象調用。但`cls`參數是自動被綁定到類的,請看下面的例子: ``` __metaclass__ = type class MyClass: def smeth(): print "This is a static method" smeth = staticmethod(smeth) def cmeth(cls): print "This is a class method of", cls cmeth = classmethod(cmeth) ``` 手動包裝和替換方法的技術看起來有些單調,在Python2.4中,為這樣的包裝方法引入了一個叫做_裝飾器_(decorator)的新語法(它能對任何可調用的對象進行包裝,既能夠用于方法也能用于函數)。使用`@`操作符,在方法(或函數)的上方將裝飾器列出,從而指定一個或者更多的裝飾器(多個裝飾器在應用時的順序與指定順序相反)。 ``` __metaclass__ = type class MyClass: @staticmethod def smeth(): print "This is a static method" @classmethod def cmeth(cls): print "This is a class method of", cls ``` 定義了這些方法以后,就可以想下面的例子那樣使用(例子中沒有實例化類): ``` >>> MyClass.smeth() This is a static method >>> MyClass.cmeth() This is a class method of <class '__main__.MyClass'> ``` 靜態方法和類成員方法在Python中向來都不是很重要,主要原因是大部分情況下可以使用函數或者綁定方法代替。在早期的版本中沒有得到支持也是一個原因。但即使看不到兩者在當前代碼中的大量應用,也不要忽視靜態方法和類成員方法的應用(比如工廠函數),可以好好地考慮一下使用新技術。 ### 9.5.3 `__getattr__`、`__setattr__`和它的朋友們 攔截(intercept)對象的所有特性訪問是可能的,這樣可以用舊式類實現屬性(因為`property`方法不能使用)。為了在訪問特性的時候可以執行代碼。必須使用一些魔法方法。下面的4種方法提供了需要的功能(在舊式類中只需要后3個)。 ? `__getattribute__(self, name)`:當特性`name`被訪問時自動被調用(只能在新式類中使用)。 ? `__getattr__(self, name)`:當特性`name`被訪問且對象沒有相應的特性時被自動調用。 ? `__setattr__(self, name, value)`:當試圖給特性`name`賦值時會被自動調用。 ? `__delatr__(self, name)`:當試圖刪除特性`name`時被自動調用。 盡管和使用`property`函數相比有點復雜(而且在某些方面效率更高),單這些特殊方法是很強大的,因為可以對處理很多屬性的方法進行再編碼。 下面還是`Rectangle`的例子,但這次使用的是特殊方法: ``` __metaclass__ = type class Rectangle: def __init__(self): self.width = 0 self.height = 0 def __setattr__(self, name, value): if name == "size": self.width, self.height = value else: self.__dict__[name] = value def __getattr__(self, name): if name == "size": return self.width, self.height else: raise AttritubeError ``` 這個版本的類需要注意增加的管理細節。當思考這個例子時,下面的兩點應該引起讀者的重視。 ? `__setattr__`方法在所涉及的特性不是`size`時也會被調用。因此,這個方法必須把兩方面都考慮進去:如果屬性是`size`,那么就像前面那樣執行操作,否則就要使用特殊方法`__dict__`,該特殊方法包含一個字典,字典里面是所有實例的屬性。為了避免`__setattr__`方法被再次調用(這樣會使程序陷入死循環),`__dict__`方法被用來代替普通的特性賦值操作。 ? `__getattr__`方法只在普通的特性沒有被找到的時候調用,這就是說如果給定的名字不是size,這個特性不存在,這個方法會引起一個`AttritubeError`異常。如果希望類和`hasattr`或者是`getattr`這樣的內建函數一起正確地工作,`__getattr__`方法就很重要。如果使用的是`size`屬性,那么就會使用在前面的實現中找到的表達式, _注:就像死循環陷阱和`__setattr__`有關系一樣,還有一個陷阱和`__getattribute__`有關系。因為`__getattribute__`攔截所有特性的訪問(在新式類中),也攔截對`__dict__`的訪問!訪問`__getattribute__`中與`self`相關的特性時,使用超類的`__getattribute__`方法(使用`super`函數)是唯一安全的途徑。_ ## 9.6 迭代器 在前面的章節中,我提到過迭代器(和可迭代),本節將對此進行深入討論。只討論一個特殊方法——`__iter__`,這個方法是迭代器規則(iterator protocol)的基礎。 ### 9.6.1 迭代器規則 _迭代_的意思是重復做一些事情很多次,就像在循環中做的那樣。到現在為止只在for循環中對序列和字典進行過迭代,但實際上也能對其他對象進行迭代:只要該對象實現了`__iter__`方法。 `__iter__`方法會返回一個迭代器(iterator),所謂的迭代器就是具有`next`方法(這個方法在調用時不需要任何參數)的對象。在調用`next`方法時,迭代器會返回它的下一個值。如果`next`方法被調用,但迭代器沒有值可以返回,就會引發一個`StopIteration`異常。 _注:迭代器規則在Python3.0中有一些變化。在新的規則中,迭代器對象應該實現`__next__`方法,而不是`next`。而新的內建函數`next`可以用于訪問這個方法。換句話說,`next(it)`等同于3.0之前版本中的`it.next()`。_ 迭代規則的關鍵是什么?為什么不使用列表?因為列表的殺傷力太大。如果有一個函數,可以一個接一個地計算值,那么在使用時可能是計算一個值時獲取一個值——而不是通過列表一次性獲取所有值。如果有很多值,列表就會占用太多的內存。但還有其他的理由:使用迭代器更通用、更簡單、更優雅。讓我們看看一個不使用列表的例子,因為要用的話,列表的長度必須無限。 這里的“列表”是一個斐波那契數列。使用的迭代器如下: ``` __metaclass__ = type class Fibs: def __init__(self): self.a = 0 self.b = 1 def next(self): self.a, self.b = self.b, self.a + self.b return self.a def __iter__(self): return self ``` 注意,迭代器實現了`__iter__`方法,這個方法實際上返回迭代器本身。在很多情況下,`__iter__`會放到其他的會在`for`循環中使用的對象中。這樣一來,程序就能返回所需的迭代器。此外,推薦使迭代器實現它自己的`__iter__`方法,然后就能直接在`for`循環中使用迭代器本身了。 _注:正式的說法是,一個實現了`__iter__`方法的對象是可迭代的,一個實現了`__next__`方法的對象則是迭代器。_ 首先,產生一個`Fibs`對象: ``` >>> fibs = Fibs() ``` 可在`for`循環中使用該對象——比如去查找在斐波那契數列中比1000大的數中的最小的數: ``` for f in fibs: if f > 1000: print f break ··· 1597 ``` 因為設置了`break`,所以循環在這里停止了,否則循環會一直繼續下去。 _注:內建函數`list`可以從可迭代的對象中獲得迭代器。_ ``` >>> it = iter([1, 2, 3]) >>> it.next() 1 >>> it.next() 2 ``` _除此之外,它也可以從函數或者其他可調用對象中獲取可迭代對象(請參見[Python庫參考](http://docs.python.org/lib/)獲取更多信息)。_ ### 9.6.2 從迭代器得到序列 除了在迭代器和可迭代對象上進行_迭代_(這是經常做的)外,還能把它們轉換為序列。在大部分能使用序列的情況下(除了在索引或者分片等操作中),都能使用迭代器(或者可迭代對象)替換。關于這個的一個很有用的例子是使用`list`構造方法顯式地將迭代器轉化為列表。 ``` __metaclass__ = type class TestIterator: value = 0 def next(self): self.value += 1 if self.value > 10: raise StopIteration return self.value def __iter__(self): return self ··· >>> ti = TestIterator() >>> list(ti) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` ## 9.7 生成器 生成器是Python新引入的概念,由于歷史原因,它也叫_簡單生成器_。它和迭代器可能是近幾年來引入的最強大的兩個特性。但是,生成器的概念則要更高級一些,需要花些功夫才能理解它使如何工作的以及它有什么用處。生成器可以幫助讀者寫出非常優雅的代碼,當然,編寫任何程序時不使用生成器也是可以的。 生成器是一種用普通的函數語法定義的迭代器。它的工作方式可以用例子來很好地展現。讓我們先看看怎么創建和使用生成器,然后再了解一下它的內部機制。 ### 9.7.1 創建生成器 創建一個生成器就像創建函數一樣簡單。相信你已經厭倦了斐波那契數列的例子,所以下面會換一個例子來說明生成器的知識。首先創建一個展開嵌套列表的函數。參數是一個列表,和下面這個很像: ``` nested = [[1, 2], [3, 4], [5]] ``` 換句話說就是一個列表的列表。函數應該按順序打印出列表中的數字。解決的辦法如下: ``` def flatten(nested): for sublist in nested: for element in sublist: yield element ``` 這個函數的大部分是簡單的。首先迭代提供的嵌套列表中的所有子列表,然后按順序迭代子列表中的元素。如果最后一行是`print element`的話,那么就容易理解了,不是嗎? 這里的`yield`語句是新知識。任何包含`yield`語句的函數成為_生成器_。除了名字不同以外,它的行為和普通的函數也有很大的差別。這就在于它不是像`return`那樣返回值,而是每次產生多個值。每次產生一個值(使用`yield`語句),函數就會被_凍結_:即函數停在那點等待被重新喚醒。函數被重新喚醒后就從停止的那點開始執行。 接下來可以通過在生成器上迭代來使用所有的值。 ``` >>> nested = [[1, 2], [3, 4], [5]] >>> for num in flatten(nested): ··· print num 1 2 3 4 5 or >>> list(flatten(nested)) [1, 2, 3, 4, 5] ``` **循環生成器** Python2.4引入了列表推導式的概念(請參見第五章),生成器推導式(或稱生成器表達式)和列表推導式的工作方式類似,只不過返回的不是列表而是生成器(并且不會立刻進行循環)。所返回的生成器允許你像下面這樣一步一步地進行計算: ``` >>> g = ((i + 2) ** 2 for i in range(2, 27)) >>> g.next() 16 ``` 和列表推導式不同的就是普通圓括號的使用方式,在這樣簡單的例子中,還是推薦大家使用列表推導式。但如果讀者希望將可迭代對象(例如生成大量的值)“打包”,那么最好不要使用列表推導式,因為它會立即實例化一個列表,從而喪失迭代的優勢。 更妙的地方在于生成器推導式可以在當前的圓括號內直接使用,例如在函數調用中,不用增加另一對圓括號,換句話說,可以像下面這樣編寫代碼: ``` sum(i ** 2 for i in range(10)) ``` ### 9.7.2 遞歸生成器 上節創建的生成器只能處理兩層嵌套,為了處理嵌套使用了兩個`for`循環。如果要處理任意層的嵌套該怎么辦?例如,可能要使用來表示樹形結構(也可用于特定的樹類,但原理是一樣的)。每層嵌套需要增加一個`for`循環,但因為不知道有幾層嵌套,所以必須把解決方案變得更靈活。現在是求助于遞歸(recursion)的時候了。 ``` def flatten(nested): try: for sublist in nested: for element in flatten(sublist): yield element except TypeError: yield nested ``` 當`flatten`被調用時,有兩種可能性(處理遞歸時大部分都是有兩種情況):_基本_情況和_需要遞歸_的情況。在基本情況中,函數被告知展開一個元素(比如一個數字),這種情況下,`for`循環會引發一個`TypeError`異常(因為試圖對一個數字進行迭代),生成器會產生一個元素。 如果展開的是一個列表(或者其他可迭代對象),那么就要進行特殊處理。程序必須遍歷所有的子列表(一些可能不是列表),并對它們調用`flatten`。然后使用另一個`for`循環來產生被展開的子列表中的所有元素。這可能看起來有點不可思議,但卻能工作。 ``` list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8])) [1, 2, 3, 4, 5, 6, 7, 8] ``` 這么做只有一個問題:如果`nested`是一個類似于字符串的對象(字符串、`Unicode`、`UserString`,等等),那么它就是一個序列,不會引發`TypeError`,但是你不想對這樣的對象進行迭代。 _注:不應該在`flatten`函數中對類似于字符串的對象進行迭代,出于兩個主要的原因。首先,需要實現的是將類似于字符串的對象當成原子值,而不是當成應被展開的序列。其次,對它們進行迭代實際上會導致無窮遞歸,因為一個字符串的第一個元素是另一個長度為1的字符串,而長度為1的字符串的第一個元素就是字符串本身。_ 為了處理這種情況,則必須在生成器的開始處添加一個檢查語句。試著將傳入的對象和一個字符串拼接,看看會不會出現`TypeError`,這是檢查一個對象是不是類似于字符串的最簡單、最快速的方法(感謝Alex Martelli指出了這個習慣用法和在這里使用的重要性)。下面是加入了檢查語句的生成器: ``` def flatten(nested): try: # 不要迭代類似字符串的對象: try: nested + "" except TypeError: pass else: raise TypeError for sublist in nested: for element in flatten(sublist): yield element except TypeError: yield nested ``` 如果表達式`nested + ""`引發了一個`TypeError`,它就會被忽略。然而如果沒有引發`TypeError`,那么內層`try`語句中的`else`子句就會引發一個它自己的`TypeError`異常。這就會按照原來的樣子生成類似于字符串的對象(在`except`子句的外面),了解了嗎? 這里有一個例子展示了這個版本的類應用于字符串的情況: ``` >>> list(flatten(["foo", ["bar", ["baz"]]])) ['foo', 'bar', 'baz'] ``` 上面的代碼沒有執行類型檢查。這里沒有測試`nested`是否是一個字符串(可以使用`isinstance`函數完成檢查),而只是檢查`nested`的行為是不是像一個字符串(通過和字符串拼接來檢查)。 ### 9.7.3 通用生成器 如果到目前的所有例子你都看懂了,那應該或多或少地知道如何使用生成器了。生成器是一個包含`yield`關鍵字的函數。當它被調用時,在函數體中的代碼不會被執行,而會返回一個迭代器。每次請求一個值,就會執行生成器中的代碼,直到遇到一個`yield`或者`return`語句。`yield`語句意味著應該生成一個值。`return`語句意味著生成器要停止執行(不再生成任何東西,`return`語句只有在一個生成器中使用時才能進行無參數調用)。 換句話說,生成器是由兩部分組成:_生成器的函數_和_生成器的迭代器_。生成器的函數是用`def`語句定義的,包含`yield`部分,生成器的迭代器是這個函數返回的部分。按一種不是很準確的說法,兩個實體經常被當做一個,合起來叫做_生成器_。 ``` >>> def simple_generator(): ... yield 1 ... >>> simple_generator <function simple_generator at 0x7fc241cad578> >>> simple_generator() <generator object simple_generator at 0x7fc241cf1cd0> ``` 生成器的函數返回的迭代器可以像其他的迭代器那樣使用。 ### 9.7.4 生成器方法 生成器的新特征(在Python2.5中引入)是在開始運行后為生成器提供值的能力。表現為生成器和“外部世界”進行交流的渠道,要注意下面兩點。 ? 外部作用域訪問生成器的`send`方法,就像訪問`next`方法一樣,只不過前者使用一個參數(要發送的“消息”——任意對象)。 ? 在內部則掛起生成器,`yield`現在作為表達式而不是語句使用,換句話說,當生成器重新運行的時候,`yield`方法返回一個值,也就是外部通過`send`方法發送的值。如果`next`方法被使用,那么`yield`方法返回`None`。 注意,使用`send`方法(而不是`next`方法)只有在生成器掛起之后才有意義(也就是說在`yield`函數第一次被執行后)。如果在此之前需要給生成器提供更多信息,那么只需使用生成器函數的參數。 _注:如果真想對剛剛啟動的生成器使用`send`方法,那么可以將`None`作為其參數進行調用。_ 下面是一個非常簡單的例子,可以說明這種機制: ``` def repeater(value): while True: new = (yield value) if new is not None: value = new ``` 使用方法如下: ``` >>> r = repeater(42) >>> r.next() 42 >>> r.send("Hello, world!") "Hello, world!" ``` 注意看`yield`表達式周圍的圓括號的使用。雖然并未嚴格要求,但在使用返回值的時候,安全起見還是要閉合`yield`表達式。 生成器還有其他兩個方法(在Python2.5及以后的版本中)。 ? `throw`方法(使用異常類型調用,還有可選的值以及回溯對象)用于在生成器內引發一個異常(在`yield`表達式中)。 ? `close`方法(調用時不用參數)用于停止生成器。 `close`方法(在需要的時候也會由Python垃圾收集器調用)也是建立在異常的基礎上的。它在`yield`運行處引發一個`GeneratorExit`異常,所以如果需要在生成器內進行代碼清理的話,則可以將`yield`語句放在`try/finally`語句中。如果需要的話,還可以捕捉`GeneratorExit`異常,但隨后必須將其重新引發(可能在清理之后),引發另外一個異常或者直接返回。試著在生成器的`close`方法被調用后再通過生成器生成一個值則會導致`RuntimeError`異常。 注:有關更多生成器方法的信息,以及如何將生成器轉換為簡單的協同程序(coroutine)的方法,請參見[PEP 342](http://www.python.org/dev/peps/pep-0342/)。 ### 9.7.5 模擬生成器 生成器在舊版本的Python中是不可用的。下面介紹的就是如何使用普通的函數模擬生成器。 先從生成器的代碼開始。首先將下面語句放在函數體的開始處: ``` result = [] ``` 如果代碼已經使用了`result`這個名字,那么應該用其他的名字代替(使用一個更具有描述性的名字是一個好主意),然后將下面這種形式的代碼: ``` yield some_expression ``` 用下面的語句替換: ``` result.append(some_expression) ``` 最后,在函數的末尾,添加下面這條語句: ``` return result ``` 盡管這個版本可能不適用于所有生成器,但對大多數生成器來說是可行的(比如,它不能用于一個無限的生成器,當然不能把它的值放入列表中)。 下面是`flatten`生成器用普通的函數重寫的版本: ``` def flatten(nested): result = [] try: # 不要迭代類似字符串的對象: try: nested + "" except TypeError: pass else: raise TypeError for sublist in nested: for element in flatten(sublist): result.append(element) except TypeError: result.append(nested) return result ``` ## 9.8 八皇后的問題 現在已經學習了所有的魔法方法,是把它們用于實踐的時候了。本節會介紹如何使用生成器解決經典的變成問題。 ### 9.8.1 生成器和回溯 生成器是逐漸產生結果的復雜遞歸算法的理想實現工具。沒有生成器的話,算法就需要一個作為額外參數傳遞的半成品方案,這樣遞歸調用就可以在這個方案上建立起來。如果使用生成器,那么所有的遞歸調用只要創建自己的`yield`部分。前一個遞歸版本的`flatten`程序中使用的就是后一種做法,相同的策略也可以用在遍歷(Traverse)圖和樹形結構中。 在一些應用程序中,答案必須做很多次選擇才能得出。并且程序不只是在一個層面上而必須在遞歸的每個層面上做出選擇。拿生活中的例子打個比方好了,首先想象一下你要出席一個很重要的會議。但你不知道在哪開會,在你的面前有兩扇門,開會的地點就在其中一扇門后面,于是有人挑了左邊的進入,然后又發現兩扇們。后來再選了左邊的門,結果卻錯了,于是_回溯_到剛才的兩扇門那里,并且選擇右邊的們,結果還是錯的,于是再次回溯,直到回到了開始點,再在那里選擇右邊的門。 **圖和樹** 如果讀者沒有聽過圖和樹,那么應該盡快學習。它們是程序設計和計算機科學中的重要概念。如果想了解更多,應該找一本與計算機科學、離散數學、數據結構或算法相關的書籍來學習。你可以從下面鏈接的網頁中得到數和圖的簡單定義: http://mathworld.wolfram.com/Graph.html http://mathworld.wolfram.com/Tree.html http://www.nist.gov/dads/HTML/tree.html http://www.nist.gov/dads/HTML/graph.html 在互聯網上搜索以及訪問[維基百科全書](http://wikipedia.org)會獲得更多信息。 這樣的回溯策略在解決需要嘗試每種組合,直到找到一種解決方案的問題時很有用。這類問題能按照下面偽代碼的方式解決: ``` # 偽代碼 第1層所有的可能性: 第2層所有的可能性: ··· 第n層所有的可能性: 可行嗎? ``` 為了直接使用`for`循環來實現,就必須知道會遇到的具體判斷層數,如果無法得知層數信息,那么可以使用遞歸。 ### 9.8.2 問題 這是一個深受喜愛的計算機科學謎題:有一個棋盤和8個要放到上面的皇后。唯一的要求是皇后之間不能形成威脅。也就是說,必須把它們放置成每個皇后都不能吃掉其他皇后的狀態。怎么樣才能做到呢?皇后要如何放置呢? 這是一個典型的回溯問題:首先嘗試放置第1個皇后(在第1行),然后放置第2個,依次類推。如果發現不能放置下一個皇后,就回溯到上一步,試著將皇后放到其他的位置。最后,或者嘗試完所有的可能或者找到解決方案。 問題會告知,棋盤上只有八個皇后,但我們假設有任意數目的皇后(這樣就更合實際生活中的回溯問題),怎么解決?如果你要自己解決,那么就不要繼續了,因為解決方案馬上要給出。 _注:實際上對于這個問題有更高效的解決方案,如果想了解更多的細節,那么可以在網上搜索,以得到很多有價值的信息。訪問 http://www.cit.gu.edu.au/~sosic/nqueens.html 可以找到關于各種解決方案的簡單介紹。_ ### 9.8.3 狀態表示 為了表示一個可能的解決方案(或者方案的一部分),可以使用元組(或者列表)。每個元組中元素都指示相應行的皇后的位置(也就是列)。如果`state[0]==3`,那么就表示在第1行的皇后是在第4列(記得么,我們是從0開始計數的)。當在某一個遞歸的層面(一個具體的行)時,只能知道上一行皇后的位置,因此需要一個長度小于8的狀態元組(或者小于皇后的數目)。 _注:使用列表來代替元組表示狀態也是可行的。具體使用哪個只是一個習慣的問題。一般來說,如果序列很小而且是靜態的,元組是一個好的選擇。_ ### 9.8.4 尋找沖突 首先從一些簡單的抽象開始。為了找到一種沒有沖突的設置(沒有皇后會被其他的皇后吃掉),首先必須定義沖突是什么。為什么不在定義的時候把它定義成一個函數? 已知的皇后的位置被傳遞給`conflict`函數(以狀態元組的形式),然后由函數判斷下一個的皇后的位置會不會有新的沖突。 ``` def conflict(state, nextX): nextY = len(state) for i in range(nextY): if abs(state[i] - nextX) in (0, nextY - i): return True return False ``` 參數`nextX`代表下一個皇后的水平位置(`x`坐標或列),`nextY`代表垂直位置(`y`坐標或行)。這個函數對前面的每個皇后的位置做一個簡單的檢查,如果下一個皇后和前面的皇后有同樣的水平位置,或者是在一條對角線上,就會發生沖突,接著返回`True`。如果沒有這樣的沖突發生,那么返回`False`,不太容易理解的是下面的表達式: ``` abs(state[i] - nextX) in (0, nextY - i) ``` 如果下一個皇后和正在考慮的前一個皇后的水平距離為0(列相同)或者等于垂直距離(在一條對角線上)就返回`True`,否則就返回`False`。 ### 9.8.5 基本情況 八皇后問題的實現雖然有點不太好實現,但如果使用生成器就沒什么難的了。如果不習慣于使用遞歸,那么你最好不要自己動手解決這個問題。需要注意的是這個解決方案的效率不是很高,因此如果皇后的數目很多的話,運行起來就會有點慢。 從基本的情況開始:最后一個皇后。你想讓它做什么?假設你想找出所有可能的解決方案;這樣一來,它能根據其他皇后的為止生成它自己能占據的所有位置(可能沒有)。能把這樣的情況直接描繪出。 ``` def queens(sum, state): if len(state) == num - 1: for pos in range(num): if not conflict(state, pos): yield pos ``` 用人類的語言來描述,它的意思是:如果只剩一個皇后沒有位置,那么遍歷它的所有可能的位置,并且返回沒有沖突發生的位置。`num`參數是皇后的總數。`state`參數是存放前面皇后的位置信息的元組。假設有4個皇后,前3個分別被放置在1、3、0號位置上,如圖9-1所示(不要在意第4行的白色皇后)。 正如在圖中看到的,每個皇后占據了一行,并且位置的標號已經到了最大(Python中都是從0開始的): ``` >>> list(queens(4, (1, 3, 0))) [2] ``` 代碼看起來就像一個魔咒。使用`list`來讓生成器生成列表中的所有值。在這種情況下,只有一個位置是可行的。白色皇后被放置在了如圖9-1所示的位置(注意顏色沒有特殊含義,不是程序的一部分)。 圖9-1 在一個4 x 4的棋盤上放4個皇后 ![](http://images2015.cnblogs.com/blog/913841/201605/913841-20160502213434029-786284403.png) ### 9.8.6 需要遞歸的情況 現在,讓我們看看解決方案中的遞歸部分。完成基本情況后,遞歸函數會假定(通過歸納)所有的來自低層(有更高編號的皇后)的結果都是正確的。因此需要做的就是為前面的`queen`函數的實現中的`if`語句增加`else`子句。 那么遞歸調用會得到什么結果呢?你想得到所有低層皇后的位置,對嗎?假設將位置信息作為一個元組返回。在這種情況下,需要修改基本情況也返回一個元組(長度為1),稍后就會那么做。 這樣一來,程序從前面的皇后得到了包含位置信息的元組,并且要為后面的皇后提供當前皇后的每種合法的位置信息。為了讓程序繼續運行下去,接下來需要做的就是把當前的位置信息添加到元組中并傳給后面的皇后。 ``` ... else: for pos in range(num): if not conflict(state, pos): for result in queens(num, state + (pos, )): yield (pos, ) + result ``` `for pos`和`if not conflict`部分和前面的代碼相同,因此可以稍微簡化一下代碼。添加一些默認的參數: ``` def queens(num=8, state=()): for pos in range(num): if not conflict(state, pos): if len(state) == num - 1: yield (pos, ) else: for result in queens(num, state + (pos, )): yield (pos, ) + result ``` 如果覺得代碼很難理解,那么就把代碼做的事用自己的語言來敘述,這樣能有所幫助。(還記得在`(pos,)`中的逗號使其必須被設置為元組而不是簡單地加上括號嗎?) 生成器`queens`能給出所有的解決方案(那就是放置皇后的所有的合法方法): ``` >>> list(queens(3)) [] >>> list(queens(4)) [(1, 3, 0, 2), (2, 0, 3, 1)] >>> for solution in queens(8): ... print solution ... (0, 4, 7, 5, 2, 6, 1, 3) (0, 5, 7, 2, 6, 3, 1, 4) ... (7, 2, 0, 5, 1, 4, 6, 3) (7, 3, 0, 2, 5, 1, 6, 4) ``` 如果用8個皇后做參數來運行`queens`,會看到很多解決方案閃過,來看看有多少種方案: ``` >>> len(list(queens(8))) 92 ``` ### 9.8.7 打包 在結束八皇后問題之前,試著將輸出處理得更容易理解一點。清理輸出總是一個好的習慣,因為這樣很容易發現錯誤。 ``` def prettyprint(solution): def line(pos, length=len(solution)): return ". " * (pos) + "X" + ". " * (length - pos - 1) for pos in solution: print line(pos) ``` 注意`prettyprint`中創建了一個小的助手函數。之所以將其放在`prettyprint`內,是因為我們假設在外面的任何地方都不會用到它。下面打印出一個令我滿意的隨機解決方案。可以看到該方案是正確的。 ``` >>> import random >>> prettyprint(random.choice(list(queens(8)))) . . X. . . . . . . . . . X. . . . . . . . . X . X. . . . . . . . . X. . . . X. . . . . . . . . . . . . X. . . . . X. . . ``` ## 9.9 小結 本章介紹了很多魔法方法,下面來總結一下。 ? 舊式類和新式類:Python中類的工作方式正在發生變化。目前(3.0版本以前)的Python內有兩種類,舊式類已經過時,新式類在2.2版本中被引入,它提供了一些新的特性(比如使用`super`函數和`property`函數,而舊式類就不能)。為了創建新式類,必須直接或間接子類化`object`,或者設置`__metaclass__`屬性也可以。 ? 魔法方法:在Python中有一些特殊的方法(名字是以雙下劃線開始和結束的)。這些方法和函數只有很小的不同,但其中的大部分方法在某些情況下被Python自動調用(比如`__init__`在對象被創建后調用)。 ? 構造方法:這是面向對象的語言共有的,可能要為自己寫的每個類實現構造方法。構造方法被命名為`__init__`并且在對象被創建后立即自動調用。 ? 重寫:一個類能通過實現方法來重寫它的超類中定義的這些方法和屬性。如果新方法要調用重寫版本的方法,可以從超類(舊式類)直接調用未綁定的版本或使用`super`函數(新式類)。 ? 序列和映射:創建自己的序列或者映射需要實現所有的序列或是映射規則的方法,包括`__getitem__`和`__setitem__`這樣的特殊方法。通過子類化`list`(或者`UserList`)和`dict`(或者`UserDict`)能節省很多工作。 ? 迭代器:_迭代器_是帶有`next`方法的簡單對象。迭代器能在一系列的值上進行迭代。當沒有值可供迭代時,`next`方法就會引發`StopIteration`異常。_可迭代對象_有一個返回迭代器的`__iter__`方法,它能像序列那樣在`for`循環中使用。一般來說,迭代器本身也是可迭代的,即迭代器有返回它自己的`next`方法。 ? 生成器:_生成器函數_(或者方法)是包含了關鍵字`yield`的函數(或方法)。當被調用時,生成器函數返回一個_生成器_(一種特殊的迭代器)。可以使用`send`、`throw`和`close`方法讓活動生成器和外界交互。 ? 八皇后的問題:八皇后問題在計算機科學領域內無人不知,使用生成器可以很輕松地解決這個問題。問題描述的是如何在棋盤上放置8個皇后,使其不會互相攻擊。 ### 9.9.1 本章的新函數 本章涉及的新函數如表9-1所示。 **表9-1 本章的新函數** ``` iter(obj) 從一個可迭代對象得到迭代器 property(fget, fset, fdel, doc) 返回一個屬性,所有的參數都是可選的 super(class, obj) 返回一個類的超類的綁定實例 ``` 注意,`iter`和`super`可能會使用一些其他(未在這里介紹的)參數進行調用。要了解更多的信息,請參見[“Standard Python Documentation”(標準Python文檔)](http://python.org/doc)。 ### 9.9.2 接下來學什么 到目前為止,Python語言的大部分知識都介紹了。那么剩下的那么多章是講什么的呢?還有很多內容要學,后面的內容很多是關于Python怎么通過各種方法和外部世界聯系的。接下來我們還會討論測試、擴展、打包和一些項目的具體實現,所以請繼續努力吧。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看