深入理解 Python 装饰器

1,947 阅读6分钟

1.介绍

Python装饰器在开发过程中,有着较为重要的地位,但是对于初学者来说,并不便于理解,本文将带着大家分析python装饰器的使用。

2.定义

装饰器本质上就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。
概念较为抽象,我们来考虑如下一个场景,现在我们需要对用户年龄进行认证,如果年龄小于18,则给出提示,年龄不符合要求(嘿嘿嘿,大家都懂)。代码如下:

class Movie(object):
    def get_movie(self,age):
        if age<18:
           raise Exception('用户年龄不符合要求')
        return self.movie
    def set_movie(self,age,movie):
        if age <18:
            raise Exception('用户年龄不符合要求')
        self.movie = movie

考虑到复用性的问题,我们对其修改:

def check_age(age):
if age < 18:
    raise Exception('用户年龄不符合要求')

class User(object):
    def get_movie(self, age):
        check_age(age)
        return self.movie

    def set_movie(self, age, movie):
        check_age(age)
        self.movie = movie

现在,代码看起来整洁了一点,但是用装饰器的话可以做的更好:

def check_age(f):
    def wrapper(*args,**kwargs):
        if args[1]<18:
            raise Exception('用户年龄不符合要求')
        return f(*args,**kwargs)
    return wrapper

class User(object):
    @check_age
    def get_movie(self, age):
        return self.movie
    @check_age
    def set_movie(self, age, movie):
        self.movie = movie

上面这段代码就是使用装饰的一个典型例子,函数check_age中定义了另一个函数wrapper,并将wrapper做为返回值。这个例子很好的展示了装饰器的语法。

2.2 装饰器的本质

上面说到装饰器的本质就是一个函数,这个函数接受另一个函数作为参数,并将其其以一个新的修改后的函数进行替换。再来看下面一个例子,可以帮我们更好的理解:

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

def sandwich():
    print('- sandwich -')

sandwich_copy = bread(sandwich)
sandwich_copy()

输出结果如下:

</''''''\>
- sandwich -
</______\>

bread是一个函数,它接受一个函数作为参数,然后返回一个新的函数,新的函数对原来的函数进行了一些修改和扩展(打印一些东西),且这个新函数可以当做普通函数进行调用。
使用python提供的装饰器语法,简化上面的代码:

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

@bread
def sandwich():
    print('- sandwich -')

sandwich =  sandwich()

到这里,我们应该理解了装饰器的用法和作用了,再次强调一遍,装饰器本质上就是一个函数,这个函数接受其他的函数作为参数,并将其以一个新的修改后的函数进行替换

3.使用装饰器需要注意的地方

前面我们介绍了装饰器的用法,可以看出装饰器其实很好理解,也非常简单。但是装饰器还有一些需要我们注意的地方

3.1 函数的属性变化

装饰器动态替换的新函数替换了原来的函数,但是,新函数缺少很多原函数的属性,如docstring和函数名。

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

@bread
def sandwich():
    '''there are something'''
    print('- sandwich -')

def hamberger():
    '''there are something'''
    print('- hamberger -')

def main():
    print(sandwich.__doc__)
    print(sandwich.__name__)

    print(hamberger.__doc__)
    print(hamberger.__name__)

main()

执行上面的程序,得到如下结果:

None
wrapper
there are something
hamberger

在上述代码中,定义了两个函数sandwich和hanberger,其中sandwich使用装饰器@bread进行了封装,我们获取sandwich和hanberger的docstring和函数名字,可以看到,使用了装饰器的函数,无法正确获取函数原有的docstring和名字,为了解决这个问题,可以使用python内置的functools模块。

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

我们只需要增加一行代码,就能正确的获取函数的属性。
此外,也可以像下面这样:

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

不过,还是第一种方法的可读性要更强一点。

3.2使用inspect函数来获取函数参数

我们再来看如下一段代码:

def check_age(f):
    @functools.wraps(f)
    def wrapper(*args,**kwargs):
        if kwargs.get('age')<18:
            raise Exception('用户年龄不符合要求')
        return f(*args,**kwargs)
    return wrapper

class User(object):
    @check_age
    def get_movie(self, age):
        return self.movie
    @check_age
    def set_movie(self, age, movie):
        self.movie = movie

user = User()
user.set_movie(19,'Avatar')

这段代码运行后会直接抛出,因为我们传入的'age'是一个位置参数,而我们却用关键字参数(kwargs)获取用户名,因此。‘kwargs.get('age')’返回None,None和int类型是无法比较的,所以会抛出异常。
为了设计一个更加智能的装饰器,我们需要使用python的inspect模块。如下所示:

def check_age(f):
    @functools.wraps(f)
    def wrapper(*args,**kwargs):
        getcallargs = inspect.getcallargs(f, *args, **kwargs)
        print(getcallargs)
        if getcallargs.get('age')<18:
            raise Exception('用户年龄不符合要求')
        return f(*args,**kwargs)
    return wrapper

通过inspect.getcallargs,返回一个将参数名和值作为键值对的字典,在上述代码中,返回{'self': <__main__.User object at 0x10be19320>, 'age': 19, 'movie': 'Avatar'},通过这种方式,我们的装饰器不必检查参数username是基于位置参数还是基于关键字参数,而只需在字典中查找即可。

3.3多个装饰器的调用顺序

在开发中,会出现对于一个函数使用两个装饰器进行包装的情况,代码如下:

def bold(f):
    def wrapper():
        return "<b>"+f()+"</b>"
    return wrapper
def italic(f):
    def wrapper():
        return "<i>"+f()+"</i>"
    return wrapper
@bold
@italic
def hello():
    return "hello world"

print(hello()) # <b><i>hello world</i></b>
  • 分析
    在前面我们提到,装饰器就是在外层进行了封装:

      @italic
      hello()
    
      hello = italic(hello)

    对于两层封装便是:

      @bold
      @italic
      hello()
    
      hello = bold(italic(hello))

    这样理解多个装饰器的调用顺序,之后就不会再有疑问了

    3.4 给装饰器传递参数

    现在,我们的需求修改了,并不是限定为18岁了,对于不同的地区可能是20岁,也可能是16岁。那么我们如何设计一个通用的装饰器呢?

def check_age(age='18'):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            getcallargs = inspect.getcallargs(f, *args, **kwargs)
            if getcallargs.get('age') < age:
                raise Exception('用户年龄不符合要求')
            return f(*args, **kwargs)

        return wrapper

    return decorator

class User(object):
    @check_age(18)
    def get_movie(self, age):
        return self.movie
    @check_age(18)
    def set_movie(self, age, movie):
        check_age(age)
        self.movie = movie
user = User()
user.set_movie(16,'Avatar')

通过上述方式,我们可以在使用装饰器时设置age的值,而不需要修改装饰器内的代码,使程序的健壮性更强,符合开闭原则。

4.总结

到这里,关于装饰器的理解,我们就介绍完了,配合在实际开发中的使用,你很快就能掌握它。