Decorator in Python – Python中的装饰器

简介

Python中的装饰器是用来在不改变原来方法的前提下, 给其他函数增加功能性的一种方式.

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

通常使用的装饰器分为函数装饰器和类装饰器, 同时根据是否传入参数又分为带参装饰器和不带参装饰器, 下面会按照这几种分类依次进行介绍.

函数装饰器

不带参函数装饰器

以一个最简单的装饰器 logger 日志记录器 作为开始. 用这个装饰器来进行调用方法名称的输出.

def logger(func):
    def wrapper(*args, **kwargs):
        print('Ready to call func : {}'.format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@logger
def hello_world(text):
    print(text)

调用hello_world来检查装饰器的效果

>>> hello_world('hello,world!')
Ready to call func : hello_world
hello,world!

在这个例子中, 函数进入和退出时, 被称为一个横截面, 这种编程方式也被称为面向切面的编程.

面向切面的编程 细节请看面向切面编程

带参函数装饰器

def say_hello(country):
    def wrapper(func):
        def deco(*args, **kwargs):
            if country == 'china':
                print('你好!')
            elif country == 'american':
                print('Hello!')
            else:
                return
            return func(*args, **kwargs)
        return deco
    return wrapper

@say_hello('china')
def chinese():
    print('我来自中国.')

@say_hello('american')
def american():
    print('I am from America.')

调用相关函数

>>> american()
Hello!
I am from America.

>>> chinese()
你好!
我来自中国.

允许带参数的装饰器实际是对原有装饰器的一个函数封装, 并返回一个装饰器(warpper). 可以将它理解为一个含有参数的闭包.

当使用@say_hello('china')时, Python能够发现这一层封装, 并把参数传递到装饰器的环境中.

这里, @say_hello('china') 相当于 @wrapper

类装饰器

基于类实现的装饰器必须实现__init____call__两个内置函数. 但带参和不带参的类装饰器中__init____call__的目的是不同的.

不带参类装饰器

不带参的类装饰器中, __init__ 用于接受被装饰函数. __call__ 用于实现装饰逻辑.

class logger:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('[INFO] Ready to call func : {func}'.format(func=self.func.__name__))
        return self.func(*args, **kwargs)

@logger
def hello_world(text):
    print(text)

调用hello_world查看具体效果

>>> hello_world('hello,world!')
[INFO] Ready to call func : hello_world
hello,world!

带参类装饰器

带参的类装饰器中, __init__ 用于接收传入参数. __call__ 用于接收被装饰函数 和 实现装饰逻辑.

class logger:
    def __init__(self, level):
        self.level = level

    def __call__(self,func):
        def wrapper(*args, **kwargs):
            print('[{level}] Ready to call func : {func}'.format(level=self.level, func=func.__name__))
            return func(*args, **kwargs)
        return wrapper

@logger(level='DEBUG')
def hello_world(text):
    print(text)

调用hello_world查看具体效果

>>> hello_world('hello,world!')
[DEBUG] Ready to call func : hello_world
hello,world!

装饰器的实现原理

以不带参数的函数装饰器为例

def logger(func):
    def wrapper(*args, **kwargs):
        print('Ready to call func : {}'.format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

@logger
def hello_world(text):
    print(text)

hello_world()

解释器在运行这段代码是, 首先进行自上而下的代码解释. 首先解释logger的定义, 然后解释被装饰的hello_world段.

在解释logger时, 内部的代码并不会被执行, 但是当解释到hello_world段中@logger这一句时, 将会触发Python的语法糖, 执行logger函数, 并将hello_world方法作为logger的参数传入, 相当于logger(hello_world). 这时候logger内部代码就会被执行. 因为looger最终返回的是wrapper函数, 所以这时相当于hello_word = logger(hello_world).

装饰器的顺序

@decorater_0
@decorater_1
@decorater_2
def hello_world():
    print('hello, world!')

在被多个装饰器装饰的过程中, 装饰器的执行顺序是从里到外, 最先调用最里层的装饰器(@decorator_2), 最后调用最外层, 也就是最先声明的装饰器(@decorator_0).

def dec1(func):
    print("1111")
    def one():
        print("2222")
        func()
        print("3333")
    return one

def dec2(func):
    print("aaaa")
    def two():
        print("bbbb")
        func()
        print("cccc")
    return two

@dec1
@dec2
def test():
    print("test test")

调用test查看装饰器执行顺序

>>> test()
aaaa
1111
2222
bbbb
test test
cccc
3333

调用test的过程相当于test=dect1(dect2(test)). 这时先执行dect2(test), 所以第一步先输出aaaa, 然后将two(也就是dect2(test))作为参数传给dect1(), 所以第二步输出的是1111, 然后test就变为了dect1(dect2(test)). 继续执行的时候, 会从one中继续执行, 符合第三步输出2222, 然后执行one中的func. 注意这里的func其实是dect2(test)也就是two, 所以会继续执行two中的bbbb, two中的func(也就是原始的test,输出test test), 最后依次递归调用出来.

wraps

在functools标准库中提供一个wraps装饰器.

functools.wraps(wrapped[, assigned][, updated])

This is a convenience function for invoking update_wrapper() as a function decorator when defining a wrapper function. It is equivalent to partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated).

当定义一个 wrapper function 时, warps 是可以被当做调用update_warpper作为方法修饰符的快捷方式来调用. 它相当于 partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)

