python魔法函式

奧辰發表於2020-09-30
 

1 什麼是魔法函式

 

先來定義一個類:

In [1]:
class Company(object):
    def __init__(self, employee_list):
        self.employee_list = employee_list
In [4]:
company = Company(['張三', '李四', '王五'])
print(company)
 
<__main__.Company object at 0x7f7c4046ebd0>
 

此時,直接對Company例項化的物件進行print輸出時,列印出來的資訊是類名稱和地址資訊。但如果我們想看的不是這些,而是想輸出employee_list,怎麼做呢?

In [7]:
class Company(object):
    def __init__(self, employee_list):
        self.employee_list = employee_list
    
    def __str__(self):
        return str(self.employee_list)
In [8]:
company = Company(['張三', '李四', '王五'])
print(company)
 
['張三', '李四', '王五']
 

在這個例子中,我們新增了一個__str__()函式,然後再列印輸出Company類例項時,輸出的就是employee_list,但是,我們並沒有顯式地呼叫__str__()函式,這是因為,在對一個例項使用print()函式時,Python內部機制自動會呼叫__str__()函式。

類似__str__()這種函式在類內部還有很多,這一類函式,我們統稱為魔法函式。現在,我們明確一下魔法函式的範疇:

魔法函式是指類內部以雙下劃線開頭,並且以雙下劃線結尾的函式,在特定時刻,Python會自動呼叫這些函式。魔法函式不是通過繼承等機制獲得的,而是類一旦定義,Python內部機制自動會給類賦予這些特殊的函式,且使用者是不能建立魔法函式的,即使函式名以雙下劃線開頭和雙下劃線結尾。通過魔法函式可以實現許多個性化、便捷的操作。

2 Python中的魔法函式

2.1 字串表示:__str____repr__

  • __str__

  • __repr__

在很多時候,人們都容易將__str____repr__兩個方法記混,甚至認為這兩的功能是一樣的,但事實上還是有一些差異的。

__str__在上文中已經說過,是用於將例項物件進行print輸出時使用。如下所示:

In [17]:
class Company(object):
    def __init__(self, name=None):
        self.name = name
        
    def __str__(self):
        return '*****公司名稱為:%s*****' % self.name
In [18]:
c = Company(name='騰訊')
print(c)
 
*****公司名稱為:騰訊*****
 

對例項化物件是用print()函式輸出時,Python內部機制會想呼叫str()方法,在str()方法內部繼續呼叫__str__方法實現輸出:

In [23]:
str(c)
Out[23]:
'*****公司名稱為:騰訊*****'
 

但是如果我們不是用print()函式而直接輸出c,那麼,輸出結果依然是原來預設的:

In [19]:
c
Out[19]:
<__main__.Company at 0x7f7c4049d050>
 

這是因為直接輸出類例項化物件時,呼叫的是__repr__方法:

In [20]:
class Company(object):
    def __init__(self, name=None):
        self.name = name
        
    def __str__(self):
        return '*****公司名稱為:%s*****' % self.name

    def __repr__(self):
        return '#####公司名稱為:%s#####' % self.name
In [22]:
c = Company(name='騰訊')
c
Out[22]:
#####公司名稱為:騰訊#####
 

綜上所述,__str____repr__的區別在於,__str__方法在對例項化物件是用print()函式輸出時呼叫,其實時Python內部機制呼叫str()方法,然後str()方法內部繼續呼叫__str__方法獲取輸出字串。而__repr__是在開發模式下直接輸出例項化物件時被呼叫。

2.2 集合、序列相關:__len____getitem____setitem____delitem____contains__

  • __len__

Python內建函式中有一個len()函式,這個函式適用於獲取序列型別資料的長度,在對一個例項使用len()方法時,真實輸出的其實是__len__的返回值。所以,只要一個類內部實現了__len__方法,就可以對其例項使用__len__方法。

In [24]:
class Company(object):
    def __init__(self, name=None, employee_lst=None):
        self.name = name
        self.employee_lst = employee_lst
        
    def __len__(self):
        return len(self.employee_lst)
In [26]:
c = Company(name='騰訊', employee_lst=['張三', '李四', '王五'])
len(c)
Out[26]:
3
 
  • __getitem____setitem____delitem__
 

