最近在做有关Django有关的项目的时候,本人看到源代码里有大量修饰器(decorator)的身影,尤其是一种基于类及类的特殊方法的修饰器吸引了我的注意,遂研究并记录其中的一些关键点于此。
最简单的修饰器
先看一个最简单的python修饰器的例子:
python
def MyFirstDecorator(func):
def wrapper(*arg, **kwargs):
print("before calling func")
func(*arg, **kwargs)
print("after calling func")
return wrapper;
@MyFirstDecorator
def MyFunc():
print('Hello World')
MyFunc()
以上代码的输出结果如下:
go
before calling func
Hello World
after calling func
相信在其他编程语言中接触过函数柯里化概念的同学已经看出了其中的端倪。我们可以将上面的代码理解为如下的操作:
python
def MyFirstDecorator(func):
def wrapper(*arg, **kwargs):
print("before calling func")
func(*arg, **kwargs)
print("after calling func")
return wrapper;
def MyFunc():
print('Hello World')
# 修饰器的真面目:函数柯里化!!!
MyFunc = MyFirstDecorator(MyFunc)
MyFunc();
理解了这点之后,我们还可以结合函数柯里化的思想,实现定制修饰器。譬如下面这个例子:
python
def createDecorator(name):
def decorator(func):
def wrapper(*arg, **kwargs):
print('hello, %s' % name)
print("before calling func")
func(*arg, **kwargs)
print("after calling func")
return wrapper
return decorator
@createDecorator("JiangNanGame")
def MyFunc():
print('test MyFunc')
MyFunc();
输出结果:
go
hello, JiangNanGame
before calling func
test MyFunc
after calling func
基于类的修饰器
下面来看一个我在实际项目源码中遇到的例子。这里为了方便讲解,我已对原先的代码进行了高度提炼和精简。
python
class my_property:
def __get__(self, obj, cls):
return self.func(obj)
def __call__(self, func, *args, **kwargs):
self.func = func
return self
class SysOptions:
@my_property()
def website_base_url(cls):
return 'http://www.jiangnangame.com'
# 输出:http://www.jiangnangame.com
print(SysOptions.website_base_url)
这个输出结果乍一看是十分令人困惑的,为什么直接访问SysOptions.website_base_url就可以得到经过修饰器修饰的website_base_url函数的返回值?
除此之外还令人费解的是,前文中的修饰器都是以单个函数的形式出现的,而在这个例子中,名为my_property的类怎么就发挥起修饰器的作用了?
类的特殊方法
为了搞清楚这段代码的原理,首先必须明白在my_property类中定义的两个特殊成员__get__和__call__的作用。这里通过简单的例子来理解。
get
先来看__get__。这里直接上一个取自于Python官方文档的例子:
python
# __get__定义于描述器中,这里的Ten就是一个描述器
class Ten:
def __get__(self, obj, cls):
print(self)
print(obj)
print(cls)
return 10
class A:
x = 5
# 只有将描述器实例化后存储在另一个类中,它才会发挥作用
y = Ten()
a = A()
# 输出:5。可见含有描述器实例的类在实例化后,不会影响实例对象的其他属性
print(a.x)
# 输出:
# <__main__.Ten object at 0x0000021F8724B5E0>
# <__main__.A object at 0x0000021F872831C0>
# <class '__main__.A'>
# 10
print(a.y)
可见当访问某个类(上例中的A)的实例化对象(上例中的a)中的值为某个描述器实例的属性(上例中的y),就会触发描述器的__get__方法,实现对取值操作的劫持。
同时__get__方法中的三个参数的意义也昭然若揭了。self代表描述器实例自身,obj表示描述器实例所处类的实例对象,cls表示描述器实例所处的类。
call
__call__理解起来就更简单。它可以使得类的实例对象如同函数一般被调用:
python
class MyClass:
def __call__(self, someResult):
print('hook call!')
return someResult
myObj = MyClass()
# 输出:
# hook call!
# 2023
print(myObj(2023))
解决开始的问题
搞明白了前面的两个特殊方法的作用,我们回过头来分析一下起先的代码。
首先第11行@my_property()
实际上是对类my_property进行了一次实例化,得到了一个my_property实例对象。而由于类my_property上设置了__call__方法,因此其实例对象也可以如同函数一般进行调用,故而可以充当修饰器。
接下来,python解释器在发现了11行调用修饰器的命令后,会将原函数website_base_url传入修饰器(即前述的my_property实例对象)以计算出经修饰器包装后的函数。这个过程便会触发修饰器的__call__方法,在第7,8行中可以看到该方法被触发后会将原函数储存为修饰器对象自己的一个属性func,并且将修饰器自身返回作为"包装后的新函数"。
至此为止,SysOptions.website_base_url
真实指向的便是前述的my_property对象了。而该对象拥有一个__get__方法,且其作为类SysOptions的一个属性存在,符合前文提到的__get__方法被触发的条件。因此当第16行对SysOptions.website_base_url
的值进行访问时,get 方法被触发,其调用在修饰器包装原函数时,储存到修饰器对象中的原函数func,并返回其返回值(字符串"JiangNanGame"),实现了对访问值操作的劫持全过程。
尾声
在本文中重点分析的基于类的,针对其他类方法的修饰器my_property是有一定实际意义的。本文中展示的的精简代码虽然只实现了对取值操作的劫持,但只要对该修饰器进行适当的拓展,我们就可以很方便地对类中的方法进行劫持,使其转变为可以直接读写的类属性,大大简化了代码的编写和维护工作。