理解python装饰器

󰃭 2017-03-09

理解python装饰器

装饰器(Derorators),正如其字面所描述,在原函数的基础上进行一些额外的操作。接下来我们将从源头出发,探探python的装饰器。

从函数开始

在日常的工作中,我们需要写形形色色的业务函数,如example 1

#example 1
def deal_business(arg):
    print("Dealing Business in {}".format(arg))
	
def main():
    # in block 1
	deal_business("block 1")
	
	# in block 2
	deal_business("block 2")
	
if __name__ == "__main__":
    main()

在某一天,你需要去统计在各个block中业务处理时间,比较朴素的方案如example 2:

#example 2
import time

def deal_business(arg):
    print("Dealing Business in {}".format(arg))
	
def main():
    # in block 1
	time_start_1 = time.clock()
	deal_business("block 1")
	time_end_1 = time.clock()
	print("time for block 1")
	
	# in block 2
	time_start_2 = time.clock()
	deal_business("block 2")
	time_end_2 = time.clock()
	print("time for block 2")
	
if __name__ == "__main__":
    main()

这种方案当然不会有问题,但是他需要在每次调用deal_business的地方都进行一个统计操作,并不是一个好的选择。那么另外一种思路,在函数里面进行统计,这样就不需要修改调用的代码了。

#example 3
import time

def deal_business(arg):
    time_start_1 = time.clock()
    print("Dealing Business in {}".format(arg))
	time_end_1 = time.clock()
	print("time for {}".format(arg))
	
def main():
    # in block 1
	deal_business("block 1")
	# in block 2
	deal_business("block 2")
	
if __name__ == "__main__":
    main()

这种方案当然也是可行的,但是他破坏了函数的结构。另外一方面,对于不同的业务函数,都需要进行对应的修改。更进一步,我们想到去使用一个outer函数,把业务作为参数穿进去来进行计时操作,如example 4:

#example 4
import time

def record_time(f, arg):
    time_start_1 = time.clock()
    f(arg)
    time_end_1 = time.clock()
    print("time for {}".format(arg))
	
def deal_business(arg):
    print("Dealing Business in {}".format(arg))

def main():
    # in block 1
    #deal_business("block 1")
    record_time(deal_business, "block 1")
    # in block 2
    #deal_business("block 2")
    record_time(deal_business, "block 2")
	
if __name__ == "__main__":
    main()

这种方案解决了example 3中的不足,针对各种函数都能进行调用。但是和example 2一样,需要修改调用代码。幸运的是,python提供了一个语法糖,他可以让我们不修改业务代码的情况下进行替换,那就是@

@语法

@语法实际上执行的是一个翻译的动作,以函数作为参数,翻译出函数并进行相关的运行操作。比如说:

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

#This is equivalent to:

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

他实际上是先进行翻译,再根据翻译的后的函数进行一个函数调用,这类似于C/C++里面的宏展开。那么在这里最值得注意的一个地方就是,不要在decorator中返回函数的执行,而是返回函数的对象。如example 5:

#example 5
def decorator(f):
    print("in decorator")
    return f()

@decorator
def f():
    print("in f")

	
'''
    这个会被翻译成 :
    print("in decorator")
	f()()
	而且更需要注意的是,他会在解释的时候就执行,也就是说在 def f() 这一行执行
'''

既然是即时执行,那么我们怎么去做到延迟执行呢?很好的一个方案是使用闭包(Closure)。

Closure

python中的闭包是指引用了自由变量的函数。所谓自由变量是指作用域包含闭包函数的变量,他们在闭包函数引用后生存期会延长到闭包函数的生存期(实际上有闭包函数的__closure__属性维护)。因而,对于example 5我们可以进行这样的变化:

#example 6
def decorator(f):
    def inner():
        print("in decorator")
        f()
    return inner

@decorator
def f():
    print("in f")

def main():
    f()

if __name__ == "__main__":
    main()

'''
    上例会被翻译成 :
    def f():
        print("in decorator")
        print("in f")
'''

inner是一个闭包函数,他可以调用上层函数的自由变量f,并执行。

装饰器,GO

有了@语法糖和闭包函数的特性,我们可以对之前的example进行改造了。回到example 4,我们需要做的是把record_time做成一个装饰器。改造后的示例如example 7:

#example 7
import time

def record_time(f):
    def inner(arg):
        time_start_1 = time.clock()
        f(arg)
        time_end_1 = time.clock()
        print("time for {}".format(arg))
    return inner
    
@record_time
def deal_business(arg):
    print("Dealing Business in {}".format(arg))

def main():
    # in block 1
    deal_business("block 1")
    # in block 2
    #deal_business("block 2")
    
if __name__ == "__main__":
    main()

'''
    上例会被翻译成 :
    def deal_business(arg):
        time_start_1 = time.clock()
        print("Dealing Business in {}".format(arg))
        time_end_1 = time.clock()
        print("time for {}".format(arg)) 
'''

