个人认为python中描述符协议是一个比较神奇的存在,是因为如果你不去了解property的内部实现,没有深究别人写的__set__、__get__方法。或许写python很多年都对这个东西没什么了解。然而这玩意儿有时候真的挺好用的。本文不会详述描述符协议。会着重讲一个小例子。如果以前没有接触过描述符,请依次查看文末相关资料的两篇文章

适用范围

描述符协议都是针对对象属性的访问。先要明白我们不会去针对一个全局的def使用property进行装饰。我们一般都是在类里面使用。可以对类的访问使用描述符(比较少用),更常用的是针对类实例的访问使用描述符协议

资料描述符和非资料描述符的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RevealAccess(object):
def __get__(self, obj, objtype):
try:
return self.val
except:
return 'error'
# def __set__(self, obj, val):
# self.val = val
class MyClass(object):
x = RevealAccess()
m = MyClass()
print(m.x)
m.x = 20
print(m.__dict__)
m.__dict__['x'] = 1
print(m.__dict__)
print(m.x)
print(m.__class__ is type(m))

首先,当对属性x进行访问的时候不是直接返回描述符对象,而是按照描述符规则执行了描述符对象的__get__等方法!资料描述符就是同时实现了__get__和__set__,区别就是是资料描述符的时候就按照资料描述符的__get__、__set__来。非资料描述符的时候那就先访问instance.__dict__['x'],没有就在按照非资料描述符的__get__来。上面的例子先注释掉__set__就是非资料描述符,对实例属性进行访问的时候先访问了instance.__dict__没有就使用了描述符对象的__get__方法。当为资料描述符的时候纵然对instance.__dict__设置了。依然会调用描述符对象。

示例应用:property加强版,增加缓存(最简代码)

首先来一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo(object):
@property
def jammy(self,_cache={}):
if 'result' in _cache:
return _cache['result']
print('jammy called')
result = 1
_cache.update({'result':result})
return result
f = Foo()
print(f.jammy)
print(f.jammy)

该方法使用python函数的默认参数只初始化一次对结果进行缓存。缺点比较明显。1.无法复用。2.对原函数进行了修改

下面看pyramid的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class reify(object):
def __init__(self, func):
self.func = func
def __get__(self, obj, cls):
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
class Foo(object):
@reify
def jammy(self):
print('jammy called')
return 1
f = Foo()
print(f.jammy)
print(f.jammy)

使用的是非资料描述符,第一次对属性进行访问的时候,因为f.__dict__是没有jammy的。故而访问了描述符,在描述符__get__里面将结果加入到了f.__dict__里面。后面访问就没__get__什么事儿了。实现了对结果的缓存

再看werkzeug的实现

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
class cached_property(property):
def __init__(self, func, name=None, doc=None):
self.__name__ = func.__name__
self.func = func
def __set__(self, obj, value):
obj.__dict__[self.__name__] = value
def __get__(self, obj, type=None):
value = obj.__dict__.get(self.__name__)
if value is None:
value = self.func(obj)
obj.__dict__[self.__name__] = value
return value
class Foo(object):
@cached_property
def jammy(self):
print('jammy called')
return 1
f = Foo()
print(f.jammy)
print(f.jammy)

可以看到和资料描述符的基本一样。可以看到__set__基本没什么用,仅仅只是表面了这是一个资料描述符。而且同样的,为了方便也一样把结果存储到了f.__dict__里面

__getattr__、__getattribute__

虽然都有get,可是区别是很大的。
描述符是控制对象某个属性的访问(所以看到描述符对象一般主要用__get____get__)。
__getattr__控制属性不存在的时候该咋办
__getattribute__和上面相反,默认行为,从__dict__中找到属性值返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo(object):
def __getattr__(self, attr):
print "looking up", attr
value = 42
self.__dict__[attr] = value
return value
f = Foo()
print f.x
print f.x
#output >>> looking up x 42
#output >>> 42
f.x = 3
print f.x
#output >>> 3

再加一个类似的例子吧O_o(https://github.com/faif/python-patterns/blob/master/lazy_evaluation.py)

相关资料

python官方文档描述符指南(译)
python描述符解密
Difference between __getattr__ vs __getattribute__