Python 装饰器 -- Decorators

Python 装饰器: Decorators

动机:Motivation

在装饰器语法出现之前,如果需要在不改动函数和方法 (function and method, 类中的函数称为方法) 的原始代码的情况下对函数行为做出改变或调整,需要将具体的改变置于函数体之后。对于大型函数来说,这将函数行为的关键组件与函数的其余外部接口的定义分离开来。如:

def foo(self):
    perform method operation
foo = classmethod(foo)

对于较长的方法而言,可读性也会降低。对于概念上是一个声明的函数,将其命名三次似乎也显得不那么 Pythonic。这个问题的一个解决方案是将方法的转换移到更接近方法本身声明的地方。

def foo(cls):
    pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)

新的装饰器语法的目的就是将函数变换部分放置在函数声明中 (新的语法形式借鉴了Java Annotations (Java 注解),Python Decorators 中介绍了装饰器语法的其他提案)。

@classmethod
@synchronized(lock)
def foo(cls):
    pass

在 Python 2.4 中,只添加了关于函数和方法 (function and method) 的装饰器,而关于类的装饰器语法在 Python 3.0 中才添加 (见 PEP 3129 – Class Decorators)。类的装饰器语法与函数的语法一致,如下面的两个代码段是等价的。

class A:
  pass
A = foo(bar(A))


@foo
@bar
class A:
  pass

装饰器语法是纯粹的语法糖1,因为它使得改变函数的行为变得更加简单易读,但是去掉了它也并不影响语言所能实现的功能2

语法

@dec2
@dec1
def func(arg1, arg2, ...):
    pass
def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

以上两段代码是等价的,前者是使用了装饰器的语法,函数嵌套的顺序是自底向上的,类似数学中的复合函数, ( g ⋅ f ) ( x ) (g \cdot f)(x) (gf)(x) 等价于 g ( f ( x ) ) g(f(x)) g(f(x)),所以 @g @f def foo() 也等价于 foo=g(f(foo))

示例

简单用例

In [1]: def decorator(func):
   ...:     def decorated():
   ...:         """decorated docstring"""
   ...:         pass
   ...:
   ...:     return decorated
   ...:

In [2]: @decorator
   ...: def fun():
   ...:     """fun docstring"""
   ...:     pass
   ...:

In [3]: fun.__name__
Out[3]: 'decorated'

In [4]: fun.__doc__
Out[4]: 'decorated docstring'

在上面的例子中,fundecorated 替代,函数名和文档字符串也都被改变。可以使用 functools.wraps 函数来保留原始的函数名及文档字符串。

In [1]: from functools import wraps

In [2]: def decorator(func):
   ...:     @wraps(func)
   ...:     def decorated():
   ...:         """decorated docstring"""
   ...:         pass
   ...:
   ...:     return decorated
   ...:

In [3]: @decorator
   ...: def fun():
   ...:     """fun docstring"""
   ...:     pass
   ...:

In [4]: fun.__name__
Out[4]: 'fun'

In [5]: fun.__doc__
Out[5]: 'fun docstring'

functools 标准库中实现了一些高阶函数,即将函数作为参数的函数,可以用作装饰器来改变函数行为,如为函数提供缓存功能的装饰器 @functools.lru_cache

当被装饰的函数带有参数时

In [1]: from functools import wraps

In [2]: def decorator(func):
   ...:     @wraps(func)
   ...:     def decorated(*args, **kwargs):
   ...:         """decorated docstring"""
   ...:         print("In decorator")
   ...:         return func(*args, **kwargs)
   ...:
   ...:     return decorated
   ...:

In [3]: @decorator
   ...: def fun(s):
   ...:     """fun docstring"""
   ...:     print(s)
   ...:

In [4]: fun("In fun")
In decorator
In fun

认证: Authorization3

from functools import wraps

def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated

日志:Logging4

from functools import wraps

def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logit
def addition_func(x):
   """Do some math."""
   return x + x


result = addition_func(4)
# Output: addition_func was called

带参数的装饰器

有时候的装饰器可能带有参数,从而实现更复杂的流程控制。如上述的用法中 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)

查看 functools.wraps 的源码可以看到,它调用了 partial,进一步查看 partial,发现它是一个类,并实现了特殊方法 __call__,从而使得类本身也可以作为函数使用。

    def __call__(*args, **keywords):
        if not args:
            raise TypeError("descriptor '__call__' of partial needs an argument")
        self, *args = args
        newkeywords = self.keywords.copy()
        newkeywords.update(keywords)
        return self.func(*self.args, *args, **newkeywords)

实际上,因为 Python 中一切皆为对象,函数也可以赋值给别的变量,装饰器作为语法糖,只是简化了函数作为参数的函数调用,所以只要作为装饰器是一个函数,并且这个函数的唯一传入参数也是函数就行。

使用函数嵌套实现带参数的装饰器
In [1]: from functools import wraps

In [2]: def greet(name):
   ...:     def decorator(func):
   ...:         @wraps(func)
   ...:         def decorated(*args, **kwargs):
   ...:             print(f"hello {name}")
   ...:             return func(*args, **kwargs)
   ...:         return decorated
   ...:     return decorator
   ...:

In [3]: @greet("world")
   ...: def fun():
   ...:     pass
   ...:

In [4]: fun()
hello world

In [5]: @greet("universe")
   ...: def bar():
   ...:     pass
   ...:

In [6]: bar()
hello universe
使用类实现带参数的装饰器
In [1]: from functools import wraps

In [2]: class greet:
   ...:     def __init__(self, name):
   ...:         self.name = name
   ...:
   ...:     def __call__(self, func):
   ...:         @wraps(func)
   ...:         def decorated(*args, **kwargs):
   ...:
   ...:             print(f"hello {self.name}")
   ...:             return func(*args, **kwargs)
   ...:
   ...:         return decorated
   ...:

In [3]: @greet("world")
   ...: def fun():
   ...:     pass
   ...:

In [4]: fun()
hello world

In [5]: @greet("universe")
   ...: def bar():
   ...:     pass
   ...:

In [6]: bar()
hello universe

总结

  1. Python 中一切皆对象。
  2. Python 作为语法糖,简化了函数调用写法。
  3. 在编写 Python 装饰器实现时,要注意区别什么是函数,什么是函数调用。

  1. https://en.wikipedia.org/wiki/Python_syntax_and_semantics#Decorators ↩︎

  2. https://en.wikipedia.org/wiki/Syntactic_sugar ↩︎

  3. https://book.pythontips.com/en/latest/decorators.html#authorization ↩︎

  4. https://book.pythontips.com/en/latest/decorators.html#logging ↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值