Python学习笔记-装饰器

作用:

Python的 decorator 本质上就是一个高阶函数,它接收一个函数作为参数,然后,返回一个新函数。就是对原函数的一种装饰作用,但是不改变原函数,只是对其调用前后增加一些操作或者信息显示。使用 decorator 用Python提供的 @ 语法,可以简化代码,避免每个函数编写重复性代码。
打印日志,@log
性能检测,@performance
数据库事务,@transaction
URL路由,@post(‘/register’)
@log的定义:

def log(f):
    def fn(x):
        print 'call ' + f.__name__ + '()...'
        return f(x)
    return fn

对于阶乘函数,@log:

@log
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))
print factorial(10)

运行结果
但是,对于参数不是一个的函数,调用将报错:

@log
def add(x, y):
    return x + y
print add(1, 2)


因为 add() 函数需要传入两个参数,但是 @log 写死了只含一个参数的返回函数。要让 @log 自适应任何参数定义的函数,可以利用Python的 *args 和 **kw,保证任意个数的参数总是能正常调用:

def log(f):
    def fn(*args, **kw):
        print 'call ' + f.__name__ + '()...'
        return f(*args, **kw)
    return fn

现在,对于任意函数,@log 都能正常工作。

例子:

编写完整@performance,打印出函数调用的时间。

import time

def performance(f):
    def fn(*args, **kw):
        t1 = time.time()
        r = f(*args, **kw)
        t2 = time.time()
        print 'call %s() in %fs' % (f.__name__, (t2 - t1))
        return r
    return fn

@performance
def factorial(n):
    return reduce(lambda x, y: x*y, range(1, n+1))

print(factorial(10))

多个修饰器

例子代码:

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "<\______/>"
    return wrapper

def ingredients(func):
    def wrapper():
        print "#apple#"
        func()
        print "~apple+~"
    return wrapper

def sandwich(food="--hello world--"):
    print food

#sandwich()
#--ham--
sandwich = bread(ingredients(sandwich))
sandwich()

运行结果:
Python学习笔记-装饰器_第1张图片
如果使用@语法的话,如下所示:

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "<\______/>"
    return wrapper

def ingredients(func):
    def wrapper():
        print "#apple#"
        func()
        print "~apple+~"
    return wrapper

@bread
@ingredients

def sandwich(food="--hello world--"):
    print food

sandwich()

与上述运行结果是一样的。

如果调整下书写装饰器的顺序呢?
代码:

def bread(func):
    def wrapper():
        print "</''''''\>"
        func()
        print "<\______/>"
    return wrapper

def ingredients(func):
    def wrapper():
        print "#apple#"
        func()
        print "~apple+~"
    return wrapper


@ingredients
@bread
def sandwich(food="--hello world--"):
    print food

sandwich()

运行结果:
Python学习笔记-装饰器_第2张图片

带参数的修饰器

@log('DEBUG') 
def my_func():
    pass

转成高阶函数的调用,就是:

my_func = log('DEBUG')(my_func)

完整表述为:

log_decorator = log('DEBUG')
my_func = log_decorator(my_func)

上述语句等价于:

log_decorator = log('DEBUG')
@log_decorator
def my_func():
    pass

所以,带参数的log函数首先返回一个decorator函数,再让这个decorator函数接收my_func并返回新函数:

def log(prefix):
    def log_decorator(f):
        def wrapper(*args, **kw):
            print '[%s] %s()...' % (prefix, f.__name__)
            return f(*args, **kw)
        return wrapper
    return log_decorator

@log('DEBUG')
def test():
    pass
print test()

其中的返回结果:
[DEBUG] test()…
None
其中的None是print test()的结果
如果对这种3层嵌套的decorator定义,你可以先把它拆开:

# 标准decorator:
def log_decorator(f):
    def wrapper(*args, **kw):
        print '[%s] %s()...' % (prefix, f.__name__)
        return f(*args, **kw)
    return wrapper
return log_decorator

# 返回decorator:
def log(prefix):
    return log_decorator(f)

拆开以后会调用会失败,因为在3层嵌套的decorator定义中,最内层的wrapper引用了最外层的参数prefix,所以,把一个闭包拆成普通的函数调用会比较困难。不支持闭包的编程语言要实现同样的功能就需要更多的代码。
上一次的@performance只能打印秒,请给 @performace 增加一个参数,允许传入’s’或’ms’,代码如下:

import time

def performance(unit):
    def performance_temp(f):
        def fn(*args, **kw):
            t1 = time.time()
            r = f(*args, **kw)
            t2 = time.time()
            t = (t2-t1)
            if unit == 'ms':
                t = (t2-t1)*1000
            else:
                pass
            print 'call %s() in %f%s' % (f.__name__, t,unit)
            return r
        return fn
    return performance_temp


@performance('ms')
def factorial(n):
    return reduce(lambda x,y: x*y, range(1, n+1))

print factorial(10)

运行结果:

完善decorator

在没有decorator的情况下,打印函数名:

def f1(x):
    pass
print f1.__name__

有decorator的情况,但是不在decorator内部使用.name方式,在调用的时候用.name的方式调用,再打印函数名(注意对比1部分中的例子):

def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print f2.__name__

打印出来的结果是:
wrapper
由于decorator返回的新函数函数名已经不是‘f2’,而是@log内部定义的’wrapper’。这对于那些依赖函数名的代码就会失效。decorator还改变了函数的doc等其它属性。如果要让调用者看不出一个函数经过了@decorator的“改造”,就需要把原函数的一些属性复制到新函数中:

def log(f):
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    wrapper.__name__ = f.__name__
    wrapper.__doc__ = f.__doc__
    return wrapper

这样写decorator很不方便,因为我们也很难把原函数的所有必要属性都一个一个复制到新函数上,所以Python内置的functools可以用来自动化完成这个“复制”的任务:

import functools
def log(f):
    @functools.wraps(f)#多了一行此处的代码
    def wrapper(*args, **kw):
        print 'call...'
        return f(*args, **kw)
    return wrapper
@log
def f2(x):
    pass
print f2.__name__

注意:@functools.wraps应该作用在返回的新函数上。对于3层的decorator函数时候,需要特别注意这点。所以,对于上述带参数的装饰器中,必要时候@functools.wraps添加在def performance_temp(f):
和def fn(*args, **kw)之间。
由于把原函数签名改成了(*args, **kw),因此,无法获得原函数的原始参数信息。即便采用固定参数来装饰只有一个参数的函数:

def log(f):
    @functools.wraps(f)
    def wrapper(x):
        print 'call...'
        return f(x)
    return wrapper

也可能改变原函数的参数名,因为新函数的参数名始终是 ‘x’,原函数定义的参数名不一定叫 ‘x’。

引申参考:

http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python/1594484#1594484

你可能感兴趣的:(python)