Python進(jìn)階——如何正確使用魔法方法?(上)
閱讀本文大約需要 10 分鐘。
在做 Python 開發(fā)時(shí),我們經(jīng)常會(huì)遇到以雙下劃線開頭和結(jié)尾的方法,例如 __init__、__new__、__getattr__、__setitem__ 等等,這些方法我們通常稱之為「魔法方法」,而使用這些「魔法方法」,我們可以非常方便地給類添加特殊的功能。
這篇文章,我們就來(lái)分析一下,Python 中的魔法方法都有哪些?使用這些魔法方法,我們可以實(shí)現(xiàn)哪些實(shí)用的功能?
魔法方法概覽
首先,我們先對(duì) Python 中的魔法方法進(jìn)行歸類,常見的魔法方法大致可分為以下幾類:
構(gòu)造與初始化 類的表示 訪問控制 比較操作 容器類操作 可調(diào)用對(duì)象 序列化
由于魔法方法分類較多,這篇文章我們先來(lái)看前幾個(gè):構(gòu)造與初始化、類的表示、訪問控制。剩下的魔法方法,我們會(huì)在下一篇文章進(jìn)行分析講解。
構(gòu)造與初始化
首先,我們來(lái)看關(guān)于構(gòu)造與初始化相關(guān)的魔法方法,主要包括以下幾種:
__init____new____del__
__init__
關(guān)于構(gòu)造與初始化的魔法方法,我們使用最頻繁的一個(gè)就是 __init__ 了。
我們?cè)诙x類的時(shí)候,通常都會(huì)去定義構(gòu)造方法,它的作用就是在初始化一個(gè)對(duì)象時(shí),定義這個(gè)對(duì)象的初始值。
# coding: utf8
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person('張三', 25)
p2 = Person('李四', 30)
__new__
在初始化一個(gè)類的屬性時(shí),除了使用 __init__ 之外,還可以使用 __new__ 這個(gè)方法。
我們?cè)谄綍r(shí)開發(fā)中使用的雖然不多,但是經(jīng)常能夠在開源框架中看到它的身影。實(shí)際上,這才是「真正的構(gòu)造方法」。
# coding: utf8
class Person(object):
def __new__(cls, *args, **kwargs):
print "call __new__"
return object.__new__(cls, *args, **kwargs)
def __init__(self, name, age):
print "call __init__"
self.name = name
self.age = age
p = Person("張三", 20)
# Output:
# call __new__
# call __init__
從例子我們可以看到,__new__ 會(huì)在對(duì)象實(shí)例化時(shí)第一個(gè)被調(diào)用,然后才會(huì)調(diào)用 __init__,它們的區(qū)別如下:
__new__的第一個(gè)參數(shù)是cls,而__init__的第一個(gè)參數(shù)是self__new__返回值是一個(gè)實(shí)例對(duì)象,而__init__沒有任何返回值,只做初始化操作__new__由于返回的是一個(gè)實(shí)例對(duì)象,所以它可以給所有實(shí)例進(jìn)行統(tǒng)一的初始化操作
了解了它們之間的區(qū)別,我們來(lái)看 __new__ 在什么場(chǎng)景下使用?
由于 __new__ 優(yōu)先于 __init__ 調(diào)用,而且它返回的是一個(gè)實(shí)例,所以我們可以利用這個(gè)特性,在 __new__ 方法中,每次返回同一個(gè)實(shí)例來(lái)實(shí)現(xiàn)一個(gè)單例類:
# coding: utf8
class Singleton(object):
"""單例"""
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
class MySingleton(Singleton):
pass
a = MySingleton()
b = MySingleton()
assert a is b # True
另外一個(gè)使用場(chǎng)景是,當(dāng)我們需要繼承內(nèi)置類時(shí),例如想要繼承 int、str、tuple,就無(wú)法使用 __init__ 來(lái)初始化了,只能通過 __new__ 來(lái)初始化數(shù)據(jù):
# coding: utf8
class g(float):
"""千克轉(zhuǎn)克"""
def __new__(cls, kg):
return float.__new__(cls, kg * 2)
a = g(50) # 50千克轉(zhuǎn)為克
print a # 100
print a + 100 # 200 由于繼承了float,所以可以直接運(yùn)算,非常方便!
在這個(gè)例子中,我們實(shí)現(xiàn)了一個(gè)類,這個(gè)類繼承了 float,之后,我們就可以對(duì)這個(gè)類的實(shí)例進(jìn)行計(jì)算了,是不是很神奇?
除此之外,__new__ 比較多的應(yīng)用場(chǎng)景是配合「元類」使用,關(guān)于「元類」的原理,我會(huì)在后面的文章中講到。
__del__
__del__ 這個(gè)方法就是我們經(jīng)常說(shuō)的「析構(gòu)方法」,也就是在對(duì)象被垃圾回收時(shí)被調(diào)用。
但是請(qǐng)注意,當(dāng)我們執(zhí)行 del obj 時(shí),這個(gè)方法不一定會(huì)執(zhí)行。
由于 Python 是通過引用計(jì)數(shù)來(lái)進(jìn)行垃圾回收的,如果這個(gè)實(shí)例在執(zhí)行 del 時(shí),還被其他對(duì)象引用,那么就不會(huì)觸發(fā)執(zhí)行 __del__ 方法。
我們來(lái)看一個(gè)例子:
class Person(object):
def __del__(self):
print '__del__'
我們定義了一個(gè)帶有 __del__ 方法的類,此時(shí)我們直接執(zhí)行:
a = Person()
print 'exit'
# Output:
# exit
# __del__
由于我們沒有對(duì)實(shí)例進(jìn)行任何引用操作時(shí),所以 __del__ 在程序退出時(shí)被調(diào)用。
如果我們顯示執(zhí)行 del obj,如下:
a = Person()
del a # 手動(dòng)銷毀對(duì)象
print 'exit'
# Output:
# __del__
# exit
同樣地,由于實(shí)例沒有被其他對(duì)象所引用,當(dāng)我們手動(dòng)銷毀這個(gè)實(shí)例時(shí),__del__ 被調(diào)用后程序正常退出。
如果這個(gè)對(duì)象被其他對(duì)象所引用:
a = Person()
b = a # b引用a
del a # 手動(dòng)銷毀 不觸發(fā)__del__
print 'exit'
# Output:
# exit
# __del__
可以看到,如果這個(gè)實(shí)例有被其他對(duì)象引用,盡管我們手動(dòng)銷毀這個(gè)實(shí)例,但不會(huì)觸發(fā) __del__ 方法,而是在程序正常退出時(shí)被調(diào)用執(zhí)行。
通常來(lái)說(shuō),__del__ 這個(gè)方法我們很少會(huì)使用到,除非需要在顯示執(zhí)行 del 執(zhí)行特殊清理邏輯的場(chǎng)景中才會(huì)使用到。
但另一方面,也給我們一個(gè)提醒,當(dāng)我們?cè)趯?duì)文件、Socket 進(jìn)行操作時(shí),如果要想安全地關(guān)閉和銷毀這些對(duì)象,最好是在 try 異常塊后的 finally 中進(jìn)行關(guān)閉和釋放操作,從而避免資源的泄露。
類的表示
接下來(lái),我們來(lái)看關(guān)于類的表示相關(guān)的魔法方法,主要包括以下幾種:
__str__/__repr____unicode____hash__/__eq____nozero__
__str__/__repr__
關(guān)于 __str__ 和 __repr__ 這 2 個(gè)魔法方法,非常類似,很多人區(qū)分不出它們有什么不同,我們來(lái)看幾個(gè)例子,就能理解這 2 個(gè)方法的效果:
>>> a = 'hello'
>>> str(a)
'hello'
>>> '%s' % a # 調(diào)用__str__
'hello'
>>> repr(a) # 對(duì)象a的標(biāo)準(zhǔn)表示 也就是a是如何創(chuàng)建的
"'hello'"
>>> '%r' % a # 調(diào)用__repr__
"'hello'"
>>> import datetime
>>> b = datetime.datetime.now()
>>> str(b)
'2017-02-22 12:28:40.923379'
>>> print b # 等同于print str(b)
2017-02-22 12:28:40.923379
>>> repr(b) # 展示對(duì)象b的標(biāo)準(zhǔn)創(chuàng)建方式(如何創(chuàng)建的)
'datetime.datetime(2017, 2, 22, 12, 28, 40, 923379)'
>>> b # 等同于print repr(b)
datetime.datetime(2017, 2, 22, 12, 28, 40, 923379)
>>> c = eval(repr(b)) # repr(b)目標(biāo)針對(duì)于機(jī)器 所以可執(zhí)行
>>> c
datetime.datetime(2017, 2, 22, 12, 28, 40, 923379)
從上述例子中我們可以看出這 2 個(gè)方法的區(qū)別:
__str__強(qiáng)調(diào)可讀性,而__repr__強(qiáng)調(diào)準(zhǔn)確性 / 標(biāo)準(zhǔn)性__str__的目標(biāo)人群是用戶,而__repr__的目標(biāo)人群是機(jī)器,__repr__返回的結(jié)果是可執(zhí)行的,通過eval(repr(obj))可以正確運(yùn)行占位符 %s調(diào)用的是__str__,而%r調(diào)用的是__repr__方法
所以,我們?cè)趯?shí)際中開發(fā)中定義類時(shí),一般這樣使用:
# coding: utf8
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
# 格式化 友好對(duì)用戶展示
return 'name: %s, age: %s' % (self.name, self.age)
def __repr__(self):
# 標(biāo)準(zhǔn)化展示
return "Person('%s', %s)" % (self.name, self.age)
person = Person('zhangsan', 20)
# 強(qiáng)調(diào)對(duì)用戶友好
print str(person) # name: zhangsan, age: 20
print '%s' % person # name: zhangsan, age: 20
# 強(qiáng)調(diào)對(duì)機(jī)器友好 結(jié)果 eval 可執(zhí)行
print repr(person) # Person('zhangsan', 20)
print '%r' % person # Person('zhangsan', 20)
明白了它們之間的區(qū)別,我們?cè)偎伎家幌?,如果只定義了 __str__ 或 __repr__ 其中一個(gè),那會(huì)是什么結(jié)果?
只定義 __str__,但沒有定義 __repr__:
# coding: utf8
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return 'name: %s, age: %s' % (self.name, self.age)
person = Person('zhangsan', 20)
print str(person) # name: zhangsan, age: 20
print '%s' % person # name: zhangsan, age: 20
print repr(person) # <__main__.Person object at 0x10bee9390>
print '%r' % person # <__main__.Person object at 0x10bee9390>
只定義 __repr__,但沒有定義 __str__:
# coding: utf8
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return "Person('%s', %s)" % (self.name, self.age)
person = Person('zhangsan', 20)
print str(person) # Person('zhangsan', 20)
print '%s' % person # Person('zhangsan', 20)
print repr(person) # Person('zhangsan', 20)
print '%r' % person # Person('zhangsan', 20)
從例子中我們可以看到結(jié)果:
如果只定義了 _str__,那么repr(person)輸出<__main__.Person object at 0x10bee9390>如果只定義了 __repr__,那么str(person)與repr(person)結(jié)果是相同的
也就是說(shuō),__repr__ 在表示類時(shí),是一級(jí)的,如果只定義它,那么 __str__ = __repr__。
而 __str__ 展示類時(shí)是次級(jí)的,如果沒有定義 __repr__,那么 repr(person) 將會(huì)展示缺省的定義。
__unicode__
如果一個(gè)類定義了 __unicode__ 方法,那么在調(diào)用 unicode(obj) 時(shí),此方法將被調(diào)用,但是其返回值類型是 unicode。
# coding: utf8
class Person(object):
def __unicode__(self):
# 這里不是u'hello'
return 'hello'
person = Person()
print unicode(person) # helllo
print type(unicode(person)) # <type 'unicode'>
從例子中我們可以看到, 雖然我們定義的 __unicode__ 返回值不是 unicode 類型,但在輸出時(shí),程序會(huì)自動(dòng)轉(zhuǎn)換成 unicode 類型。
這個(gè)方法在開發(fā)中一般很少使用,通常我們只需要定義 __str__ 即可。
__hash__/__eq__
__hash__ 方法返回一個(gè)整數(shù),用來(lái)表示實(shí)例對(duì)象的唯一標(biāo)識(shí),配合 __eq__ 方法,可以判斷兩個(gè)對(duì)象是否相等:
# coding: utf8
class Person(object):
def __init__(self, uid):
self.uid = uid
def __repr__(self):
return 'Person(%s)' % self.uid
def __hash__(self):
return self.uid
def __eq__(self, other):
return self.uid == other.uid
p1 = Person(1)
p2 = Person(1)
p1 == p2 # True
p3 = Person(2)
print set([p1, p2, p3]) # 根據(jù)唯一標(biāo)識(shí)去重輸出 set([Person(1), Person(2)])
如果我們需要判斷兩個(gè)對(duì)象是否相等,只需要我們重寫 __hash__ 和 __eq__ 方法就可以了。
此外,當(dāng)我們使用 set 時(shí),在 set 中存放這些對(duì)象,也會(huì)根據(jù)這兩個(gè)方法進(jìn)行去重操作。
__nonzero__
當(dāng)調(diào)用 bool(obj) 時(shí),會(huì)調(diào)用 __nonzero__ 方法,返回 True 或 False:
# coding: utf8
class Person(object):
def __init__(self, uid):
self.uid = uid
def __nonzero__(self):
return self.uid > 10
p1 = Person(1)
p2 = Person(15)
print bool(p1) # False
print bool(p2) # True
在 Python3 中,
__nonzero__被重命名為__bool__。
訪問控制
接下來(lái),我們來(lái)看關(guān)于訪問控制的魔法方法,主要包括以下幾種:
__setattr__:通過「.」設(shè)置屬性或setattr(key, value)設(shè)置屬性時(shí)調(diào)用__getattr__:訪問不存在的屬性時(shí)調(diào)用__delattr__:刪除某個(gè)屬性時(shí)調(diào)用__getattribute__:訪問任意屬性或方法時(shí)調(diào)用
我們來(lái)看使用這些方法的完整例子:
# coding: utf8
class Person(object):
def __setattr__(self, key, value):
"""屬性賦值"""
if key not in ('name', 'age'):
return
if key == 'age' and value < 0:
raise ValueError()
super(Person, self).__setattr__(key, value)
def __getattr__(self, key):
"""訪問某個(gè)不存在的屬性"""
return 'unknown'
def __delattr__(self, key):
"""刪除某個(gè)屬性"""
if key == 'name':
raise AttributeError()
super(Person, self).__delattr__(key)
def __getattribute__(self, key):
"""所有屬性/方法調(diào)用都經(jīng)過這里"""
if key == 'money':
return 100
if key == 'hello':
return self.say
return super(Person, self).__getattribute__(key)
def say(self):
return 'hello'
p1 = Person()
p1.name = 'zhangsan' # 調(diào)用__setattr__
p1.age = 20 # 調(diào)用__setattr__
print p1.name # zhangsan
print p1.age # 20
setattr(p1, 'name', 'lisi') # 調(diào)用__setattr__
setattr(p1, 'age', 30) # 調(diào)用__setattr__
print p1.name # lisi
print p1.age # 30
p1.gender = 'male' # __setattr__中忽略對(duì)gender賦值
print p1.gender # gender不存在 所以會(huì)調(diào)用__getattr__返回unknown
print p1.money # money不存在 在__getattribute__中返回100
print p1.say() # hello
print p1.hello() # hello 調(diào)用__getattribute__ 間接調(diào)用say方法
del p1.name # __delattr__中引發(fā)AttributeError
p2 = Person()
p2.age = -1 # __setattr__中引發(fā)ValueError
我們仔細(xì)看一下這個(gè)例子,我已經(jīng)添加好了詳細(xì)的注釋。
__setattr__
先來(lái)說(shuō) __setattr__,當(dāng)我們?cè)诮o一個(gè)對(duì)象進(jìn)行屬性賦值時(shí),都會(huì)經(jīng)過這個(gè)方法,在這個(gè)例子中,我們只允許對(duì) name 和 age 這 2 個(gè)屬性進(jìn)行賦值,忽略了 gender 屬性,除此之外,我們還對(duì) age 賦值進(jìn)行了校驗(yàn)。
通過 __setattr__ 方法,我們可以非常方便地對(duì)屬性賦值進(jìn)行控制。
__getattr__
再來(lái)看 __getattr__,由于我們?cè)?nbsp;__setattr__ 中忽略了對(duì) gender 屬性的賦值,所以當(dāng)訪問這個(gè)不存在的屬性時(shí),會(huì)調(diào)用 __getattr__ 方法,在這個(gè)方法中返回了默認(rèn)值 unknown。
很多同學(xué)以為這個(gè)方法與 __setattr__ 方法對(duì)等的,一個(gè)是賦值,一個(gè)是獲取。其實(shí)不然,__getattr__ 只有在訪問「不存在的屬性」時(shí)才會(huì)被調(diào)用,這里我們需要注意。
__getattribute__
了解了 __getattr__ 后,還有一個(gè)和它非常類似的方法:__getattribute__。
很多人經(jīng)常把這個(gè)方法和 __getattr__ 混淆,通過例子我們可以看出,它與前者的區(qū)別在于:
__getattr__只有在訪問不存在的屬性時(shí)被調(diào)用,而__getattribute__在訪問任意屬性時(shí)都會(huì)被調(diào)用__getattr__只針對(duì)屬性訪問,而__getattribute__不僅針對(duì)所有屬性訪問,還包括方法調(diào)用
在上面的例子,雖然我們沒有定義 money 屬性和 hello 方法,但是在 __getattribute__ 里攔截到了這個(gè)屬性和方法,就可以對(duì)其執(zhí)行不同的邏輯。
__delattr__
最后,我們來(lái)看 __delattr__,它比較簡(jiǎn)單,當(dāng)刪除對(duì)象的某個(gè)屬性時(shí),這個(gè)方法會(huì)被調(diào)用,所以它一般會(huì)用在刪除屬性前的校驗(yàn)場(chǎng)景中使用。
總結(jié)
這篇文章,我們主要介紹了 Python 中常見的魔法方法,主要有構(gòu)造與初始化、類的表示、訪問控制這 3 個(gè)模塊。
構(gòu)造與初始化的魔法方法,常常用在類的初始化過程中,其中 __init__一般用于實(shí)例初始化, 而 __new__ 可以改變初始化實(shí)例的行為,通過它我們可以實(shí)現(xiàn)一個(gè)單例或者繼承一個(gè)內(nèi)置類。
關(guān)于類的表示的魔法方法,比較常用的,當(dāng)我們想表示一個(gè)類時(shí),可以使用 __str__ 或 __repr__ 方法,當(dāng)需要判斷兩個(gè)對(duì)象是否相等時(shí),可以使用 __hash__ 和 __eq__ 方法。
關(guān)于訪問控制的魔法方法,它可以控制實(shí)例的屬性賦值、屬性訪問、方法訪問、屬性刪除等操作,這對(duì)于我們實(shí)現(xiàn)一個(gè)復(fù)雜功能的類有很大幫助。
在下一篇文章,我們會(huì)繼續(xù)分析剩下的魔法方法,主要包括關(guān)于比較操作、容器類操作、可調(diào)用對(duì)象、序列化相關(guān)的魔法方法。
更多閱讀
特別推薦

點(diǎn)擊下方閱讀原文加入社區(qū)會(huì)員