我們知道,在Python的dict型別資料中,可以通過方括號的方式來賦值、取值和刪除值,例如通過t_dict['attr1'] = 1的方式進行賦值,通過t_dict['attr1']可以取得值,通過del t_dict['attr1']可以刪除一個值。那麼在自定義的一個類裡面,通過__getitem____setitem____delitem__這三個,我們也可以讓我們自定義類的例項化物件擁有這樣的操作。

In [48]:
class Company(object):
    def __init__(self):
        self.company_info = {}
        
    def __setitem__(self,key,value):  # 令類例項化物件可以通過c[key] = value的方式賦值
        self.company_info[key] = value
        
    def __getitem__(self,key):          # 令類例項化物件可以通過c[key]的方式取值
            return self.company_info[key]
        
    def __delitem__(self, key):          # 令類例項化物件可以通過del c[key]的方式刪除值
        del self.company_info[key]
In [51]:
c = Company()
c['name'] = '騰訊'
c['type'] = 'IT'
print(c['name'])
del c['name']
print(c.company_info)
 
騰訊
{'type': 'IT'}
 

有些時候,配合Python的反射機制類使用這三個魔法函式會有更加魔幻的效果,可以直接對例項屬性進行操作:

In [59]:
class Company(object):
        
    def __setitem__(self,key,value):
        setattr(self, key, value)
        
    def __getitem__(self,key):
        return getattr(self, key)
    
    def __delitem__(self, key):
        delattr(self, key)
In [60]:
c = Company()
c['name'] = '騰訊'
c['type'] = 'IT'
In [61]:
c['type']
Out[61]:
'IT'
In [62]:
del c['type']
In [63]:
c['type']
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-63-56601054285d> in <module>
----> 1c['type']

<ipython-input-59-b82d5d10cbb4> in __getitem__(self, key)
      5 
      6     def __getitem__(self,key):
----> 7return getattr(self, key)
      8 
      9     def __delitem__(self, key):

AttributeError: 'Company' object has no attribute 'type'
 
  • __contains__
 

對於Python中dict型別的資料結構,可以使用in關鍵字判斷序列內部是否包含某個key,在我們自定義的類中,如果定義了__contains__方法,那麼也能使用in關鍵字判斷是否包含某個屬性。

In [67]:
class Company(object):
    def __init__(self):
        self.company_info = {}
        
    def __contains__(self, key):
        return key in self.company_info
In [69]:
c = Company()
c.company_info['name'] = '騰訊'
print('name' in c)
print('type' in c)
 
True
False
 

結合反射機制使用:

In [70]:
class Company(object):
    def __setitem__(self,key,value):
        setattr(self, key, value)
        
    def __contains__(self, key):
        return hasattr(self, key)
In [75]:
c = Company()
c['name'] = '騰訊'
print('name' in c)
print('type' in c)
 
True
False
 

2.3 迭代相關:__iter____next__

  • __iter____next__

我之前寫過一篇部落格《為什麼for迴圈可以遍歷list:Python中迭代器與生成器》,很詳細得介紹了Python中關於迭代器與生成器的原理。關於迭代器和生成器,其核心就在於__iter____next__兩個方法。

iter是Iterable的簡寫,表示“可迭代的”,所以,任何內部定義了__iter__的物件,我們都可以稱之為可迭代物件,在Python中,有一個類專門與之對應:Iterable,我們可以通過判斷物件是否是Iterable類的例項來判斷是否是可迭代物件。進一步的,如果一個類內部定義了__iter__方法的同時,也定了__next__方法,那麼,它的例項化物件就是迭代器,也有一個類與迭代器對應,那就是Iterator。

In [99]:
from collections.abc import Iterable
from collections.abc import Iterator
In [81]:
isinstance(123, Iterable)  # 整型不是可迭代物件
Out[81]:
False
In [101]:
isinstance('abc', Iterator)  # 字串不是迭代器
Out[101]:
False
In [102]:
isinstance('abc', Iterable)  # 字串是可迭代物件
Out[102]:
True
In [103]:
class Company():
    def __iter__(self):  # 自定義一個類,只要實現了__iter__方法,就是可迭代物件
        pass
