最近在研究FastAPI实现一个更易于操作的API框架.然后因为FastAPI的语法规则,每次都要写到@app.get('')
那么这个@开头的奇怪东西...我虽然知道是装饰器...可是怎么用...?


简单的装饰器

比方说,你现在需要记录输出10000个数的时间,你可能会这么写:

import time

startTime = time.time()
for i in range(10000):
    print i
endTime = time.time()
print(endTime - startTime)

那么现在你需要记录快速排序10个数的时间,又是什么样的代码呢?

import time

def partition(arr,low,high): 
    i = ( low-1 )
    pivot = arr[high]     
  
    for j in range(low , high): 
        if   arr[j] <= pivot: 
            i = i+1 
            arr[i],arr[j] = arr[j],arr[i] 
    arr[i+1],arr[high] = arr[high],arr[i+1] 
    return ( i+1 ) 

def quickSort(arr,low,high): 
    if low < high: 
        pi = partition(arr,low,high) 
        quickSort(arr, low, pi-1) 
        quickSort(arr, pi+1, high) 
  
arr = [10,7,8,9,1,5,11,20,0,4] 
n = len(arr) 
startTime = time.time()
quickSort(arr,0,n-1) 
endTime = time.time()
for i in range(n): 
    print ("%d" %arr[i])
print(endTime - startTime)

问题来了,假如你有100个算法需要计算时间,你就需要改动每个脚本里的函数
很麻烦对不对?
这里,我们可以引入装饰器

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
总的来讲,装饰器的功能就是在不改变函数或对象代码的情况下增添额外的功能.

def timeDcrtr(func):
    def wrapper(*args,**kargs):
        startTime = time.time()
        f = func(*args,**kargs)
        endTime = time.time()
        execTime = endTime - startTime
        return f
    return wrapper

上面的代码展现了如何定义一个计算时间的装饰器,我们将它再次引入上文中输出10000个数字的代码:

import time

def timeDcrtr(func):
    def wrapper():
        startTime = time.time()
        f = func()
        endTime = time.time()
        execTime = endTime - startTime
        return f
    return wrapper

@timeDcrtr # @语法糖
for i in range(10000):
    print i

到这里,你已经实现了Python中最简单的装饰器功能.
但是如果你要为装饰器传入参数怎么办?
因为返回的函数并不能接受参数,你可以指定装饰器函数wrapper接受和原函数一样的参数:

def debug(func):
    def wrapper(something): #指定与下面output()函数一样的参数
        print "enter {}()".format(func.__name__)
        return func()
    return wrapper

@debug
def output(something):
    print "hello {}!".format(something)

但是问题又来了: debug装饰器在一个脚本里应用还好,可是你如果有100个脚本需要使用@debug呢?你需要手动修改每个脚本里的装饰器参数吗?
别忘了python中的可变参数: *args,**kwargs
于是我们可以打造一个接受可变参数的装饰器:

def debug(func):
    def wrapper(*args,**kwargs): 
        print "enter {}()".format(func.__name__)
        return func()
    return wrapper

更高级的装饰器

假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么装饰器就会是这样的:

def logging(level):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print "[{level}]: enter function {func}()".format(
                level=level,
                func=func.__name__)
            return func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@logging(level='INFO')
def say(something):
    print "say {}!".format(something)

# 如果没有使用@语法,等同于
# say = logging(level='INFO')(say)

@logging(level='DEBUG')
def do(something):
    print "do {}...".format(something)

if __name__ == '__main__':
    say('hello')
    do("my work")

装饰器函数其实是这样一个接口约束它,必须接受一个callable对象作为参数,,然后返回一个callable对象.在Python中一般callable对象都是函数,但也有例外,只要某个对象重载了__call__()方法,那么这个对象就是callable的.
装饰器要求接受一个callable对象,并返回一个callable对象.那么用类来实现也是也可以的.我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果.

class logging(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print "[DEBUG]: enter function {func}()".format(
            func=self.func.__name__)
        return self.func(*args, **kwargs)
@logging
def say(something):
    print "say {}!".format(something)

小结

装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都被命名为wrapper(),意义在于包装.函数只有在被调用时才会发挥其作用.比如@logging装饰器可以在函数执行时额外输出日志,@cache装饰过的函数可以缓存计算结果等等.