歡迎光臨
每天分享高質量文章

全面深入瞭解 Python 魔法函式

魔法函式概念

魔法函式是以雙下劃線開頭並且以雙下劃線結尾的功能函式,可以用來定義自己類的新特性。

舉一個例子:

class magic:
    def __init__(self,num):
        self.num = num

    def __getitem__(self, item):
        return self.num[item]

a = magic(['1','2','3'])
for x in a:
    print x

傳回結果:

 

1
2
3

 

簡直鵝妹子嚶!

這裡的__getitem__是魔法函式中的其中一個,具有的功能是傳回一個有序化陣列的值。在定義的類magic中,你取用了一個魔法函式,這個magic類就擁有了該魔法函式的功能。

當使用for迴圈的時候,因為getitem基於你的magic類一個可迭代的功能,所以magic類具有可迭代功能。

魔法函式對類的影響

如上若是,僅僅在類中添加了getitem這個魔法函式,就能直接使用for迴圈,也就是說魔法函式在一定程度上可以影響python自定義類的語法,或者說是增強了你這個類的型別。

透過python內建的大量魔法函式,你可以創造出具有獨特個性的資料型別,符合業務的需求。

舉個例子:

class magic:
    '''
    這是功能性註釋
    使用__doc__就可以看到啦
    '''
    def __init__(self,num):
        self.num = num

    def __len__(self):
        return 6666666

a = magic(5)
print len(a)
print a.__doc__

傳回結果:

 

6666666

    這是功能性註釋
    使用__doc__就可以看到啦

 

透過魔法函式__len__實現獲取,len本來是獲取字串或者串列數量長度,但是透過自定義類就實現了傳回6666666.

內建魔法函式

python中內建了大量的魔法函式,嘗試理解和記下這些魔法函式在以後的業務需求中可以如魚得水,所以說還是要背啊~~

字串表示

 

1. __repr__ 格式化字串式樣,主用開發樣式下
2. __str__    常用的字串,格式化字串

這連個魔法函式的作用都是和字串相關,一般來說在print列印中會呼叫這個魔法函式

 

class magic:
    def __init__(self,num):
        self.num = num

    def __str__(self):
        return (self.num + '\n')*5

a = magic('浪子好帥啊')
print a

 

輸出結果:

 

浪子好帥啊
浪子好帥啊
浪子好帥啊
浪子好帥啊
浪子好帥啊

 

這裡使用print a和使用 print a.__str__()效果是一樣的。同理repr(a)和a.__repr__

區別:

 

__repr__ 目的是為了表示清楚,是為開發者準備的。

__str__ 目的是可讀性好,是為使用者準備的。

__repr__ 應該盡可能的表示出一個物件來源的類以及繼承關係,方便程式員們瞭解這個物件。而 __str__ 就簡單的表示物件,而不要讓不懂程式設計的以為輸出的是 bug。

同時定義 repr 方法和 str 方法時,print() 方法會呼叫 str 方法。

集合序列

1. __len__
2. __getitem__
3. __setitem__
4. __delitem__
5. __contains__

迭代

 

1. __iter__
2. __next__

 

在儲存資料的資料結構中有list,set,tuple,dict,當使用for迴圈他們的時候,本質上是做了兩件事。

  1. 獲得一個迭代物件,呼叫__iter__魔法函式
  2. 迴圈的時候,迴圈呼叫__next__魔法函式

舉個例子:

class magic:
    def __init__(self,num):
        self.num = num

    def __iter__(self):
        # 使用__iter__,magic類就變成了可迭代物件
        return self

    def __next__(self):
        # __next__魔法函式的作用是在迴圈的時候,無限提供輸出下一個值,直到沒有資料後丟擲異常
        if self.num >5:
            # 設定上限
            raise StopIteration
        else:
            self.num += 1
            return self.num

a = magic(-5)
for x in a:
    print(x)

傳回結果:

-4
-3
-2
-1
0
1
2
3
4
5
6