print('Company()是可迭代物件嗎:',isinstance(Company(),Iterable))
print('Company()是迭代器嗎:',isinstance(Company(),Iterator))
 
Company()是可迭代物件嗎: True
Company()是迭代器嗎: False
In [104]:
class Company():
    def __iter__(self):  
        pass
    def __next__(self):  # 自定義一個類,同時實現了__iter__方法和__next__方法,就是迭代器
        pass
print('Company()是可迭代物件嗎:',isinstance(Company(),Iterable))
print('Company()是迭代器嗎:',isinstance(Company(),Iterator))
 
Company()是可迭代物件嗎: True
Company()是迭代器嗎: True
 

知道怎麼區分可迭代物件和迭代器之後,就可以解釋__iter____next__的作用了。那就是定義了這兩個方法,就可以對例項化物件進行遍歷。以for迴圈為例,通過for迴圈對一個可迭代物件進行迭代時,for迴圈內部機制會自動通過呼叫iter()方法執行可迭代物件內部定義的__iter__方法來獲取一個迭代器,然後一次又一次得迭代過程中通過呼叫next()方法執行迭代器內部定義的__next__方法獲取下一個元素,當沒有下一個元素時,for迴圈自動捕獲並處理StopIteration異常。

In [94]:
class B():
    def __init__(self, lst):
        self.lst = lst
        self.index = 0
    def __iter__(self):
        print('B.__iter__()方法被呼叫')
        return self
    def __next__(self):
        try:
            print('B.__next__()方法被呼叫')
            value = self.lst[self.index]
            self.index += 1
            return value
        except IndexError:
            raise StopIteration()
In [98]:
b = B([1, 2, 3])
for i in b:
    print(i)
 
B.__iter__()方法被呼叫
B.__next__()方法被呼叫
1
B.__next__()方法被呼叫
2
B.__next__()方法被呼叫
3
B.__next__()方法被呼叫
 

2.4 可呼叫:__call__

  • __call__

假如有一個物件A,如果A是一個類,我們使用A()進行呼叫,那麼就是建立一個A類的例項化物件,如果A是一個函式,我們使用A()就是呼叫函式A。那麼,如果A是一個某個類的例項化物件時,A()是進行什麼操作呢?答案就是呼叫該類的__call__方法,我們可以理解為,__call__就是“()”運算子。

In [88]:
class Company(object):
    def __init__(self):
        pass
    def __call__(self, name):
        self.name = name
        print('__call__方法被呼叫,name:%s' % self.name)
In [89]:
c = Company()
c('騰訊')
 
__call__方法被呼叫,name:騰訊
 

現在,我們證實了__call__就是“()”運演算法,那麼,是不是類、函式這些可使用“()”運算子的物件內部都定義有__call__函式呢?答案是肯定的。

In [90]:
class Company(object):
    def __init__(self):
        pass
def A():
    pass
In [91]:
print('類Company是否有__call_方法:', hasattr(Company, '__call__'))
print('函式A是否有__call_方法:', hasattr(A, '__call__'))
 
類Company是否有__call_方法: True
函式A是否有__call_方法: True
 

藉助這一特性,我們可以彌補hasattr()函式的不足。我們知道,通過hasattr()函式可以判斷一個類內部是否有某個屬性,但是沒法判斷到底是變數還是方法,但進一步藉助方法內部肯定定義有__call__這個特性,就可以進一步判斷。

In [92]:
class Company(object):
    def __init__(self):
        self.name = None
    def func(self):
        pass
In [93]:
c = Company()
print('c中是否存在屬性name:', hasattr(c, 'name'))
print('c中是否存在屬性func:', hasattr(c, 'func'))
print('name是函式嗎:', hasattr(c.name, '__call__'))
print('func是函式嗎:', hasattr(c.func, '__call__'))
 
c中是否存在屬性name: True
c中是否存在屬性func: True
name是函式嗎: False
func是函式嗎: True
 

2.5 with上下文管理器:__enter____exit__

