详解装饰器

装饰器非常重要,不仅使代码看起来更简洁优雅,而且最大限度的复用了代码。本文主要讲解装饰器的3种用法,分别是:
1、闭包装饰器
2、类装饰器
3、Python第三方模块提供的方法
个人更推荐后面两种,可读性较好。这只是一个习惯而已,有人也觉得第一种更加简洁。

1. 闭包装饰器

闭包
闭包装饰器含有两个名词分别是闭包和装饰器,初学者可能不太清楚,在这里做一个简单的说明。闭包在很多语言中都有这个概念,是指外层函数的局部变量是内层函数的全局变量(概念比较狭义,方便理解Python中的闭包)
我们来看这样一个问题:

def add_tag(tag):
    def add_content(content):
        return "<%s> %s " % (tag, content, tag)

    return add_content

>>>a = add_tag("a")
>>>a(content="hello world")
 hello world 

tag这个变量对于外层函数add_tag来说只是一个局部变量,但对与内层函数add_content来说就是全局变量了,并且对封装函数中任何一个变量,外部都无法访问,这就形成了一个封闭的概念。

简单闭包装饰器
做一个输入检查的问题,验证name=admin,password=123456。当然如果对于多个函数接口都需要验证这样的参数,使用装饰器是最好的选择。

def login(name, password):
    if name != "admin" or password != "123456":
        return "username or password error"

    print "login success."
    print "do something."

使用装饰器以后,登陆函数中间不必做验证,只需要实现逻辑:

def check_login(func):
    def _wrapper(name, password):
        if name != "admin" or password != "123456":
            print "login failed"
            return "username or password error"
        return func(name, password)

    return _wrapper


@check_login
def login(name, password):
    print "login success."
    print "do something."

>>>login("admin", "123456")
login success.
do something.
>>>login("admin", "156")
login failed

注意观察外层接收函数对象,内层接受参数,那么装饰就是这样的

>>>check_login(login)("admin", "123456")
login success.
do something.
>>>check_login(login)("admin", "1256")
login failed

发现结果是一样的,check_login(login)返回的是内层函数对象,内层函数刚好接收用户名和密码两个参数,也就是说@这个语法糖只是简化了展开式。

思考
仔细思考会发现,这里有问题,这个登陆检测装饰器只接受两个参数,如何支持任意多个参数呢,这个问题还是留给读者吧。

为了加深理解,再加一个日志装饰器,输出调用函数的名称,两个装饰器一起装饰login函数。(此处有坑,小心)

def log(func):
    def _wrapper(*args, **kwargs):
        print "call %s " % func.__name__
        return func(*args, **kwargs)

    return _wrapper


def check_login(func):
    def _wrapper(name, password):
        if name != "admin" or password != "123456":
            print "login failed"
            return "username or password error"
        return func(name, password)

    return _wrapper


@log
@check_login
def login(name, password):
    print "login success."
    print "do something."


>>>login("admin", "156")
call _wrapper 
login failed

>>>login("admin", "123456")
call _wrapper 
login success.
do something.

装饰器的最外层函数接收的参数为函数名,这样函数名就作为内部的全局变量,在任何地方都可以调用了,内层函数接受函数的参数,主要的逻辑放在内层函数里面,比如添加日志信息,或者做参数检查等操作。最后就是按层级关系返回函数,但在这里还有一个问题,就是调用函数的名称发生了变化,因为外层函数是由_wrapper返回的,因此最终得到的函数签名就变成了_wrapper
只需要导入functools下的wraps,然后装饰在内层函数上就能保证函数签名的了。

import functools

def log(func):
    @functools.wraps(func)
    def _wrapper(*args, **kwargs):
        print "call %s " % func.__name__
        return func(*args, **kwargs)

    return _wrapper

给装饰器添加参数
不仅需要输出函数名的调用,还需要指定日志的级别,比如我们装饰一个函数@log(info),这个函数就得输出[info] call login。只需要在最外层改变接受参数,次内层接受函数名,内层接受函数的参数,也就是加了三层,看起来比较复杂了。

import functools

def log(level):
    def _decorator(func):
        @functools.wraps(func)
        def _wrapper(*args, **kwargs):
            print "[%s] call %s " % (level, func.__name__)
            return func(*args, **kwargs)

        return _wrapper

    return _decorator

@log("info")
def login(name, password):
    print "login success."
    print "do something."


>>>login("admin", "123456")
[info] call login 
login success.
do something.

对于三层的装饰器,@语法糖相当于log("info")(login)("admin", "123456"),看着是不是有点蒙了,(我刚开始学习的时候也是抓狂的,所以推荐后面两种),理解以后用哪种就看个人口味了。给出部分代码如下:

def login(name, password):   
    print "login success."   
    print "do something."

>>>log("info")(login)("admin", "123456")
[info] call login 
login success.
do something.

2. 类装饰器

最麻烦的闭包装饰器解决了,类装饰器写起来容易,并且可读性好多了,废话不多说,直接实现上面日志例子吧。

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

    def __call__(self, *args, **kwargs):
        print "call %s" % self.func.__name__
        return self.func(*args, **kwargs)

@Log
def login(name, password):
    print name, password

>>> login("admin", "123456")
call login
admin 123456

初始化返回的对象必须是可调用的,只需要改变类的__call__属性,代码是不是易懂多了。实现带参数装饰器,也就是添加日志的级别的装饰器,稍稍麻烦一点:

class Log(object):
    def __init__(self, level):
        self.level = level

    def __call__(self, func):
        def _wrapper(*args, **kwargs):
            print "[%s] call %s" % (self.level, func.__name__)
            return func(*args, **kwargs)

        return _wrapper

@Log("debug")
def login(name, password):
    print name, password

>>> login("admin", "123456")
[debug] call login
admin 123456

3.第三方包实现的装饰器

wrapt这个包封装了装饰器,直接使用@wrapt.decorator装饰在函数上即可,对于无参数装饰器,实现很简单了,可读性就不用多说了,毕竟是第三方的包。

import wrapt

@wrapt.decorator
def log(func, instance, args, kwargs):
    print "call %s" % func.__name__
    return func(*args, **kwargs)

@log
def login(name, password):
    print name, password


>>>login("admin", "123456")
call login
admin 123456

有参数的情况,还需要在外面加一层函数,这层函数用来接收参数。

import wrapt

def log(level):
    @wrapt.decorator
    def wrapper(func, instance, args, kwargs):
        print "[%s]: call %s" % (level, func.__name__)
        return func(*args, **kwargs)

    return wrapper

@Log("debug")
def login(name, password):
    print name, password


>>>login("admin", "123456")
[debug] call login
admin 123456

小结

装饰器非常重要,刚开始学习都是很困难的。当然如果你要绕过困难,不学也可以实现相同的功能,只是你的代码会很糟糕,可读性差,代码复用率低。Python就是简洁优雅的,你非要写出冗余代码,真心浪费了Guido的一片苦心,一个上午就写了这么点文章,后期还会继续完善。

你可能感兴趣的