第九章 魔法方法、特性和迭代器
構造函數
構造函數(constructor),它其實就是初始化方法,只是命名為__init__
。
構造函數不同于普通方法的地方在于,將在對象創建后自動調用它們
。
在Python中,創建構造函數很容易,只需將方法init的名稱從普通的init改為魔法版__init__即可。
class Beyond:def __init__(self):self.band = 1999sq = Beyond()
sq.band#結果為:1999
給構造函數添加幾個參數
class Beyond:def __init__(self,value = 1999):self.band = valueyy = Beyond("This is a band")
yy.band#結果為:'This is a band'
Python提供了魔法方法__del__,也稱作析構函數(destructor)
。這個方法在對象被銷毀(作為垃圾被收集)前被調用,但鑒于你無法知道準確的調用時間,建議盡可能不要使用__del__
。
重寫普通方法和特殊的構造函數
每個類都有一個或多個超類,并從它們那里繼承行為。
對類B的實例調用方法(或訪問其屬性)時,如果找不到該方法(或屬性),將在其超類A中查找。
類A定義了一個名為hello的方法,并被類B繼承。
由于類B自己沒有定義方法hello,因此對其調用方法hello時,打印的是消息"Hello, I am A."。
class A:def hello(self):print("Hello,I am A")class B(A):passa = A()
b = B()a.hello()#結果為:Hello,I am A
b.hello()#結果為:Hello,I am A
當然,類B可以重寫
方法hello
重寫是繼承機制的一個重要方面,對構造函數來說尤其重要。
class A:def hello(self):print("Hello,I am A")class B(A):def hello(self):print("Hello,I am B")b = B()
b.hello()#結果為:Hello,I am B
構造函數用于初始化新建對象的狀態,而對大多數子類來說,除超類的初始化代碼外,還需要有自己的初始化代碼。
重寫構造函數時,必須調用超類(繼承的類)的構造函數,否則可能無法正確地初始化對象。
Bird類,定義了所有鳥都具備的一種基本能力:進食。鳥進食后就不再饑餓。
class Bird:def __init__(self):self.hungry = Truedef eat(self):#進食if self.hungry:print('Yes,hungry')self.hungry = False#進食完,就不餓了else:print("No,thanks!")b = Bird()
b.eat()#結果為:Yes,hungry
b.eat()#結果為:No,thanks!
子類SongBird,它新增了鳴叫功能。
SongBird是Bird的子類,繼承了方法eat,但是一旦調用該方法會報異常:SongBird沒有屬性hungry
class SongBird(Bird):def __init__(self):self.sound = 'beyond'def sing(self):print(self.sound)sb = SongBird()
sb.sing()#結果為:beyondsb.eat()#報錯!!!
'''
AttributeError Traceback (most recent call last)
<ipython-input-34-05f67b3cf162> in <module>()
----> 1 sb.eat()<ipython-input-28-ede0d63683b8> in eat(self)3 self.hungry = True4 def eat(self):
----> 5 if self.hungry:6 print('Yes,hungry')7 self.hungry = FalseAttributeError: 'SongBird' object has no attribute 'hungry'
'''
SongBird中重寫了構造函數,但新的構造函數沒有包含任何初始化屬性hungry的代碼。要消除這種錯誤,SongBird的構造函數必須調用其超類(Bird)的構造函數,以確保基本的初始化得以執行。
有兩種方法可以解決:調用未關聯的超類構造函數
,以及使用函數super
。
下面開始介紹這兩種解決方法
調用未關聯的超類構造函數
在SongBird類中,只添加了一行,其中包含代碼Bird.__init__(self)
。
通過將這個未關聯方法的self參數設置為當前實例,將使用超類的構造函數來初始化SongBird對象。這意味著將設置其屬性hungry。
class Bird:def __init__(self):self.hungry = Truedef eat(self):if self.hungry:print('Yes,hungry')self.hungry = Falseelse:print("No,thanks!")class SongBird(Bird):def __init__(self):Bird.__init__(self)self.sound = 'beyond'def sing(self):print(self.sound)sb = SongBird()
sb.eat()#結果為:Yes,hungry
sb.eat()#結果為:No,thanks!
對實例調用方法時,方法的參數self將自動關聯到實例(稱為關聯的方法)
通過類調用方法(如Bird.init),就沒有實例與其相關聯,可隨便設置參數self,這樣的方法稱為未關聯的。
使用函數 super
調用這個函數時,將當前類和當前實例作為參數。對其返回的對象調用方法時,調用的將是超類(而不是當前類)的方法。
在SongBird的構造函數中,可不使用Bird,而是使用super(SongBird, self)。
在Python 3中調用函數super時,可不提供任何參數(通常也應該這樣做)。
class Bird:def __init__(self):self.hungry = Truedef eat(self):if self.hungry:print('Yes,hungry')self.hungry = Falseelse:print("No,thanks!")class SongBird(Bird):def __init__(self):super().__init__(self)self.sound = 'beyond'def sing(self):print(self.sound)sb = SongBird()
sb.eat()#結果為:Yes,hungry
sb.eat()#結果為:No,thanks!
相比于直接對超類調用未關聯方法,使用函數super更直觀
即便有多個超類,也只需調用函數super一次(條件是所有超類的構造函數也使用函數super)。
使用函數super比調用超類的未關聯構造函數(或其他方法)要好得多。
函數super返回的到底是什么呢?
通常,你無需關心這個問題,只管假定它返回你所需的超類即可。
實際上,它返回的是一個super對象,這個對象將負責為你執行方法解析。
當你訪問它的屬性時,它將在所有的超類(以及超類的超類,等等)中查找,直到找到指定的屬性或引發AttributeError異常。
元素訪問
在Python中,協議通常指的是規范行為的規則
在Python中,多態僅僅基于對象的行為(而不基于祖先,如屬于哪個類或其超類等)
其他的語言可能要求對象屬于特定的類或實現了特定的接口,而Python通常只要求對象遵循特定的協議。
基本的序列和映射協議
序列和映射基本上是元素(item)的集合,要實現它們的基本行為(協議),不可變對象需要實現2個方法,而可變對象需要實現4個。
方法名稱 | 解釋 |
---|---|
len(self) | 這個方法應返回集合包含的項數,對序列來說為元素個數,對映射來說為鍵-值對數。如果__len__返回零(且沒有實現覆蓋這種行為的__nonzero__),對象在布爾上下文中將被視為假(就像空的列表、元組、字符串和字典一樣) |
getitem(self, key) | 這個方法應返回與指定鍵相關聯的值。對序列來說,鍵應該是0~n-1的整數(也可以是負數),其中n為序列的長度。對映射來說,鍵可以是任何類型。 |
setitem(self, key, value) | 這個方法應以與鍵相關聯的方式存儲值,以便以后能夠使用__getitem__來獲取。當然,僅當對象可變時才需要實現這個方法。 |
delitem(self, key) | 這個方法在對對象的組成部分使用__del__語句時被調用,應刪除與key相關聯的值。同樣,僅當對象可變(且允許其項被刪除)時,才需要實現這個方法。 |
對于這些方法,還有一些額外的要求 |
---|
對于序列,如果鍵為負整數,應從末尾往前數。換而言之,x[-n]應與x[len(x)-n]等效。 |
如果鍵的類型不合適(如對序列使用字符串鍵),可能引發TypeError異常。 |
對于序列,如果索引的類型是正確的,但不在允許的范圍內,應引發IndexError異常。 |
模塊collections的文檔有更復雜的接口和使用的抽象基類(Sequence)
實現一個算術序列,其中任何兩個相鄰數字的差都相同。
第一個值是由構造函數的參數start(默認為0)指定的,而相鄰值之間的差是由參數step(默認為1)指定的。
允許用戶修改某些元素,這是通過將不符合規則的值保存在字典changed中實現的。
如果元素未被修改,就使用公式self.start + key * self.step來計算它的值。
'''
函數check_index(key):指定的鍵是否是可接受的索引?
鍵必須是非負整數,才是可接受的。如果不是整數,
將引發TypeError異常;如果是負數,將引發Index
Error異常(因為這個序列的長度是無窮的)
'''
def check_index(key):#索引檢查if not isinstance(key,int):raise TypeErrorif key < 0 :raise IndexErrorclass ArithmeticSequence:
#start -序列中的第一個值
#step -兩個相鄰值的差
#changed -一個字典,包含用戶修改后的值def __init__(self,start=0,step=1):#初始化這個算術序列self.start = start#存儲起始值self.step = step#存儲步長值self.changed = {}#沒有任何元素被修改def __getitem__(self,key):#從算術序列中獲取一個元素check_index(key)try:return self.changed[key]#修改過?except KeyError:# 如果沒有修改過,就計算元素的值return self.start + key * self.stepdef __setitem__(self,key,value):#修改算術序列中的元素check_index(key)self.changed[key] = value#存儲修改后的值s = ArithmeticSequence(1,2)
s[4]#結果為:9s[4] = 2
s[4]#結果為:2s[5]#結果為:11#由于沒有實現__del__,故禁止刪除元素
del s[4]#報異常!!!
'''
AttributeError Traceback (most recent call last)
<ipython-input-70-9cf88d1604ce> in <module>()
----> 1 del s[4]AttributeError: __delitem__
'''#這個類沒有方法__len__,因為其長度是無窮的。
#如果所使用索引的類型非法,將引發TypeError異常;
#如果索引的類型正確,但不在允許的范圍內(即為負數),將引發IndexError異常。
s["beyond"]#結果為:報異常!!!
'''
TypeError Traceback (most recent call last)
<ipython-input-73-f5e041f04983> in <module>()
----> 1 s["beyond"]<ipython-input-66-2648d9225498> in __getitem__(self, key)6 7 def __getitem__(self,key):
----> 8 check_index(key)9 try:return self.changed[key]10 except KeyError:<ipython-input-55-f34d4d9f3aac> in check_index(key)1 def check_index(key):
----> 2 if not isinstance(key,int):raise TypeError3 if key < 0 :raise IndexErrorTypeError:
'''s[-99]#結果為:報異常!!!
'''
IndexError Traceback (most recent call last)
<ipython-input-74-8aa71bcb4b31> in <module>()
----> 1 s[-99]<ipython-input-66-2648d9225498> in __getitem__(self, key)6 7 def __getitem__(self,key):
----> 8 check_index(key)9 try:return self.changed[key]10 except KeyError:<ipython-input-55-f34d4d9f3aac> in check_index(key)1 def check_index(key):2 if not isinstance(key,int):raise TypeError
----> 3 if key < 0 :raise IndexErrorIndexError:
'''
從 list、dict 和 str 派生
一個帶訪問計數器的列表。
CounterList類深深地依賴于其超類(list)的行為。
CounterList沒有重寫的方法(如append、extend、index等)都可直接使用。
在兩個被重寫的方法中,使用super來調用超類的相應方法,并添加了必要的行為:初始化屬性counter(在__init__中)和更新屬性counter(在__getitem__中)。
重寫__getitem__并不能保證一定會捕捉用戶的訪問操作,因為還有其他訪問列表內容的方式,如通過方法pop。
CounterList的行為在大多數方面都類似于列表,但它有一個counter屬性(其初始值為0)。
每當你訪問列表元素時,這個屬性的值都加1。
執行加法運算cl[4] + cl[2]后,counter的值遞增兩次,變成了2。
class CounterList(list):def __init__(self,*args):super().__init__(*args)self.counter = 0def __getitem__(self,index):self.counter += 1return super(CounterList,self).__getitem__(index)c1 = CounterList(range(10))
c1#結果為:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]c1.reverse()
c1#結果為:[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]del c1[3:6]
c1#結果為:[9, 8, 7, 3, 2, 1, 0]c1.counter#結果為:0c1[4] + c1[2]#結果為:9c1.counter#結果為:2
特性
通過存取方法定義的屬性通常稱為特性(property)。
在Python中,實際上有兩種創建特定的機制,重點解釋較新的那種——函數property,它只能用于新式類。
class Rectangle:def __init__(self):self.width = 0self.height = 0def set_size(self,size):self.width,self.height = sizedef get_size(self):return self.width,self.heightr = Rectangle()
r.width = 10
r.height = 5
r.get_size()#結果為:(10, 5)r.set_size((150,100))
r.width#結果為:150
get_size和set_size是假想屬性size的存取方法,這個屬性是一個由width和height組成的元組。(可隨便將這個屬性替換為更有趣的屬性,如矩形的面積或其對角線長度。)
函數 property
通過調用函數property并將存取方法作為參數(獲取方法在前,設置方法在后)創建了一個特性,然后將名稱size關聯到這個特性。
class Rectangle:def __init__(self):self.width = 0self.height = 0def set_size(self,size):self.width,self.height = sizedef get_size(self):return self.width,self.heightsize = property(get_size,set_size)r = Rectangle()
r.width = 10
r.height = 5
r.get_size()#結果為:(10, 5)r.set_size((150,100))
r.width#結果為:150
實際上,調用函數property時,還可不指定參數、指定一個參數、指定三個參數或指定四個參數。
如果沒有指定任何參數,創建的特性將既不可讀也不可寫。
如果只指定一個參數(獲取方法),創建的特性將是只讀的。
第三個參數是可選的,指定用于刪除屬性的方法(這個方法不接受任何參數)。
第四個參數也是可選的,指定一個文檔字符串。
這些參數分別名為fget
、fset
、fdel
和doc
。
如果你要創建一個只可寫且帶文檔字符串的特性,可使用它們作為關鍵字參數來實現。
函數property的工作原理
property其實并不是函數,而是一個類。
它的實例包含一些魔法方法,而所有的魔法都是由這些方法完成的。
這些魔法方法為__get__
、__set__
和__delete__
,它們一道定義了所謂的描述符協議。
只要對象實現了這些方法中的任何一個,它就是一個描述符。描述符的獨特之處在于其訪問方式。
讀取屬性(具體來說,是在實例中訪問類中定義的屬性)時,如果它關聯的是一個實現了__get__的對象,將不會返回這個對象,而是調用方法__get__并將其結果返回。
靜態方法和類方法
靜態方法和類方法是這樣創建的:將它們分別包裝在staticmethod和classmethod類的對象中。
靜態方法的定義中沒有參數self,可直接通過類來調用。
類方法的定義中包含類似于self的參數,通常被命名為cls。
對于類方法,也可通過對象直接調用,但參數cls將自動關聯到類。
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)MyClass.smeth()#結果為:This is a static method
MyClass.cmeth()#結果為:This is a class method of <class '__main__.MyClass'>
在Python 2.4中,引入了一種名為裝飾器的新語法,可用于像這樣包裝方法。(實際上,裝飾器可用于包裝任何可調用的對象,并且可用于方法和函數。)可指定一個或多個裝飾器,為此可在方法(或函數)前面使用運算符@列出這些裝飾器(指定了多個裝飾器時,應用的順序與列出的順序相反)。
裝飾器語法也可用于特性,詳情請參閱有關函數property的文檔。
class MyClass:@staticmethoddef smeth():print("This is a static method1")@classmethoddef cmeth(cls):print("This is a class method of1 ",cls)MyClass.smeth()#結果為:This is a static method
MyClass.cmeth()#結果為:This is a class method of <class '__main__.MyClass'>
getattr、__setattr__等方法
方法 | 解釋 |
---|---|
__getattribute__(self, name) | 在屬性被訪問時自動調用(只適用于新式類) |
__getattr__(self, name) | 在屬性被訪問而對象沒有這樣的屬性時自動調用。 |
__setattr__(self, name, value) | 試圖給屬性賦值時自動調用。 |
__delattr__(self, name) | 試圖刪除屬性時自動調用。 |
'''
即便涉及的屬性不是size,也將調用方法__setattr__。
因此這個方法必須考慮如下兩種情形:如果涉及的屬性為size,就執行與以前一樣的操作;否則就使用魔法屬性__dict__。
__dict__屬性是一個字典,其中包含所有的實例屬性。
之所以使用它而不是執行常規屬性賦值,是因為旨在避免再次調用__setattr__,進而導致無限循環。
僅當沒有找到指定的屬性時,才會調用方法__getattr__。
這意味著如果指定的名稱不是size,這個方法將引發AttributeError異常。
這在要讓類能夠正確地支持hasattr和getattr等內置函數時很重要。
如果指定的名稱為size,就使用前一個實現中的表達式。
'''
class Rectangle:def __init__(self):self.width = 0self.height = 0def __setattr__(self,name,value):if name == 'size':self.width,self.height = valueelse:self.__dict__[name] = valuedef __getattr__(self,name):if name == 'size':return self.width,self.heightelse:raise AttributeError()
迭代器
__iter__
,它是迭代器協議的基礎。
迭代器協議
迭代(iterate)意味著重復多次,就像循環那樣。
方法__iter__返回一個迭代器,它是包含方法__next__的對象,而調用這個方法時可不提供任何參數。
調用方法__next__時,迭代器應返回其下一個值。
如果迭代器沒有可供返回的值,應引發StopIteration異常。
使用內置的便利函數next,在這種情況下,next(it)與it.next()等效。
斐波那契數列
class Fibs:def __init__(self):self.a = 0self.b = 1def __next__(self):self.a , self.b = self.b , self.a + self.breturn self.adef __iter__(self):return selffibs = Fibs()
for f in fibs:if f > 1000:print(f)#結果為:1597break
通過對可迭代對象調用內置函數iter,可獲得一個迭代器。
it = iter([1,2,3])
next(it)#結果為:1
next(it)#結果為:2
next(it)#結果為:3
從迭代器創建序列
使用構造函數list顯式地將迭代器轉換為列表。
class TestIterator:value = 0def __next__(self):self.value += 1if self.value > 10:raise StopIterationreturn self.valuedef __iter__(self):return selfti = TestIterator()
list(ti)#結果為:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
生成器
生成器它也被稱為簡單生成器(simple generator)。
生成器是一種使用普通函數語法定義的迭代器。
創建生成器
包含yield
語句的函數都被稱為生成器
。
生成器不是使用return返回一個值,而是可以生成多個值,每次一個。
每次使用yield生成一個值后,函數都將凍結,即在此停止執行,等待被重新喚醒。
被重新喚醒后,函數將從停止的地方開始繼續執行。
def flatten(nested):for sublist in nested:for element in sublist:yield elementnested = [[1,2],[3,4],[5]]
for num in flatten(nested):print(num)#結果為:
'''
1
2
3
4
5
'''list(flatten(nested))#結果為:[1, 2, 3, 4, 5]
生成器推導(也叫生成器表達式)。其工作原理與列表推導相同,但不是創建一個列表(即不立即執行循環),而是返回一個生成器,讓你能夠逐步執行計算。不同于列表推導,這里使用的是圓括號。
g = ((i + 2) ** 2 for i in range(2, 27))
next(g)#結果為:16
直接在一對既有的圓括號內(如在函數調用中)使用生成器推導時,無需再添加一對圓括號。
sum(i ** 2 for i in range(10))#結果為:285
遞歸式生成器
上述設計的生成器只能處理兩層的嵌套列表,這是使用兩個for循環來實現的。
如果要處理任意層嵌套的列表,該如何辦呢?
該求助于遞歸了。
調用flatten時,有兩種可能性(處理遞歸時都如此):基線條件和遞歸條件。
在基線條件下,要求這個函數展開單個元素(如一個數)。
在這種情況下,for循環將引發TypeError異常(因為你試圖迭代一個數),而這個生成器只生成一個元素。
如果要展開的是一個列表(或其他任何可迭代對象):遍歷所有的子列表(其中有些可能并不是列表)并對它們調用flatten,然后使用另一個for循環生成展開后的子列表中的所有元素。
如果nested是字符串或類似于字符串的對象,它就屬于序列,因此不會引發TypeError異常,但并不想對其進行迭代。
def flatten(nested):try:for sublist in nested:for element in flatten(sublist):yield elementexcept TypeError:yield nestedlist(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))#結果為:[1, 2, 3, 4, 5, 6, 7, 8]
在函數flatten中,不應該對類似于字符串的對象進行迭代,主要原因有兩個。
首先,你想將類似于字符串的對象視為原子值,而不是應該展開的序列。
其次,對這樣的對象進行迭代會導致無窮遞歸,因為字符串的第一個元素是一個長度為1的字符串,而長度為1的字符串的第一個元素是字符串本身!
要處理這種問題,必須在生成器開頭進行檢查。
要檢查對象是否類似于字符串,最簡單、最快捷的方式是,嘗試將對象與一個字符串拼接起來,并檢查這是否會引發TypeError異常。
如果表達式nested + ''引發了TypeError異常,就忽略這種異常;
如果沒有引發TypeError異常,內部try語句中的else子句將引發TypeError異常,這樣將在外部的excpet子句中原封不動地生成類似于字符串的對象。
def flatten(nested):try:# 不迭代類似于字符串的對象:try:nested + ''except TypeError:passelse:raise TypeErrorfor sublist in nested:for element in flatten(sublist):yield elementexcept TypeError:yield nestedlist(flatten(['foo', ['bar', ['baz']]]))#['foo', 'bar', 'baz']
這里沒有執行類型檢查:沒有檢查nested是否是字符串,而只是檢查其行為是否類似于字符串,即能否與字符串拼接。
通用生成器
生成器是包含關鍵字yield的函數,但被調用時不會執行函數體內的代碼,而是返回一個迭代器。
每次請求值時,都將執行生成器的代碼,直到遇到yield或return。yield意味著應生成一個值,而return意味著生成器應停止執行(即不再生成值;僅當在生成器調用return時,才能不提供任何參數)。
換而言之,生成器由兩個單獨的部分組成:生成器的函數和生成器的迭代器。
生成器的函數是由def語句定義的,其中包含yield。生成器的迭代器是這個函數返回的結果。
用不太準確的話說,這兩個實體通常被視為一個,通稱為生成器。
def simple_generator():yield isimple_generator#結果為:<function __main__.simple_generator>
simple_generator()#結果為:<generator object simple_generator at 0x000001C5C9CF2B48>
生成器的方法
在生成器開始運行后,可使用生成器和外部之間的通信渠道向它提供值。這個通信渠道包含如下兩個端點。
端點名稱 | 解釋 |
---|---|
外部世界 | 外部世界可訪問生成器的方法send,這個方法類似于next,但接受一個參數(要發送的“消息”,可以是任何對象)。 |
生成器 | 在掛起的生成器內部,yield可能用作表達式而不是語句。換而言之,當生成器重新運行時,yield返回一個值——通過send從外部世界發送的值。如果使用的是next,yield將返回None。 |
僅當生成器被掛起(即遇到第一個yield)后,使用send(而不是next)才有意義。
def repeater(value):while True:new = (yield value)if new is not None:value = newr = repeater(1999)
next(r)#結果為:1999r.send("Hello,beyond band!")#結果為:'Hello,beyond band!'
生成器還包含另外兩個方法。
方法 | 解釋 |
---|---|
throw | 用于在生成器中(yield表達式處)引發異常,調用時可提供一個異常類型、一個可選值和一個traceback對象。 |
close | 用于停止生成器,調用時無需提供任何參數。 |
模擬生成器
使用普通函數重寫生成器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
八皇后問題
生成器的回溯
回溯
的簡單理解:
你要去參加一個很重要的會議。你不知道會議在哪里召開,但前面有兩扇門,而會議室就在其中一扇門的后面。你選擇進入左邊那扇門后,又看到兩扇門。你再次選擇進入左邊那扇門,但發現走錯了。因此你往回走,并進入右邊那扇門,但發現也走錯了。因此你繼續往回走到起點,現在可以嘗試進入右邊那扇門。
問題
將8個皇后放在棋盤上,條件是任何一個皇后都不能威脅其他皇后,即任何兩個皇后都不能吃掉對方。任意兩個皇后都不能處于同一行、同一列或同一斜線上(八個方向)。 怎樣才能做到這一點呢?應將這些皇后放在什么地方呢?
這是一個典型的回溯問題:在棋盤的第一行嘗試為第一個皇后選擇一個位置,再在第二行嘗試為第二個皇后選擇一個位置,依次類推。在發現無法為一個皇后選擇合適的位置后,回溯到前一個皇后,并嘗試為它選擇另一個位置。最后,要么嘗試完所有的可能性,要么找到了答案。
狀態表示
可簡單地使用元組(或列表)來表示可能的解(或其一部分),其中每個元素表示相應行中皇后所在的位置(即列)。
如果state[0] == 3,就說明第1行的皇后放在第4列(從0開始計數)
完全可以使用列表(而不是元組)來表示狀態,如果序列較小且是靜態的,使用元組可能是不錯的選擇。
檢測沖突
定義沖突:
函數conflict接受(用狀態元組表示的)既有皇后的位置,并確定下一個皇后的位置是否會導致沖突。
參數nextX表示下一個皇后的水平位置(x坐標,即列),而nextY為下一個皇后的垂直位置(y坐標,即行)。
這個函數對既有的每個皇后執行簡單的檢查:
如果下一個皇后與當前皇后的x坐標相同或在同一條對角線上,將發生沖突,因此返回True;
如果沒有發生沖突,就返回False。
abs(state[i] - nextX) in (0, nextY - i)
如果下一個皇后和當前皇后的水平距離為0(在同一列)或與它們的垂直距離相等(位于一條對角線上),這個表達式就為真;否則為假。
def conflict(state, nextX):nextY = len(state)for i in range(nextY):if abs(state[i] - nextX) in (0, nextY - i):return Truereturn False
基線條件
如果只剩下最后一個皇后沒有放好,就遍歷所有可能的位置,并返回那些不會引發沖突的位置。參數num為皇后總數,而參數state是一個元組,包含已放好的皇后的位置。
def queens(num, state):if len(state) == num-1:for pos in range(num):if not conflict(state, pos):yield pos
#假設總共有4個皇后,而前3個皇后的位置分別為1、3和0,故還有位置2可以放置
list(queens(4, (1, 3, 0)))#結果為:[2]
遞歸條件
理想狀態:遞歸調用返回當前行下面所有皇后的位置
對于遞歸調用,向它提供的是由當前行上面的皇后位置組成的元組。
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,) + resultlist(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)
(0, 6, 3, 5, 7, 1, 4, 2)
(0, 6, 4, 7, 1, 3, 5, 2)
(1, 3, 5, 7, 2, 0, 6, 4)
(1, 4, 6, 0, 2, 7, 5, 3)
(1, 4, 6, 3, 0, 7, 5, 2)
(1, 5, 0, 6, 3, 7, 2, 4)
(1, 5, 7, 2, 0, 3, 6, 4)
(1, 6, 2, 5, 7, 4, 0, 3)
(1, 6, 4, 7, 0, 3, 5, 2)
(1, 7, 5, 0, 2, 4, 6, 3)
(2, 0, 6, 4, 7, 1, 3, 5)
(2, 4, 1, 7, 0, 6, 3, 5)
(2, 4, 1, 7, 5, 3, 6, 0)
(2, 4, 6, 0, 3, 1, 7, 5)
(2, 4, 7, 3, 0, 6, 1, 5)
(2, 5, 1, 4, 7, 0, 6, 3)
(2, 5, 1, 6, 0, 3, 7, 4)
(2, 5, 1, 6, 4, 0, 7, 3)
(2, 5, 3, 0, 7, 4, 6, 1)
(2, 5, 3, 1, 7, 4, 6, 0)
(2, 5, 7, 0, 3, 6, 4, 1)
(2, 5, 7, 0, 4, 6, 1, 3)
(2, 5, 7, 1, 3, 0, 6, 4)
(2, 6, 1, 7, 4, 0, 3, 5)
(2, 6, 1, 7, 5, 3, 0, 4)
(2, 7, 3, 6, 0, 5, 1, 4)
(3, 0, 4, 7, 1, 6, 2, 5)
(3, 0, 4, 7, 5, 2, 6, 1)
(3, 1, 4, 7, 5, 0, 2, 6)
(3, 1, 6, 2, 5, 7, 0, 4)
(3, 1, 6, 2, 5, 7, 4, 0)
(3, 1, 6, 4, 0, 7, 5, 2)
(3, 1, 7, 4, 6, 0, 2, 5)
(3, 1, 7, 5, 0, 2, 4, 6)
(3, 5, 0, 4, 1, 7, 2, 6)
(3, 5, 7, 1, 6, 0, 2, 4)
(3, 5, 7, 2, 0, 6, 4, 1)
(3, 6, 0, 7, 4, 1, 5, 2)
(3, 6, 2, 7, 1, 4, 0, 5)
(3, 6, 4, 1, 5, 0, 2, 7)
(3, 6, 4, 2, 0, 5, 7, 1)
(3, 7, 0, 2, 5, 1, 6, 4)
(3, 7, 0, 4, 6, 1, 5, 2)
(3, 7, 4, 2, 0, 6, 1, 5)
(4, 0, 3, 5, 7, 1, 6, 2)
(4, 0, 7, 3, 1, 6, 2, 5)
(4, 0, 7, 5, 2, 6, 1, 3)
(4, 1, 3, 5, 7, 2, 0, 6)
(4, 1, 3, 6, 2, 7, 5, 0)
(4, 1, 5, 0, 6, 3, 7, 2)
(4, 1, 7, 0, 3, 6, 2, 5)
(4, 2, 0, 5, 7, 1, 3, 6)
(4, 2, 0, 6, 1, 7, 5, 3)
(4, 2, 7, 3, 6, 0, 5, 1)
(4, 6, 0, 2, 7, 5, 3, 1)
(4, 6, 0, 3, 1, 7, 5, 2)
(4, 6, 1, 3, 7, 0, 2, 5)
(4, 6, 1, 5, 2, 0, 3, 7)
(4, 6, 1, 5, 2, 0, 7, 3)
(4, 6, 3, 0, 2, 7, 5, 1)
(4, 7, 3, 0, 2, 5, 1, 6)
(4, 7, 3, 0, 6, 1, 5, 2)
(5, 0, 4, 1, 7, 2, 6, 3)
(5, 1, 6, 0, 2, 4, 7, 3)
(5, 1, 6, 0, 3, 7, 4, 2)
(5, 2, 0, 6, 4, 7, 1, 3)
(5, 2, 0, 7, 3, 1, 6, 4)
(5, 2, 0, 7, 4, 1, 3, 6)
(5, 2, 4, 6, 0, 3, 1, 7)
(5, 2, 4, 7, 0, 3, 1, 6)
(5, 2, 6, 1, 3, 7, 0, 4)
(5, 2, 6, 1, 7, 4, 0, 3)
(5, 2, 6, 3, 0, 7, 1, 4)
(5, 3, 0, 4, 7, 1, 6, 2)
(5, 3, 1, 7, 4, 6, 0, 2)
(5, 3, 6, 0, 2, 4, 1, 7)
(5, 3, 6, 0, 7, 1, 4, 2)
(5, 7, 1, 3, 0, 6, 4, 2)
(6, 0, 2, 7, 5, 3, 1, 4)
(6, 1, 3, 0, 7, 4, 2, 5)
(6, 1, 5, 2, 0, 3, 7, 4)
(6, 2, 0, 5, 7, 4, 1, 3)
(6, 2, 7, 1, 4, 0, 5, 3)
(6, 3, 1, 4, 7, 0, 2, 5)
(6, 3, 1, 7, 5, 0, 2, 4)
(6, 4, 2, 0, 5, 7, 1, 3)
(7, 1, 3, 0, 6, 4, 2, 5)
(7, 1, 4, 2, 0, 6, 3, 5)
(7, 2, 0, 5, 1, 4, 6, 3)
(7, 3, 0, 2, 5, 1, 6, 4)
'''len(list(queens(8)))#結果為:92
掃尾工作
有可能在其他地方都用不到它,故在prettyprint中創建了一個簡單的輔助函數。
def prettyprint(solution):def line(pos, length=len(solution)):return '. ' * (pos) + 'X ' + '. ' * (length-pos-1)for pos in solution:print(line(pos))import random
prettyprint(random.choice(list(queens(8))))#結果為:
'''
. X . . . . . .
. . . . . . X .
. . X . . . . .
. . . . . X . .
. . . . . . . X
. . . . X . . .
X . . . . . . .
. . . X . . . .
'''
小結
概念 | 解釋 |
---|---|
新式類和舊式類 | Python類的工作方式在不斷變化。較新的Python 2版本有兩種類,其中舊式類正在快速退出舞臺。新式類是Python 2.2引入的,提供了一些額外的功能,如支持舊式類正在快速退出舞臺。新式類是Python 2.2引入的,提供了一些額外的功能,如支持或設置__metaclass__。 |
魔法方法 | Python中有很多特殊方法,其名稱以兩個下劃線開頭和結尾。這些方法的功能Python中有很多特殊方法,其名稱以兩個下劃線開頭和結尾。這些方法的功能 |
構造函數 | 很多面向對象語言中都有構造函數,對于你自己編寫的每個類,都可能需要很多面向對象語言中都有構造函數,對于你自己編寫的每個類,都可能需要 |
重寫 | 類可重寫其超類中定義的方法(以及其他任何屬性),為此只需實現這些方法即可。要調用被重寫的版本,可直接通過超類調用未關聯版本(舊式類),也可使用函數super來調用(新式類)。 |
序列和映射 | 要創建自定義的序列或映射,必須實現序列和映射協議指定的所有方法,其中包括__getitem__和__setitem__等魔法方法。通過從list(或UserList)和dict(或UserDict)派生,可減少很多工作量。 |
迭代器 | 簡單地說,迭代器是包含方法__next__的對象,可用于迭代一組值。沒有更多的值可供迭代時,方法__next__應引發StopIteration異常。可迭代對象包含方法__iter__,它返回一個像序列一樣可用于for循環中的迭代器。通常,迭代器也是可迭代的,即包含返回迭代器本身的方法__iter__。 |
生成器 | 生成器的函數是包含關鍵字yield的函數,它在被調用時返回一個生成器,即一種特殊的迭代器。要與活動的生成器交互,可使用方法send、throw和close。 |
八皇后問題 | 八皇后問題是個著名的計算機科學問題,使用生成器可輕松地解決它。這個問題要求在棋盤上放置8個皇后,并確保任何兩個皇后都不能相互攻擊(一個皇后的八個方向都不能有另一個皇后的存在)。 |
本章介紹的新函數
函數 | 描述 |
---|---|
iter(obj) | 從可迭代對象創建一個迭代器 |
next(it) | 讓迭代器前進一步并返回下一個元素 |
property(fget, fset, fdel, doc) | 返回一個特性;所有參數都是可選的 |
super(class, obj) | 返回一個超類的關聯實例 |