用Descriptor实现另一种风格的Decorator

标签:Python

之前我曾写过《在Python中实现Decorator模式》,里面展示的例子都是一个函数封装另一个函数,然后返回封装后的函数,即:
def decorator(func):
	def wrapper(arg):
		#做另一些事
		return func(arg)
	return wrapper

@decorator
def f(param):
	#做一些事
虽然这就是正统的写法,但对于不太了解闭包的人来说,那个wrapper太难懂了,就会有种Decorator很恐怖的印象。

今天在Aiur(也是个玩星际的啊~)的博客看到篇《Decorator的另一种实现方式》,才知道原来还可以这样写:
def decorator(func, arg):
	#做另一些事
	return func(arg)

@decorator.__get__
def f(param):
	#做一些事
这样一来,封装器很容易弄懂了,但封装器的__get__是个什么东西呢?

这就涉及到Python的另一个语法知识:Descriptor了。它一般被翻译成描述符,不过看这个名字仍然不知所谓,于是就深入了解一下吧。

原来Python在使用obj.attr这种方式来访问对象的属性时,会有几种策略,最常见的便是__dict__,而__get__也是其中之一。
它的用意是让属性的访问变成方法的调用,虽然在外部的调用者看来是一样的,但是内部的实现却可以由自己来决定了。
简单来说,当一个类C的类属性attr是一个实现了__get__方法的对象时,那么获取C.attr以及它的实例obj的属性obj.attr时,都会调用attr的__get__方法来获取。而这个attr就叫做descriptor。

下面就来看一个简单的例子,每次访问对象的属性时都自动加1:
class Counter(object):
	def __init__(self, value=0):
		self.value = value

	def __get__(self, instance, owner):
		self.value += 1
		return self.value

class A(object):
	value = Counter()

a = A()
print a.value
print a.value
print A.value
结果是1、2和3。它究竟做了什么事呢?
在定义A类时,它的内部变量value(类属性)被赋值为Counter类的实例了。接着我们生成了一个A类的实例a,并访问a的value属性。
很明显,a并没有value属性,于是变成了访问A的value属性。但神奇的是,这次访问实际上变成了Counter.__get__(A的value属性, a, A)。
而更神奇的是,A有value属性,但返回的不是该属性,而是Counter.__get__(A的value属性, None, A)。
这个Counter类的方法神不知鬼不觉地就插入了A类,有没有点元编程的味道~

这里要注意,value必须是A的类属性,且定义了__get__方法。一般的对象是没有这种效果的,且如果对象定义了同名属性,那么就不会调用__get__方法。
同时还注意到,此处我们是对一个类属性的访问进行计数统计,而不是针对某个对象。

接着再来看看Python的动态性:
class Counter(object):
	def __init__(self, value=0):
		self.value = value

	def __get__(self, instance, owner):
		self.value += 1
		return self.value

class A(object):
	pass

class B(object):
	value = Counter()

a = A()
A.value = Counter()
print a.value
print a.value
a.__class__ = B
print a.value
print a.value
B.value = Counter()
print a.value
结果是1、2、1、2、1。
我们既可以改变对象的类来更改计数器,还可以直接替换计数器。类、对象和属性之间的关系有没有把你弄糊涂呢~

还是回归正题吧。实际上除了__get__,还有__set__和__delete__方法,看名字就很容易懂了。
实现了这些方法的类,就称为descriptor类了;而这些类的实例就称为descriptor对象。
如果只实现了__get__,那么就称为non-data descriptor;而同时还实现了__set__或__delete__的话,就称为data descriptor了。它们的区别在于:non-data descriptor的优先级低于__dict__,而data descriptor的优先级高于__dict__。

值得一提的是,Python里一切都是对象,函数也不例外,并且函数的类也实现了__get__方法,因此函数也是个descriptor对象:
>>> def f():pass
...
>>> type(f) # 实际上就是types.FunctionType
<type 'function'>
>>> type(f).__get__
<slot wrapper '__get__' of 'function' objects>
那么函数类实现__get__有什么用呢?

还记得实例方法的定义和调用吧:
class C(object):
	def hello(self):
		print 'hello'

o = C()
o.hello()
C.hello(o)
很多人不明白为什么实例方法的第一个参数必须写上,而不能像C++那样隐式地设为this;原因就在于实例方法也可以被类调用。
或许你已经注意到了,这个实例方法实际上就是一个类的内部函数,那为什么用实例调用时不需要传递参数呢?
这就是__get__的作用了:当访问o.hello时,o并没有定义这个属性,于是转向了查询C的hello属性。这个属性是类属性,而且又是descriptor对象,因此就变成了这样的调用:函数类.__get__(C的hello属性, o, C)。
现在,函数hello、对象o和类C就被绑定在一起了,这便被称为method对象。于是通过这神奇的语法糖,函数就变成方法了,设计Python的人真是天才啊…
最后,当调用method对象时,它会去调用真正的类的函数,即类似这种形式:C.hello(o)。

此外别忘了,Python是支持缺省参数和不定参数的。无论传什么参数,函数类的__get__只需要返回一个method就行了。
于是再看看这段代码:
@decorator.__get__
def f(param):
	#做一些事
它实际上就是:
def f(param):
	#做一些事
f = decorator.__get__(f)
而decorator.__get__(f)是一个method,它将f作为参数,传给了decorator函数,于是f(arg)这种调用,最终的变成了:decorator(f(arg))。这就和一般的Decorator效果一样了。
如果你对此存有怀疑的话,不妨试试输出type(f),结果会是:<type 'instancemethod'>。

最后,如果你还想深入了解的话,可以看看《Python的descriptor》这篇文章。英文不错的话,还可以看看《How-To Guide for Descriptors》

0条评论 你不来一发么↓

    想说点什么呢?