>>> from functools import wraps
>>> def my_decorator(f):
...     @wraps(f)
...     def wrapper(*args, **kwds):
...         print('Calling decorated function')
...         return f(*args, **kwds)
...     return wrapper
...
>>> @my_decorator
... def example():
...     """Docstring"""
...     print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'

Without the use of this decorator factory, the name of the example function would have been ‘wrapper’, and the docstring of the original example() would have been lost.

如果没有这个修饰符工厂, 则实例方法中的名字将会是 ‘wrapper’, 而原始example()方法中的docstring也会随之丢失.

来看下如果没有使用wraps的结果.

def my_decorator_without_wraps(f):
    def wrapper(*args, **kwargs):
        print('Calling decorated function without wraps')
        f(*args, **kwargs)
    return wrapper

@my_decorator_without_wraps
def example_without_wraps():
    """Docstring"""
    print('Called example function without wraps')

>>> example_without_wraps()
Calling decorated function without wraps
Called example function without wraps

>>> example_without_wraps.__name__
wrapper

>>> example_without_wraps.__doc__
None

对比之下可以看出, 如果没有使用wraps, 则example经过装饰器装饰之后, 原本的docstring__name__属性都会变为装饰器内部wrapper对应的值(也就是Nonewrapper).

functools.wraps装饰器的作用是将被修饰的函数的一些属性复制给装饰器函数. 而其本身也是一个偏函数. 其源码如下:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

深追下去, update_wrapper的源码如下:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',  '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

实战

函数运行超时

一篇文章搞懂装饰器所有用法(建议收藏) 中给出了一个实现控制函数运行超时的装饰器.

如果被装饰的func运行时间超时, 则会抛出异常.

import signal

class TimeoutException(Exception):
    def __init__(self, error='Timeout waiting for response from Cloud'):
        Exception.__init__(self, error)

def timeout_limit(timeout_time):
    def wraps(func):
        def handler(signum, frame):
            raise TimeoutException()

        def deco(*args, **kwargs):
            signal.signal(signal.SIGALRM, handler)
            signal.alarm(timeout_time)
            func(*args, **kwargs)
            signal.alarm(0)
        return deco
    return wraps

上述代码将handler注册为SIGALRM信号的处理函数, 同时利用alarm(timeout_time)来设定在timeout_time秒后发送一个SIGALRM信号. 如果这个时候没有执行完func(*args,**kwargs)的话, 则会抛出SIGALRM信号触发handler进行处理. 如果执行完毕, 则通过signal.alarm(0)进行取消.

延迟2秒, 报警1秒

@timeout_limit(1)
def hello_world():
    print('start')
    time.sleep(2)
    print('end')

>>> hello_world()
hello!
Traceback (most recent call last):
  File "/Users/src/utils/hello_world.py", line 64, in <module>
    hello_world()
  File "/Users/src/utils/hello_world.py",  line 50, in deco
    func(*args, **kwargs)
  File ""/Users/src/utils/hello_world.py"", line 61, in hello_world
    time.sleep(2)
  File "/Users/src/utils/hello_world.py", line 45, in handler
    raise TimeoutException()
__main__.TimeoutException: Timeout waiting for response from Cloud

延迟2秒, 报警3秒

@timeout_limit(3)
def hello_world():
    print('start')
    time.sleep(2)
    print('end')

>>> hello_world()
hello!
world!

如果想知道更多关于Python中如何实现函数超时报警的内容, 可以查阅 Python函数超时该怎么解决?

函数作为装饰器参数

python装饰器 中实现了一个可以将函数作为参数的函数装饰器, 这也说明了Python中万物皆为对象.

def Before(request,kargs):
    print 'before'

def After(request,kargs):
    print 'after'

def Filter(before_func,after_func):
    def outer(main_func):
        def wrapper(request,kargs):
            before_result = before_func(request,kargs)
            if(before_result != None):
                return before_result;
            main_result = main_func(request,kargs)
            if(main_result != None):
                return main_result;
            after_result = after_func(request,kargs)
            if(after_result != None):
                return after_result;
        return wrapper
    return outer

@Filter(Before, After)
def Index(request,kargs):
    print 'index'

参考 References

一篇文章搞懂装饰器所有用法(建议收藏) 一篇很好的介绍装饰器的文章, 建议收藏.

理解 Python 装饰器看这一篇就够了 浅显易懂.

装饰器 – 廖雪峰 廖老师关于装饰器的讲解.

python 装饰器

decorators in the python standard lib (@deprecated specifically)2019-07-12 15:50:44 星期五

Python Decorator 里介绍了几个比较好的案例, 例如用装饰器实现不同格式的日志输出, 支持线程异步处理的装饰器 , 限于篇幅不在这里展开, 感兴趣的人可以移步.

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据