更近一步,针对更加复杂的案例,我们同样可以解决。比如,如果deal_business的参数定义为def deal_business(*args, **kwargs),只需要给inner同样的参数即可。这些在本次讨论之外,点到为止。 另外一方面,装饰器可以由函数实现,所以他可以可以做到嵌套调用以及参数调用。如example 8给出了带参数的装饰器以及嵌套使用的装饰器:

# example 8
import time

def nest_description(*nest_args):
    print("user info: {}".format(nest_args))
    def outer(f):
        def inner(arg):
            f(arg)
        return inner
    return outer

def record_time(**deco_args):
    def outer(f):
        arg_list = [" : ".join([item[0], item[1]]) for item in deco_args.items()]
        print("version info: {}".format(arg_list))
        def inner(arg):
            time_start_1 = time.clock()
            f(arg)
            time_end_1 = time.clock()
            print("time for {}".format(arg))
        return inner
    return outer

@nest_description("erdao", "master")
@record_time(name = "test", version = "v1")
def deal_business(arg):
    print("Dealing Business in {}".format(arg))


def main():
    # in block 1
    deal_business("block 1")
    # in block 2
    # deal_business("block 2")


if __name__ == "__main__":
    main()

实际上,装饰器还可以由类来实现。作为装饰器存在的类,他有以下几个特征:

  • 类需要实现__init____call__两个方法
  • __init__实现对编译器的解释,__call__完成对编译器的调用

example 9我们可以看到类式装饰器的解译和执行的顺序。

# example 9
class ExampleDecorator(object):
    
    def __init__(self, f):
        print("init decorator")
        self._func = f

    def __call__(self):
        print("call")
        self._func()

@ExampleDecorator
def func_a():
    print("in func a")

@ExampleDecorator
def func_b():
    print("in func b")

print("special cut line")

def main():
    func_a()
    func_b()


if __name__ == "__main__":
    main()

'''
输出是 :
init decorator
init decorator
special cut line
call
in func a
call
in func b
'''

也就是说在@语法糖执行的时候进行__init__,在调用的时候执行__call__。更深一步的探入,@语法实际上执行的是以下操作:

@ExampleDecorator
def f()
# 等同于
f = ExampleDecorator(f).__call__

但是,这种方式是有一个限制的:我们发现不能对类式装饰器进行参数初始化。幸运的是,我们可以利用@忠实解释的过程,__call__操作前移,即在@的时候进行参数调用:

# example 10
class ExampleDecorator(object):
    
    def __init__(self, *args):
        print("init decorator")
        self._arglist = args

    def __call__(self, f):
        print("decorator arglist : {}".format(self._arglist))
        print("call")
        return f

@ExampleDecorator("ver1")
def func_a():
    print("in func a")

@ExampleDecorator("ver2")
def func_b():
    print("in func b")

print("special cut line")

def main():
    func_a()
    func_b()


if __name__ == "__main__":
    main()

上例中解释为:

f = ExampleDecorator("ver1").__call__(f)

如果有需要对func_a带上参数的话,可以仿照之前闭包函数的实现方式进行实现。

装饰器用处

说了这么多装饰器的原理是实现,那么装饰器都可以用在什么地方呢?

  • 辅助功能——比如在常用的Logging/Debugging中输出函数的调用递归、行数,时间消耗等
  • 缓存——进行某些优化
  • 隔离——根据一些attr来区别运行
  • 类型保障——在函数运行前保障参数类型

最后几个例子

#example 11
'''
实现一个登录系统:检测是否有账户,如果有进行验证,否则抛出异常
'''

def authenticate(func):
    def authenticate_and_call(*args, **kwargs):
        if not Account.is_authentic(request): 
            raise Exception('Authentication Failed.')
        return func(*args, **kwargs)
    return authenticate_and_call

def authorize(role):
    def wrapper(func):
        def authorize_and_call(*args, **kwargs):
            if not current_user.has(role): 
                raise Exception('Unauthorized Access!')
            return func(*args, **kwargs)
        return authorize_and_call
    return wrapper

@authorize('admin')
@authenticate
def deal_request(request):
    print("login in")
#example 12
'''
实现一个单例模式
'''

def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class MyClass(object):
    
    def process(self):
        print("in singleton MyClass")


a = MyClass()
print(a)
b = MyClass()
print(b)
#example 13
'''
实现对参数数量的限定
'''
def default_error_func(*args):
    print("wrong args")
    
def accepts(f):
    if f.func_code.co_argcount != 2:
        return default_error_func
    return f

@accepts
def func_a(arg1):
    print("in func a")

@accepts
def func_b(arg1, arg2):
    print("in func b")

def main():
    func_a(1)
    func_b(1, "1")

if __name__ == "__main__":
    main()