只要你熟悉Python開發,那麼對with上下文管理就一定不會陌生,例如操作文字時,我們通常習慣with open來對開啟檔案,獲得控制程式碼。使用with來開啟檔案的好處就是在開啟檔案後進行操作的過程中,無論是否出現異常,Python都會對關閉控制程式碼,也就是一定會進行收尾工作,避免佔用記憶體資源。

這種上下文管理機制是怎麼實現的呢?這就涉及到我們現在要說的兩個兩個魔法函式__enter____exit__

__enter__:with語句開始執行時候呼叫

__exit__:with語句結束時候呼叫,注意,無論with語句中的程式碼是否正常結束,都會執行__exit__方法

除了讀寫檔案之外,我們使用Python來運算元據庫時,也需要做收尾處理,也就是關閉資料庫連線,那麼,這個時候我們也可以用with來進行。

In [3]:
import pymysql


class Dao(object):
    def __init__(self, cursor_type=None):
        self.conn = pymysql.connect( # 建立資料庫連線
            host='192.168.31.201', # 要連線的資料庫所在主機ip
            database='test',
            user='root', # 資料庫登入使用者名稱
            password='admin123456', # 登入使用者密碼
            charset='utf8' # 編碼,注意不能寫成utf-8
        )
               
        self.cursor = None
        if cursor_type:
            self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)
        else:
            self.cursor = self.conn.cursor()

    def __enter__(self):
        return self.cursor  # 返回類例項本身

    def __exit__(self, exc_type, exc_value, exc_trace):
        self.conn.commit()  # 提交事務
        self.cursor.close()  # 關閉遊標
        self.conn.close()  # 關閉資料庫連線
In [6]:
with Dao() as cursor:
    cursor.execute("select * from employee;")
    e = cursor.fetchall()
    print(e)
 
((1, '張三'), (2, '李四'))
 

2.6 屬性相關:__getattr____setattr____getattribute__

  • __getattr____setattr__

__getattr__函式的作用: 在一個類例項中查詢一個屬性時,通過__dict__失敗, 那麼會呼叫到類的__getattr__函式,如果沒有定義這個函式,那麼丟擲AttributeError異常。也就是說__getattr__是屬性查詢的最後一步。

In [13]:
class Company(object):
    def __init__(self, name):
        self.company_name = name
    
    def fun(self):
        print('fun方法被呼叫……')
        
    def __getattr__(self, name):
        print('__getattr__方法被呼叫')
        raise AttributeError('哥們,你查詢的屬性"%s"不存在' % name)
In [14]:
c = Company('騰訊')
 

如果提前找到了某個屬性,那麼將不會繼續呼叫__getattr__

In [15]:
print(c.company_name)
print(c.fun)
 
騰訊
<bound method Company.fun of <__main__.Company object at 0x7fa0a8077100>>
 

當屬性不存在是,將會呼叫__getattr__,所以,我們可以通過__getattr__函式來定義當找不到屬性時候的提醒方式,甚至是返回一個其他的預設值。

In [16]:
c.abc
 
__getattr__方法被呼叫
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-a2bb1cff9d71> in <module>
----> 1c.abc

<ipython-input-13-810c2a9c4f3c> in __getattr__(self, name)
      8     def __getattr__(self, name):
      9         print('__getattr__方法被呼叫')
---> 10raise AttributeError('哥們,你查詢的屬性"%s"不存在' % name)

AttributeError: 哥們,你查詢的屬性"abc"不存在
 

通過__getattr__方法,我們可以對Python的字典進行改造,另外開始通過dict_name.key的方式來訪問。

In [21]:
class Dict(dict):
    def __init__(self, *args, **kwargs):
        super(Dict, self).__init__(*args, **kwargs)
        
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
In [22]:
d = Dict({'name': '張三', 'age': '李四'})
d.name
Out[22]:
'張三'
 

__getattr__是用來獲取屬性,那麼__setattr__就是用來給屬性賦值,當我們使用例項.key=value的方式進行賦值的時候就一定會呼叫__setattr__方法。

In [27]:
class Company(object):
    def __init__(self, name):
        self.company_name = name

    def __setattr__(self, name, value):
        print("__setattr__方法被呼叫")
#         self.name = value   # 第一種寫法
#         object.__setattr__(self, name, value)   # 第二種寫法
        self.__dict__[name] = value         # 第三種寫法
