最近看Flask源碼時(shí)發(fā)現(xiàn)很多不熟悉的語法,其中一個(gè)就是描述符,在config.py中出現(xiàn),描述符的用處很多,是Python中很多特性的底層機(jī)制,如properties, methods, static methods, class methods和super()。
什么是描述符
描述符一般是一個(gè)有綁定動(dòng)作的屬性對象,這個(gè)屬性的獲取、賦值、刪除操作和途徑被描述符協(xié)議重寫。對象屬性的正常獲取順序是這樣的,比如想要獲取a.x,那么首先查找a.__dict__['x'],如果找不到則查看type(a).__dict__['x'],如果還沒有則查看父類的__dict__。
Python中有很多協(xié)議,比如迭代對象的迭代器協(xié)議,上下文管理協(xié)議等,都是靠重寫類中以__開頭和結(jié)尾的魔法方法來實(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)這些方法后,對屬性進(jìn)行操作時(shí)就不走正常途徑,而是調(diào)用這幾個(gè)魔法方法。需要注意的是,描述符必須是一個(gè)新式類。
為什么需要描述符
寫過的Java的應(yīng)該有一些印象,類里的屬性一般是private的,如果想要拿到這個(gè)屬性,一般是通過一個(gè)public的get_xxx方法來獲得屬性,重新賦值時(shí)也是一個(gè)道理。但是這樣雖然隱藏了屬性,但是后續(xù)寫代碼時(shí)都得用object.get_xxx()來獲取值,而不是object.attr,顯然第二種方式更簡單,更美觀,所以Python這種簡潔的語言就提供了這樣的更簡潔的實(shí)現(xiàn)方式---描述符協(xié)議。
還有一種情況,假設(shè)有一個(gè)Person類,它有一個(gè)age屬性,那么在對年齡賦值時(shí)是有一些限制的,比如必須是整數(shù),必須大于0。所以應(yīng)該在賦值時(shí)進(jìn)行檢查,這么一看好像賦值時(shí)又需要通過方法xiaoming.age=cls.examine_age(1000),又不美觀了,而描述符協(xié)議可以在背地里幫我們做這種檢查,而我們還是可以使用xiaoming.age=1000這個(gè)更簡潔的語句。
這里有個(gè)我之前一直困惑的地方提一下,可能你們覺得不難,但是確實(shí)干擾了我很久。那就是這三個(gè)魔法方法定義在什么地方,還是回到上面那個(gè)例子,好像只有Person是一個(gè)類,所以我之前一直覺得應(yīng)該定義在Person類中,但其實(shí)不是。魔法方法應(yīng)該定義在一個(gè)Age類中,然后age屬性是一個(gè)Age對象實(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è)對象,在這里就是xiaoming,owner是獲取屬性的對象的類,在這里就是Person。
描述符的調(diào)用機(jī)制
上面提到了非描述符屬性的獲取途徑,定義了描述符協(xié)議后,obj.b的操作將調(diào)用b.__get__(obj)這個(gè)方法來獲取屬性。描述符的調(diào)用機(jī)制根據(jù)調(diào)用對象是對象還是類有一些區(qū)別。
描述符是通過type.__getattribute__()方法被調(diào)用,這也是為什么描述符必須是在新式類中的原因,繼承自object的類被稱為新式類,否則沒有這個(gè)方法,則無法調(diào)用描述符的方法。
對于對象來說,object.__getattribute__()會(huì)將b.x 轉(zhuǎn)換為 type(b).__dict__['x'].__get__(b, type(b))。這個(gè)轉(zhuǎn)換通過下面這樣的一個(gè)優(yōu)先鏈:data descriptors大于實(shí)例變量,實(shí)例變量大于 non-data descriptors,如果存在__getattr__(),則__getattr__()優(yōu)先級最低。完整的C實(shí)現(xiàn)在PyObject_GenericGetAttr() in Objects/object.c.
對于類來說,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è)最簡單的描述符實(shí)例,就是對屬性進(jìn)行取值或者賦值時(shí)進(jìn)行額外的操作,同時(shí)保持代碼的簡潔。描述符在Python語言中本來也有很多的應(yīng)用,但是能力不夠,不能很好的理解其中的奧妙,就不誤導(dǎo)大家了。主要是Property,Function and method和static method and class method這幾個(gè)方面,給出鏈接,有興趣的可以鉆研一下。