python基礎教程(第三版)學習筆記(九)
第九章 魔法方法、特性和迭代器
9.1 如果你使用的不是python3
在Python 2.2中,Python物件的工作方式有了很大的變化。這種變化帶來了多個方面的影響。這些影響對Python程式設計新手來說大都不重要,但有一點需要注意:即便你使用的是較新的Python 2版本,有些功能(如特性和函式super)也不適用於舊式類。要讓你的類是新式的,要麼在模組開頭包含賦值語句__metaclass__ = type(這以前提到過),要麼直接或間接地繼承內建類object或其他新式類。
在Python 3中沒有舊式類,因此無需顯式地繼承object或將__metaclass__設定為type。所有的類都將隱式地繼承object。如果沒有指定超類,將直接繼承它,否則將間接地繼承它。
9.2 建構函式
建構函式是幾乎所有程式中都用的典型的魔法方法:__init__(是否記得,在定義My_Exception時用過)。
建構函式不同於普通方法的地方在於,將在物件建立後自動呼叫它們。
9.2.1 重寫普通方法和特殊的建構函式
每個類都有一個或多個超類,並從它們那裡繼承行為。對類B的例項呼叫方法(或訪問其屬性)時,如果找不到該方法(或屬性),將在其超類A中查詢。
'''
class A: def hello(self): print("你好,這是A類的方法。") class B(A): pass a=A() b=B() a.hello() b.hello()
'''
你好,這是A類的方法。
你好,這是A類的方法。
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
如果子類中也定義了一個和父類同名的方法,那麼子類物件呼叫這個方法時,它不再呼叫父類方法而呼叫子類方法。
'''
class D(A): def hello(self): print("你好,這是D類的方法。") d=D() d.hello()
'''
你好,這是A類的方法。
你好,這是A類的方法。
你好,這是D類的方法。
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
這種在子類中重新定義同名方法的方法就叫方法重寫。
重寫是繼承機制的一個重要方面,對建構函式來說尤其重要。建構函式用於初始化新建物件的狀態,而對大多數子類來說,除超類的初始化程式碼外,還需要有自己的初始化程式碼。雖然所有方法的重寫機制都相同,但與重寫普通方法相比,重寫建構函式時更有可能遇到一個特別的問題:重寫建構函式時,必須呼叫超類(繼承的類)的建構函式,否則可能無法正確地初始化物件。
'''
class C:
def __init__(self):
self.a=10
print("我是C類")
def ser(self):
print("我是C類的ser方法",self.a)
class E(C):
def __init__(self):
print("我是E類")
c=C()
e=E()
c.ser()
#e.ser()
'''
我是C類
我是E類
我是C類的ser方法 10
Traceback (most recent call last):
File "xx.py", line29, in <module>
e.ser()
File "xx.py", line 20, in ser
print("我是C類的ser方法",self.a)
AttributeError: 'E' object has no attribute 'a'
------------------
(program exited with code: 1)
請按任意鍵繼續. . .
為何會這樣呢?因為在子類E中重寫了建構函式,但新的建構函式沒有包含任何初始化屬性父類a的程式碼。要消除這種錯誤,子類E的建構函式必須呼叫其父類C的建構函式,以確保基本的初始化得以執行。為此,有兩種方法:呼叫未關聯父超類建構函式,或者使用函式super。
9.2.2 呼叫未關聯的建構函式
(這主要應用於Python2.x版本,因此略去)
9.2.3使用函式super
'''
class F(C):
def __init__(self):
super().__init__()
print("我是F類")
c=C()
f=F()
c.ser()
f.ser()
'''
我是C類
我是F類
我是C類的ser方法 10
我是C類的ser方法 10
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
9.3 元素訪問
雖然__init__無疑是你目前遇到的最重要的特殊方法,但還有不少其他的特殊方法,讓你能夠完成很多很酷的任務。
9.3.1 基本的序列和對映協議
序列和對映基本上是元素(item)的集合,要實現它們的基本行為(協議),不可變物件需要實現2個方法,而可變物件需要實現4個。
i、__len__(self):這個方法應返回集合包含的項數,對序列來說為元素個數,對對映來說為鍵-值對數。如果__len__返回零(且沒有實現覆蓋這種行為的__nonzero__),物件在布林上下文中將被視為假(就像空的列表、元組、字串和字典一樣)。
ii、__getitem__(self, key):這個方法應返回與指定鍵相關聯的值。對序列來說,鍵應該是0~n -1的整數(也可以是負數,這將在後面說明),其中n為序列的長度。對對映來說,鍵可以是任何型別。
iii、__setitem__(self, key, value):這個方法應以與鍵相關聯的方式儲存值,以便以後能夠使用__getitem__來獲取。當然,僅當物件可變時才需要實現這個方法。
iv、__delitem__(self, key):這個方法在對物件的組成部分使用__del__語句時被呼叫,應刪除與key相關聯的值。同樣,僅當物件可變(且允許其項被刪除)時,才需要實現這個方法。
對於這些方法,還有一些額外的要求。
i、對於序列,如果鍵為負整數,應從末尾往前數。換而言之,x[-n]應與x[len(x)-n]等效。
ii、如果鍵的型別不合適(如對序列使用字串鍵),可能引發TypeError異常。
iii、對於序列,如果索引的型別是正確的,但不在允許的範圍內,應引發IndexError異常。
'''
d={"a":10,"b":20,"c":30,"d":20,"e":10,}
len1=d.__len__()
getitem1=d.__getitem__("c")
print(len1,"\t",getitem1)
d.__setitem__("f",90)
print(d.__len__())
print(d.__len__(),"\t",d.__getitem__("f"))
d.__delitem__("f")
print(d.__len__(),"\t",d.__getitem__("c"))
'''
5 30
6
6 90
5 30
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
以上程式如果最後一句改為print(d.__len__(),"\t",d.__getitem__("c"))將會報錯。
9.3.2 從list、dict和str派生
可以建立一些繼承了list、dict和str的新類,以滿足不同的需要。形如:
'''
class NewList(list):
def __init__(self,*args):
super().__init__(*args)
#定義新方法或重寫list的方法
pass
'''
9.4 其他魔法方法9.5 特性
封裝是面向物件程式設計的一個重要的特性,但是經過封裝的類,是不能直接訪問私有屬性的,為了達到訪問類中私有屬性的目的,在類中含必須建立一些方法,利用這些方法達到間接訪問這些私有屬性的目的。由這種方法訪問的屬性稱為特性(property)。
9.5.1 函式property
property() 函式的作用是在類中返回屬性值。將 property 函式用作裝飾器可以很方便的建立只讀屬性。
格式為class property([fget[, fset[, fdel[, doc]]]])其中:fget為獲取屬性值的函式;fset為設定屬性值的函式;fdel為刪除屬性值函式;doc為屬性描述資訊。呼叫函式property時,還可不指定引數、指定一個引數、指定三個引數或指定四個引數。如果沒有指定任何引數,建立的特性將既不可讀也不可寫。如果只指定一個引數(獲取方法) ,建立的特性將是隻讀的。第三個引數是可選的,指定用於刪除屬性的方法(這個方法不接受任何引數)。第四個引數也是可選的,指定一個文件字串。這些引數分別名為fget、fset、fdel和doc。如果你要建立一個只可寫且帶文件字串的特性,可使用它們作為關鍵字引數來實現。
'''
class Rect:
def __init__(self):
self.__width=0
self.__leng=0
def get_size(self):
if self.__width and self.__leng:
return "矩形的寬{},矩形的長{}".format (self.__width,self.__leng)
else:
return 0
def set_size(self,size):
self.__width,self.__leng=size
def del_size(self):
del self.__width
del self.__leng
size=property(get_size,set_size,del_size,"隱藏了set、get、del方法")
r=Rect()
r.size=10,5
print(r.size)
r.size=150,100
print(r.size)
'''
矩形的寬10,矩形的長5
矩形的寬150,矩形的長100
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
這樣不但隱藏了類的私有屬性而且把類的set、get及del方法也隱藏了。
9.5.2 靜態方法和類方法
靜態方法和類方法是這樣建立的:將它們分別包裝在staticmethod和classmethod類的物件中。靜態方法的定義中沒有引數self,可直接通過類來呼叫。類方法的定義中包含類似於self的引數,通常被命名為cls。對於類方法,也可通過物件直接呼叫,但引數cls將自動關聯到類。
'''
class MyClass():
__a=0
b=1
@staticmethod #靜態方法裝飾器(以後說)
def smeth(): #定義靜態方法
print("這是靜態方法")
@classmethod #類方法裝飾器(以後說)
def cmeth(cls): #定義類方法
print("這是類方法")
def __init__(self):
print("這是MyClass類")
def run(self):
print("這是run方法")
MyClass.smeth() #靜態方法和類方法不需要例項,可直接呼叫。
MyClass.cmeth()
my_class=MyClass()
my_class.run() #例項方法不需例項化才能呼叫
'''
這是靜態方法
這是類方法
這是MyClass類
這是run方法
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
9.5.3 __getattr、__setattr__等方法
這些方法大都是攔截對物件屬性訪問的。
i、__getattribute__(self, name)在屬性被訪問時自動呼叫(只適用於新式類)。
ii、__getattr__(self, name)在屬性被訪問而物件沒有這樣的屬性時自動呼叫。
iii、__setattr__(self, name, value)試圖給屬性賦值時自動呼叫。
iv、__delattr__(self, name)試圖刪除屬性時自動呼叫。
相比函式property,這些魔法方法使用起來要棘手些(從某種程度上說,效率也更低),但它們很有用,因為你可在這些方法中編寫處理多個特性的程式碼。然而,在可能的情況下,還是使用函式property吧。
9.6 迭代器
將更詳細地介紹迭代器。對於魔法方法,這裡只介紹__iter__,它是迭代器協議的基礎。
9.6.1 迭代器協議
迭代(iterate)意味著重複多次,就像迴圈那樣。本書前面只使用for迴圈迭代過序列和字典,但實際上也可迭代其他物件:實現了方法__iter__的物件
方法__iter__返回一個迭代器,它是包含方法__next__的物件,而呼叫這個方法時可不提供任何引數。當你呼叫方法__next__時,迭代器應返回其下一個值。如果迭代器沒有可供返回的值,應引發StopIteration異常。你還可使用內建的便利函式next,在這種情況下,next(it)與it.__next__()等效。
使用迭代器不是像for in那樣一次獲取所有的值,而是根據需要一次可獲取一個或幾個值,因此它節省空間.使用迭代器更通用、更簡單、更優雅。
還有一些序列不能用列表的方式顯示,這時迭代器就是更好的工具。下面來看一個不能使用列表的示例,因為如果使用列表,這個列表的長度必須是無窮大的!這個“列表”為斐波那契數列。
為更好的理解書中的例子,先了解一下下面的程式:
'''
a=0
b=1
a,b=b,a+b
'''
等價於:a=b,b=a+b,即把後一個數傳給前一個數(b賦值給a),
而後一個數得到與前一個數的和(即把a+b賦給b)。
'''
print(a)
print(b)
for i in range(5):
a,b=b,a+b
print("_"*80)
print(a)
print(b)
'''
1
1
_______________________________________________________________________________
1
2
_______________________________________________________________________________
2
3
_______________________________________________________________________________
3
5
_______________________________________________________________________________
5
8
_______________________________________________________________________________
8
13
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
斐波那契數列:
'''
class Fibonacci:
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 #返回例項化物件
fib=Fibonacci()
for f in fib:
if f>1000:
print(f)
break
'''
1597
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
9.6.2從迭代器建立序列
除了對迭代器和可迭代物件進行迭代(通常這樣做)之外,還可將它們轉換為序列。
'''
class Test:
value = 0
sums=0
def __next__(self):
self.value+=1
self.sums=1+(1/self.value) #無限序列(如果以此為基礎輸出常數e的近似值怎麼辦?)
if self.value>10:
raise StopIteration #設定異常終止程式(StopIteration異常是在迴圈物件窮盡所有元素時的報錯)
return self.sums
def __iter__(self):
return self
ti = Test()
print(list(ti))
'''
[2.0, 1.5, 1.3333333333333333, 1.25, 1.2, 1.1666666666666667, 1.1428571428571428
, 1.125, 1.1111111111111112, 1.1]
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
9.7 生成器
生成器是一種使用普通函式語法定義的迭代器。
9.7.1 建立生成器
生成器(generator)即是一個含有yield的函式。反過來說包含yield語句的函式都被稱為生成器。
生成器和一般函式不同,它不是使用return返回一個值,而是可以生成多個值,每次一個。每次使用yield生成一個值後,函式都將凍結,即在此停止執行,等待被重新喚醒。被重新喚醒後,函式將從停止的地方開始繼續執行。
生成器的另一個特點就是節省空間,它可以一邊迴圈一邊計算,可以在迴圈的過程中不斷推算出後續的元素。
要建立一個generator,有很多種方法。
第一種方法很簡單,只有把一個列表生成式的[]中括號改為()小括號,就建立一個generator
'''
a=[x for x in range(10)]
print(type(a))
b=(x for x in range(10))
print(type(b))
'''
<class 'list'>
<class 'generator'>
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
列表和生成器的輸出方式不同:
'''
print(a)
print("*"*80)
print(b)
print("*"*80)
print(next(b)) #它必須用next函式輸出,每次列印一個數字
print(next(b)) #第二次接著列印第二個數字
'''
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
***********************************************************************
<generator object <genexpr> at 0x000000000213CCF0>
***********************************************************************
0
1
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
也可以用for in迴圈:
'''
for i in b:
print(i)
'''
0
1
2
3
4
5
6
7
8
9
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
建立一個generator後,一般不會呼叫next(),而是通過for迴圈來迭代,並且不需要關心StopIteration的錯誤。
第二個建立生成器的方法就是定義含有yield關鍵字的“函式”。
'''
def fib(num):
i,x,y=0,0,1
while i<num:
yield y
x,y=y,x+y #斐波那契數列
i+=1
return
for j in fib(10):
print(j,end="\t")
'''
1 1 2 3 5 8 13 21 34 55
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
9.7.2 遞迴式生成器
'''
def gen(nums):
try:
try:nums+'' #篩選字串
except TypeError:pass
else:
raise TypeError
for n in nums:
for i in gen(n): #遞迴
yield i
except TypeError:
yield nums
nums=[22,33,44,[44,67,[88,23,19]]]
lis=list(gen(nums))
print(lis)
'''
[22, 33, 44, 44, 67, 88, 23, 19]
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
以上可以看出,在同為可迭代的情況下,生成器和函式僅有yield和return的區別。
9.7.4 通用生成器
生成器由兩個單獨的部分組成:生成器的函式和生成器的迭代器。生成器的函式是由def語句定義的,其中包含yield。生成器的迭代器是這個函式返回的結果。用不太準確的話說,這兩個實體通常被視為一個,通稱為生成器。
'''
def generator():
print("一")
yield 10
print("二")
yield 18
print("三")
yield 22
print("四")
yield 60
g=generator()
print(next(g))
print(next(g))
'''
一
10
二
18
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
用這種方式可以生成任意的資料序列,且能達到節約空間的目的。
9.7.4 生成器(generator}的方法
在生成器開始執行後,可使用生成器和外部之間的通訊渠道向它提供值。這個通訊渠道包含如下兩個端點。
i、外部世界:外部世界可訪問生成器的方法send,這個方法類似於next,但接受一個引數(要傳送的“訊息”,可以是任何物件)。
ii、生成器:在掛起的生成器內部,yield可能用作表示式而不是語句。換而言之,當生成器重新執行時,yield返回一個值——通過send從外部世界傳送的值。如果使用的是next,yield將返回None。僅當生成器被掛起(即遇到第一個yield)後,使用send(而不是next)才有意義。要在此之前向生成器提供資訊,可使用生成器的函式的引數。
send函式官方文件是這樣說的,“恢復執行,並將一個值傳送到生成器函式。引數為當前輸出的結果。send()方法返回下一個值,如果到達生成器末尾,則引發StopIteration。如果是剛啟動生成器就用send函式,那麼就用None作為其引數呼叫send函式,因為不知道當前輸出的結果。”——其實在不知道當前輸出的結果時也要用None作為引數。
'''
print(g.send(18))
print(g.send(None))
'''
三
22
四
60
------------------
(program exited with code: 0)
請按任意鍵繼續. . .
生成器還包含另外兩個方法。
方法throw:用於在生成器中(yield表示式處)引發異常,呼叫時可提供一個異常型別、一個可選值和一個traceback物件。
方法close:用於停止生成器,呼叫時無需提供任何引數。
9.7.5 模擬生成器
(主要針對較老的Python版本,略)
9.8 八皇后問題
9.8.1 生成器的回溯
在有些應用程式中,你不能馬上得到答案。你必須嘗試多次,且在每個遞迴層級中都如此。打個現實生活中的比方吧,假設你要去參加一個很重要的會議。你不知道會議在哪裡召開,但前面有兩扇門,而會議室就在其中一扇門的後面。你選擇進入左邊那扇門後,又看到兩扇門。你再次選擇進入左邊那扇門,但發現走錯了。因此你往回走,並進入右邊那扇門,但發現也走錯了。因此你繼續往回走到起點,現在可以嘗試進入右邊那扇門。這就是回溯.對於逐步得到結果的複雜遞迴演算法,非常適合使用生成器來實現。
虛擬碼:
for 第一級的可能路徑:
for 第一級的可能路徑:
for 第一級的可能路徑:
.
.
.
for 第一級的可能路徑:
能解決嗎?
9.8.2 問題
.
.
.
(待續)