可能有些難理解,這是第一次自己做出來的一個資料型別,他的作用是提供一個原始值,自增長到6就停止。

如果這樣做也可以的:

a = magic(-5)
print(a.__next__())
print(next(a))

傳回結果:

 

-4
-3

 

總的來說,使用iter魔法函式,這個類就變成了可迭代物件,但是如何呼叫這個可迭代物件的數值呢?這個時候就需要使用next來迴圈呼叫了。

註意:含有__next__()函式的物件都是一個迭代器(Iterator),也就是說__next__要繼承Iterator,__iter__要繼承Iterable,繼承的類來自與collections

可呼叫

1. __call__

一個類實體變成一個可呼叫物件,只需要實現一個特殊方法__call__()。在建立類的時候,只要使用了call這個魔法函式,那麼這個類就是可呼叫的。

函式之所以可以被直接呼叫,原因在於他的底層是用call實現的。

舉個例子:

class magic:
    def __init__(self):
        pass
    def __call__(self, num):
        return '浪子:' + num

a = magic()
print(a('admin'))

執行結果:

 

浪子:admin

 

可以看到結果直接執行出來了。這樣的例子不夠深刻,嘗試把場景轉移到業務需求中來。

浪子餐館賣饅頭,每次買出一個,店小二就會大喊’xxx買了一個饅頭~花了1塊錢~~’

使用類來實現:

class ao:
    def __init__(self,name):
        self.name =name

    def __call__(self, money):
        return self.name + '大老闆買了一個饅頭~~花了%s元~~讓我們感謝這位老鐵~~'%money

a = ao('小桃紅')
print(a(15))
b = ao('貓餅餅')
print(b(666))

執行結果:

小桃紅大老闆買了一個饅頭~~花了15元~~讓我們感謝這位老鐵~~
貓餅餅大老闆買了一個饅頭~~花了666元~~讓我們感謝這位老鐵~~

總的來說,就是你定義類中只要有call,那麼就可以直接呼叫,註意call是物件不是類。他和new以及init的關係如下(可以先不看)

  1. new: 物件的建立,是一個靜態方法,第一個引數是cls。(想想也是,不可能是self,物件還沒建立,哪來的self)

  2. init : 物件的初始化, 是一個實體方法,第一個引數是self。

  3. call : 物件可call,註意不是類,是物件。

問題:為了讓下麵這段程式碼執行,需要增加哪些程式碼?

class A(object):
    def __init__(self,a,b):
        self.__a = a
        self.__b = b
    def myprint(self):
        print 'a=', self.__a, 'b=', self.__b

a1=A(10,20)
a1.myprint()

a1(80)

為了能讓物件實體能被直接呼叫,需要實現call方法

class A(object):
    def __init__(self,a,b):
        self.__a = a
        self.__b = b
    def myprint(self):
        print('a=', self.__a, 'b=', self.__b)
    def __call__(self, *args, **kwargs):
        print(args)

with背景關係管理

1. __enter__
2. __exit__

with背景關係管理器,對於那些需要必須成對開啟關閉的操作是非常方便的,比如開啟關閉檔案。他的實現原理就是透過enter和exit這兩個魔法函式來實現的。

先看看常規的實現一個with背景關係管理器的步驟,Pymysql with 操作

import contextlib
@contextlib.contextmanager
def mysql(host='127.0.0.1',user='root',passwd='root',db='meizi',port=3306,charset='utf8'):
    conn = pymysql.connect(host='127.0.0.1',user='root',passwd='root',db='meizi',port=3306,charset='utf8')
    cursor = conn.cursor()
    try:
        yield cursor
    finally:
        conn.commit()
        cursor.close()
        conn.close()
# # 執行sql
# with mysql() as cursor:
#    print(cursor)
#    row_count = cursor.execute("select * from tb7")
#    row_1 = cursor.fetchone()
#    print row_count, row_1

想要自己實現這種類的話,就必須要使用到enter和exit這兩個魔法函式。

比如上面的

with mysql() as cursor

