在Python中,屬性查詢(attribute lookup)是比較複雜的,特別是涉及到描述符descriptor的時候。
在上一文章末尾,給出了一段程式碼,就涉及到descriptor與attribute lookup的問題。而get系列函式(__get__, __getattr__, __getattribute__) 也很容易搞暈,本文就這些問題簡單總結一下。
首先,我們知道:
- python中一切都是物件,“everything is object”,包括類,類的例項,數字,模組
- 任何object都是類(class or type)的例項(instance)
- 如果一個descriptor只實現了__get__方法,我們稱之為non-data descriptor, 如果同時實現了__get__ __set__我們稱之為data descriptor。
例項屬性查詢
按照python doc,如果obj是某個類的例項,那麼obj.name(以及等價的getattr(obj,’name’))首先呼叫__getattribute__。如果類定義了__getattr__方法,那麼在__getattribute__丟擲 AttributeError 的時候就會呼叫到__getattr__,而對於描述符(__get__)的呼叫,則是發生在__getattribute__內部的。官網文件是這麼描述的
The implementation works through a precedence chain that gives data descriptors priority over instance variables, instance variables priority over non-data descriptors, and assigns lowest priority to
__getattr__()
if provided.
obj = Clz(), 那麼obj.attr 順序如下:
(1)如果“attr”是出現在Clz或其基類的__dict__中, 且attr是data descriptor, 那麼呼叫其__get__方法, 否則
(2)如果“attr”出現在obj的__dict__中, 那麼直接返回 obj.__dict__[‘attr’], 否則
(3)如果“attr”出現在Clz或其基類的__dict__中
(3.1)如果attr是non-data descriptor,那麼呼叫其__get__方法, 否則
(3.2)返回 __dict__[‘attr’]
(4)如果Clz有__getattr__方法,呼叫__getattr__方法,否則
(5)丟擲AttributeError
下面是測試程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#coding=utf-8 class DataDescriptor(object): def __init__(self, init_value): self.value = init_value def __get__(self, instance, typ): return 'DataDescriptor __get__' def __set__(self, instance, value): print ('DataDescriptor __set__') self.value = value class NonDataDescriptor(object): def __init__(self, init_value): self.value = init_value def __get__(self, instance, typ): return('NonDataDescriptor __get__') class Base(object): dd_base = DataDescriptor(0) ndd_base = NonDataDescriptor(0) class Derive(Base): dd_derive = DataDescriptor(0) ndd_derive = NonDataDescriptor(0) same_name_attr = 'attr in class' def __init__(self): self.not_des_attr = 'I am not descriptor attr' self.same_name_attr = 'attr in object' def __getattr__(self, key): return '__getattr__ with key %s' % key def change_attr(self): self.__dict__['dd_base'] = 'dd_base now in object dict ' self.__dict__['ndd_derive'] = 'ndd_derive now in object dict ' def main(): b = Base() d = Derive() print 'Derive object dict', d.__dict__ assert d.dd_base == "DataDescriptor __get__" assert d.ndd_derive == 'NonDataDescriptor __get__' assert d.not_des_attr == 'I am not descriptor attr' assert d.no_exists_key == '__getattr__ with key no_exists_key' assert d.same_name_attr == 'attr in object' d.change_attr() print 'Derive object dict', d.__dict__ assert d.dd_base != 'dd_base now in object dict ' assert d.ndd_derive == 'ndd_derive now in object dict ' try: b.no_exists_key except Exception, e: assert isinstance(e, AttributeError) if __name__ == '__main__': main() |
注意第50行,change_attr給例項的__dict__裡面增加了兩個屬性。通過上下兩條print的輸出如下:
Derive object dict {‘same_name_attr’: ‘attr in object’, ‘not_des_attr’: ‘I am not descriptor attr’}
Derive object dict {‘same_name_attr’: ‘attr in object’, ‘ndd_derive’: ‘ndd_derive now in object dict ‘, ‘not_des_attr’: ‘I am not descriptor attr’, ‘dd_base’: ‘dd_base now in object dict ‘}
呼叫change_attr方法之後,dd_base既出現在類的__dict__(作為data descriptor), 也出現在例項的__dict__, 因為attribute lookup的循序,所以優先返回的還是Clz.__dict__[‘dd_base’]。而ndd_base雖然出現在類的__dict__, 但是因為是nondata descriptor,所以優先返回obj.__dict__[‘dd_base’]。其他:line48,line56表明了__getattr__的作用。line49表明obj.__dict__優先於Clz.__dict__
cached_property例子
我們再來看看上一文章的這段程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import functools, time class cached_property(object): """ A property that is only computed once per instance and then replaces itself with an ordinary attribute. Deleting the attribute resets the property. """ def __init__(self, func): functools.update_wrapper(self, func) self.func = func def __get__(self, obj, cls): if obj is None: return self value = obj.__dict__[self.func.__name__] = self.func(obj) return value class TestClz(object): @cached_property def complex_calc(self): print 'very complex_calc' return sum(range(100)) if __name__=='__main__': t = TestClz() print '>>> first call' print t.complex_calc print '>>> second call' print t.complex_calc |
cached_property是一個non-data descriptor。在TestClz中,用cached_property裝飾方法complex_calc,返回值是一個descriptor例項,所以在呼叫的時候沒有使用小括號。
第一次呼叫t.complex_calc之前,obj(t)的__dict__中沒有”complex_calc“, 根據查詢順序第三條,執行cached_property.__get__, 這個函式代用快取的complex_calc函式計算出結果,並且把結果放入obj.__dict__。那麼第二次訪問t.complex_calc的時候,根據查詢順序,第二條有限於第三條,所以就直接返回obj.__dict__[‘complex_calc’]。bottle的原始碼中還有兩個descriptor,非常厲害!
類屬性查詢
前面提到過,類的也是物件,類是元類(metaclass)的例項,所以類屬性的查詢順序基本同上。區別在於第二步,由於Clz可能有基類,所以是在Clz及其基類的__dict__”查詢“attr,注意這裡的查詢並不是直接返回clz.__dict__[‘attr’]。具體來說,這第二步分為以下兩種情況:
(2.1)如果clz.__dict__[‘attr’]是一個descriptor(不管是data descriptor還是non-data descriptor),都呼叫其__get__方法
(2.2)否則返回clz.__dict__[‘attr’]
這就解釋了一個很有意思的問題:method與function的問題
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>>> class Widget(object): ... def func(self): ... pass ... >>> w = Widget() >>> Widget.__dict__ dict_proxy({'__dict__': <attribute '__dict__' of 'Widget' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Widget' objects>, '__doc__': None, 'func': <function func at 0x7fdc7d0d1668>}) >>> w.__dict__ {} >>> Widget.__dict__['func'] <function func at 0x7fdc7d0d1668> >>> Widget.func <unbound method Widget.func> >>> |
Widget是一個之定義了一個func函式的類,func是類的屬性,這個也可以通過Widget.__dict__、w.__dict__看到。Widget.__dict__[‘func’]返回的是一個function,但Widget.func是一個unbound method,即Widget.func並不等同於Widget.__dict__[‘func’],按照前面的類屬性的訪問順序,我們可以懷疑,func是一個descriptor,這樣才不會走到第2.2這種情況。驗證如下:
1 2 |
>>> dir(Widget.__dict__['func']) ['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name'] |
屬性賦值
Python的屬性賦值(attribute assignment)也會受到descriptor(data descriptor)的影響,同時也會受到__setattr__函式的影響。當然Python中還有一個setattr,setattr(x, ‘foobar’, 123)等價於x.foobar = 123,二者都叫attribute assignment。
首先看看__setattr__:
object.__setattr__(self, name, value)
Called when an attribute assignment is attempted. This is called instead of the normal mechanism
那什麼是normal mechanism,簡單來說就是x.__dict__[‘foobar’] = 123,不管’foobar’之前是否是x的屬性(當然賦值之後就一定是了)。但是如果‘’foobar‘’是類屬性,且是data descriptor,那麼回優先呼叫__set__。我們來看一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MaxValDes(object): def __init__(self, attr, max_val): self.attr = attr self.max_val = max_val def __get__(self, instance, typ): return instance.__dict__[self.attr] def __set__(self, instance, value): instance.__dict__[self.attr] = min(self.max_val, value) print 'MaxValDes __set__', self.attr, instance.__dict__[self.attr] class Widget(object): a = MaxValDes('a', 10) def __init__(self): self.a = 0 # def __setattr__(self, name, value): # self.__dict__[name] = value # print 'Widget __setattr__', name, self.__dict__[name] if __name__ == '__main__': w0 = Widget() w0.a = 123 |
輸出如下:
1 2 |
MaxValDes __set__ a 0 MaxValDes __set__ a 10 |
可以看到,即使Widget的例項也有一個‘a’屬性,但是呼叫w.a的時候會呼叫類屬性‘a’(一個descriptor)的__set__方法。如果不註釋掉第18到第20行,輸出如下
1 2 |
Widget __setattr__ a 0 Widget __setattr__ a 123 |
可以看到,優先呼叫Widget 的__setattr__方法。因此:對於屬性賦值,obj = Clz(), 那麼obj.attr = var,按照這樣的順序:
- 如果Clz定義了__setattr__方法,那麼呼叫該方法,否則
- 如果“attr”是出現在Clz或其基類的__dict__中, 且attr是data descriptor, 那麼呼叫其__set__方法, 否則
- 等價呼叫obj.__dict__[‘attr’] = var
references
- Descriptor HowTo Guide, https://docs.python.org/2/howto/descriptor.html#descriptor-protocol
- Object attribute lookup in Python, http://www.betterprogramming.com/object-attribute-lookup-in-python.html
- python __set__ __get__ 等解釋, http://blog.csdn.net/huithe/article/details/7484606