1. 程式人生 > >python基礎教程(第三版)學習筆記(九)

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 問題
    .
    .
    .

(待續)