把步驟分析一下:

  1. 當with的後面mysql()函式被執行的時候,物件的enter方法被呼叫
  2. 函式主動發起資料庫連線,獲取一個遊標
  3. 隨後使用yield生成器寄存這個遊標
  4. 這個遊標被賦值給as後面的cursor
  5. 當with後面的程式碼全都執行完畢後,呼叫前面傳回物件的exit方法

舉個例子:

class magic:
    def __enter__(self):
        print('enter魔法函式執行')
        return 'enter魔法函式執行完畢'
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exti魔法函式執行')
        return 'exit魔法函式執行完畢'

def ma():
    return magic()

with ma() as a:
    print('隨便執行一些東西')

執行結果:

enter魔法函式執行
隨便執行一些東西
exti魔法函式執行

是不是想到了裝飾器?透過檢視contextlib庫的原始碼發現匯入了wrapper裝飾器,說到裝飾器你是不是你又想到了閉包?然後又想到了變數的作用域?

是的,這些知識都是一個完整的體系,相互串聯。

還沒結束,繼續深入分析。

在exit魔法函式的值中的含義

exc_type:異常類(如果丟擲異常,這裡獲取異常的型別 )
exc_value:異常實體(如果丟擲異常,這裡顯示異常內容)
exc_tb:異常位置(如果丟擲異常,這裡顯示所在位置)
traceback:追溯物件()

因為with操作本身就是為了簡寫try/finally操作的。

比如在熟悉的with操作文字檔案一樣,開啟檔案是放在enter函式中,關閉檔案放在exit函式中。

with真正強大之處是它不僅可以完善的管理背景關係,同時還可以處理異常。

__enter__

__enter__ 用於賦值給 as 後面的變數。不過 with 陳述句中 as 不是必須的。__enter__ 和 __exit__ 必須並用。

__exit__

用於捕獲異常,它的傳回值是一個 boolean 物件。除了 self 之外,必須傳入另外三個引數,分別表示 exception 的型別,值(如 IndexError: list index out of range 中,冒號後面的部分就是值),以及 traceback。

傳回 True 則表示這個異常被忽略。

傳回 None, False 等則這個異常會丟擲。

如果要忽略所有的異常可以這樣寫:

def __exit__(self, exc_type, exc_value, traceback):
    return True

經過測試, SyntaxError 是不能忽略的,其他已知的是可以的。

數值轉換

 

1. __abs__
2. __bool__
3. __int__
4. __float__
5. __hash__
6. __index__

元類相關

1. __new__
2. __init__

說到init你會想到建立類的時候實體的物件,但是new是啥?

依照Python官方檔案的說法,__new__方法主要是當你繼承一些不可變的class時(比如int, str, tuple), 提供給你一個自定義這些類的實體化過程的途徑。還有就是實現自定義的metaclass。

是不是感覺有點迷?沒關係,用例子來說明就好了。

class magic(object):
    def __init__(self,nums):
        self.nums = nums
        print '666'
        print 'init:' + self.nums

    def __new__(cls, nums):
        cls.nums = nums
        print 'new:'+cls.nums

a = magic('A')

傳回結果:

 

new:A

 

這裡看到只輸出了new的物件,並且註意magic類繼承了object,在上一篇文章中有說道,元類(即類的類)都必須要繼承自object,因為記住了,繼承自object的新類才有__new__

重新修改一下程式碼,讓init也執行下呢?

class magic(object):
    def __init__(self,nums):
        self.nums = nums
        print '666'
        print 'init:' + self.nums

    def __new__(cls, nums):
        cls.nums = nums
        print 'new:'+cls.nums
        # 執行到這裡會列印內容
        return object.__new__(cls)
        # 這裡會傳回一個內容
        # 傳回的內容會傳遞到init中的self中去
a = magic('A')

傳回結果:

 

new:A
666
init:A

 

大家註意看,這裡傳回了object的new方法,然後init就執行了,這證明上面說的,new會先於init執行,並且new方法傳回的值就是init方法中的self。

