最近看Flask源碼時(shí)發(fā)現(xiàn)很多不熟悉的語(yǔ)法舱权,其中一個(gè)就是描述符镐确,在config.py中出現(xiàn),描述符的用處很多譬圣,是Python中很多特性的底層機(jī)制,如properties
, methods
, static methods
, class methods
和super()
雄坪。
什么是描述符
描述符一般是一個(gè)有綁定動(dòng)作的屬性對(duì)象厘熟,這個(gè)屬性的獲取、賦值维哈、刪除操作和途徑被描述符協(xié)議重寫绳姨。對(duì)象屬性的正常獲取順序是這樣的,比如想要獲取a.x
,那么首先查找a.__dict__['x']
,如果找不到則查看type(a).__dict__['x']
,如果還沒有則查看父類的__dict__
阔挠。
Python中有很多協(xié)議飘庄,比如迭代對(duì)象的迭代器協(xié)議,上下文管理協(xié)議等购撼,都是靠重寫類中以__
開頭和結(jié)尾的魔法方法來(lái)實(shí)現(xiàn)的跪削。描述符協(xié)議也不例外,只要實(shí)現(xiàn)了__get__(self, instance, owner)
迂求、__set__(self, instance, value)
碾盐、__delete__(self, instance)
中任意一個(gè)或全部的方法,這個(gè)類就變成了一個(gè)描述符揩局。如果只定義了__get__
,則這是一個(gè)non-data descriptor
毫玖,定義了__get__
和__set__
兩個(gè)方法的是data descriptor
,這里的區(qū)別,后面會(huì)提到付枫。實(shí)現(xiàn)這些方法后烹玉,對(duì)屬性進(jìn)行操作時(shí)就不走正常途徑,而是調(diào)用這幾個(gè)魔法方法励背。需要注意的是春霍,描述符必須是一個(gè)新式類。
為什么需要描述符
寫過(guò)的Java的應(yīng)該有一些印象叶眉,類里的屬性一般是private
的,如果想要拿到這個(gè)屬性芹枷,一般是通過(guò)一個(gè)public
的get_xxx
方法來(lái)獲得屬性衅疙,重新賦值時(shí)也是一個(gè)道理。但是這樣雖然隱藏了屬性鸳慈,但是后續(xù)寫代碼時(shí)都得用object.get_xxx()
來(lái)獲取值饱溢,而不是object.attr
,顯然第二種方式更簡(jiǎn)單,更美觀走芋,所以Python這種簡(jiǎn)潔的語(yǔ)言就提供了這樣的更簡(jiǎn)潔的實(shí)現(xiàn)方式---描述符協(xié)議绩郎。
還有一種情況,假設(shè)有一個(gè)Person
類翁逞,它有一個(gè)age
屬性肋杖,那么在對(duì)年齡賦值時(shí)是有一些限制的,比如必須是整數(shù)挖函,必須大于0状植。所以應(yīng)該在賦值時(shí)進(jìn)行檢查,這么一看好像賦值時(shí)又需要通過(guò)方法xiaoming.age=cls.examine_age(1000)
,又不美觀了怨喘,而描述符協(xié)議可以在背地里幫我們做這種檢查津畸,而我們還是可以使用xiaoming.age=1000
這個(gè)更簡(jiǎn)潔的語(yǔ)句。
這里有個(gè)我之前一直困惑的地方提一下必怜,可能你們覺得不難肉拓,但是確實(shí)干擾了我很久。那就是這三個(gè)魔法方法定義在什么地方梳庆,還是回到上面那個(gè)例子暖途,好像只有Person
是一個(gè)類,所以我之前一直覺得應(yīng)該定義在Person
類中靠益,但其實(shí)不是丧肴。魔法方法應(yīng)該定義在一個(gè)Age
類中,然后age
屬性是一個(gè)Age
對(duì)象實(shí)例胧后。
class Age(object):
def __init__(self, age):
self.age = age
def __get__(self, instance, owner):
print('instance={}, owner={}'.format(instance, owner))
return self.age
def __set__(self, instance, value):
print('instance={}, value={}'.format(instance, value))
if value < 0:
raise AttributeError('age should > 0')
self.age = value
class Person(object):
age = Age(100)
xiaoming = Person()
xiaoming.age = 10
print(xiaoming)
print(xiaoming.age)
# output::::::::
# >>> instance=<__main__.Person object at 0x107a24310>, value=10
# >>> <__main__.Person object at 0x107a24310>
# >>> instance=<__main__.Person object at 0x107a24310>, owner=<class '__main__.Person'>
# >>> 10
方法中的instance
屬性返回的是獲取屬性的那個(gè)對(duì)象芋浮,在這里就是xiaoming
,owner
是獲取屬性的對(duì)象的類,在這里就是Person
纸巷。
描述符的調(diào)用機(jī)制
上面提到了非描述符屬性的獲取途徑镇草,定義了描述符協(xié)議后,obj.b
的操作將調(diào)用b.__get__(obj)
這個(gè)方法來(lái)獲取屬性瘤旨。描述符的調(diào)用機(jī)制根據(jù)調(diào)用對(duì)象是對(duì)象還是類有一些區(qū)別梯啤。
描述符是通過(guò)type.__getattribute__()
方法被調(diào)用,這也是為什么描述符必須是在新式類中的原因存哲,繼承自object
的類被稱為新式類因宇,否則沒有這個(gè)方法,則無(wú)法調(diào)用描述符的方法祟偷。
對(duì)于對(duì)象來(lái)說(shuō)察滑,object.__getattribute__()
會(huì)將b.x
轉(zhuǎn)換為 type(b).__dict__['x'].__get__(b, type(b))
。這個(gè)轉(zhuǎn)換通過(guò)下面這樣的一個(gè)優(yōu)先鏈:data descriptors
大于實(shí)例變量修肠,實(shí)例變量大于 non-data descriptors
贺辰,如果存在__getattr__()
,則__getattr__()
優(yōu)先級(jí)最低嵌施。完整的C實(shí)現(xiàn)在PyObject_GenericGetAttr()
in Objects/object.c.
對(duì)于類來(lái)說(shuō)饲化,object.__getattribute__()
會(huì)將 B.x
轉(zhuǎn)換為B.__dict__['x'].__get__(None, B)
。Python實(shí)現(xiàn)如下:
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
描述符實(shí)例
上面提到了一個(gè)最簡(jiǎn)單的描述符實(shí)例吗伤,就是對(duì)屬性進(jìn)行取值或者賦值時(shí)進(jìn)行額外的操作吃靠,同時(shí)保持代碼的簡(jiǎn)潔。描述符在Python語(yǔ)言中本來(lái)也有很多的應(yīng)用牲芋,但是能力不夠撩笆,不能很好的理解其中的奧妙,就不誤導(dǎo)大家了缸浦。主要是Property
,Function and method
和static method and class method
這幾個(gè)方面夕冲,給出鏈接,有興趣的可以鉆研一下裂逐。