In [29]:
c = Company('騰訊')
c.company_name = '阿里'
print(c.company_name)
 
__setattr__方法被呼叫
__setattr__方法被呼叫
阿里
 

為什麼__setattr__被呼叫了兩次呢?因為在__init__中也使用了一次例項.key=value的方式賦值。

所以,在定義__setattr__的時候一定要注意,一定不能使用上述程式碼中被註釋掉的第一種寫法,因為使用self.name = value進行賦值時,本身又會再次呼叫__setattr__方法,這就造成了無線遞迴,造成bug。所以使用第二和第三種寫法才是正確的。

 

繼續用__setattr__方法改造字典:

In [30]:
class Dict(dict):
    def __init__(self, *args, **kwargs):
        super(Dict, self).__init__(*args, **kwargs)
        
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
            
    def __setattr__(self, key, name):
        self[key] = name
In [31]:
d = Dict()
d.name = '張三'
print(d.name)
 
張三
 
  • __getattribute__
 

__getattribute__與上面的__getattr__很相似,區別在於__getattr__是在類中未找到屬性時呼叫,而__getattribute__是不管類中有無查詢的屬性存在,都優先呼叫。不過在使用__getattribute__方法市,必須注意陷入無限遞迴,當在__getattribute__程式碼塊中,再次執行屬性的獲取操作時,會再次觸發__getattribute__方法的呼叫,程式碼將會陷入無限遞迴,直到Python遞迴深度限制,所以,在__getattribute__中獲取屬性時,需要通過父類的__getattribute__方法獲取對應的屬性。

In [32]:
class Company(object):
    def __init__(self, name):
        self.company_name = name
    
    def __getattribute__(self, name):
        print('__getattribute__方法被呼叫')
        return object.__getattribute__(self, name)
#         raise AttributeError('哥們,你查詢的屬性"%s"不存在' % name)
In [33]:
c = Company('騰訊')
c.company_name
 
__getattribute__方法被呼叫
Out[33]:
'騰訊'
In [34]:
c.abc
 
__getattribute__方法被呼叫
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-34-a2bb1cff9d71> in <module>
----> 1c.abc

<ipython-input-32-e6bee225b017> in __getattribute__(self, name)
      5     def __getattribute__(self, name):
      6         print('__getattribute__方法被呼叫')
----> 7return object.__getattribute__(self, name)
      8 #         raise AttributeError('哥們,你查詢的屬性"%s"不存在' % name)

AttributeError: 'Company' object has no attribute 'abc'
 
  • __dict__dir()__dir__
 

上文中提到過__dict____dict__是物件的一個屬性,並不是函式,它的作用是返回物件的所有屬性名為key,屬性值為value的一個字典,注意,這裡所說的所有屬性是指資料物件本身的屬性,例如類的__dict__只包含類本身的屬性和函式,而類例項也只包含類例項的屬性。這一點與dir()函式不同,dir()將會返回一個列表,列表中包含物件所有有關的屬性名。也就是說,__dict__dir()的子集。而dir()實際上呼叫的是__dir__方法。

In [37]:
class Company(object):
    def __init__(self, name):
        self.company_name = name
    
    def fun(self):
        print('fun方法被呼叫……')
In [38]:
c = Company('騰訊')
In [40]:
c.__dict__
Out[40]:
{'company_name': '騰訊'}
In [41]:
Company.__dict__
Out[41]:
mappingproxy({'__module__': '__main__',
              '__init__': <function __main__.Company.__init__(self, name)>,
              'fun': <function __main__.Company.fun(self)>,
              '__dict__': <attribute '__dict__' of 'Company' objects>,
              '__weakref__': <attribute '__weakref__' of 'Company' objects>,
              '__doc__': None})
In [44]:
dir(c)
Out[44]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'company_name',
 'fun']
In [45]:
c.__dir__()
Out[45]:
['company_name',
 '__module__',
 '__init__',
 'fun',
 '__dict__',
 '__weakref__',
 '__doc__',
 '__repr__',
 '__hash__',
 '__str__',
 '__getattribute__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__new__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']

相關文章