繼續舉例子:

class magic(object):
    def __init__(self,nums):
        self.nums = self.nums
        self.langzi = 'langzi'
        print '666'
        print 'init:' + self.nums

    def __new__(cls, nums):
        print nums
        # 這裡打印出傳遞進來的數值
        cls.nums = nums+'BCDEFG'
        cls.langzi = '浪子'
        print 'new:'+cls.nums
        return object.__new__(cls)

a = magic('A')
print '-'*10
print a.langzi
print a.nums

先猜一猜會輸出什麼呢?

傳回結果:

A
new:ABCDEFG
666
init:ABCDEFG
----------
langzi
ABCDEFG

來分析一下

  1. 列印傳遞進來的A
  2. 隨後賦值給cls.nums,列印new:ABCDEFG,同時定義cls.langzi=浪子
  3. 繼續執行,new方法傳回object物件,這個時候會執行到init
  4. 這裡的self其實就是new中傳回的物件
  5. 然後定義self.nums和self.langzi=langzi(這個時候這個類的langzi物件就變成了langzi,不再是浪子)
  6. 列印666
  7. 打印出init:ABCDEFG
  8. 列印—————-
  9. 驗證這個類中的屬性值a.langzi和a.nums

透過分析這個實體步驟,來進一步研究new和init的關係:

透過上面程式碼的執行結果我們可以發現程式首先執行了new,之後執行的init,這說明,在類中,如果newinit同時存在會優先呼叫new

new方法會傳回所構造的物件,init則不會。init無傳回值。

new至少要有一個引數cls,代表要實體化的類(類物件),此引數在實體化時由Python直譯器自動提供。

new必須要有傳回值,傳回實體化出來的實體,這點在自己實現,new時要特別註意,可以return父類new出來的實體或者直接是object的new出來的實體。

init有一個引數self,就是這個new傳回的實體,initnew的基礎上可以完成一些其它初始化的動作,init不需要傳回值

我們可以將類比作製造商,new方法就是前期的原材料購買環節,init方法就是在有原材料的基礎上,加工,初始化商品環節

總而言之就是:

  1. new:建立物件時呼叫,會傳回當前物件的一個實體,new是在實體建立之前被呼叫的,因為它的任務就是建立實體然後傳回該實體,是個靜態方法,常用於允許繼承不可變型別(str,int, tuple)
  2. init:建立完物件後呼叫,對當前物件的一些實體初始化,無傳回值,init是當實體物件建立完成後被呼叫的,然後設定物件屬性的一些初始值。

下麵這段程式碼輸出什麼?

class B(object):
    def fn(self):
        print 'B fn'
    def __init__(self):
        print "B INIT"

class A(object):
    def fn(self):
        print 'A fn'

    def __new__(cls,a):
            print "NEW", a
            if a>10:
                return super(A, cls).__new__(cls)
            return B()

    def __init__(self,a):
        print "INIT", a

a1 = A(5)
a1.fn()
a2=A(20)
a2.fn()

傳回結果:

 

NEW 5
B INIT
B fn
NEW 20
INIT 20
A fn

 

使用new方法,可以決定傳回那個物件,也就是建立物件之前,這個可以用於設計樣式的單例、工廠樣式。init是建立物件是呼叫的。

屬性相關

1. __getattr__,__setattr__
2. __getattribute__,setattribute__
3. __dir__

屬性描述符

 

1. __get__,__set__,__delete__

協程

1. __await__
2. __aiter__
3. __anext__
4. __aenter__
5. __aexit__

這個是最重要也是最難的,以後慢慢說。

梳理

 

  1. 魔法函式是python內建的,具有雙下劃線開頭結尾的特性。
  2. 自定義的類中使用魔法函式,該類就具有了該魔法函式的功能,比如使用iter魔法函式,該類就具有迭代功能。
  3. 使用魔法函式實現高靈活性,實現自己所需要的獨特的資料型別。

    贊(0)

    分享創造快樂