Python装饰器(一)

装饰器

简介

装饰器就是一个函数,它可以接受一个函数作为输入并返回一个新的函数作为输出。在 Python 中,装饰器的应用顺序是从下到上的,即最后一个应用的装饰器最先被执行。只会在函数定义的时候应用一次。

普通 python 函数定义如下

def countdown(n): 
    ... 
countdown = timethis(countdown)

如果使用装饰器的则如下所示

@timethis 
def countdown(n): 
    ...

常见的内建的装饰器比如@staticmethod、@classmethod 以及@property 的工作方式也是一样的,如下两个代码片段效果一致

class A: 
    @classmethod 
    def method(cls): 
        pass 
 
class B: 
    # Equivalent definition of a class method 
    def method(cls): 
	    pass 
method = classmethod(method)

装饰器内部的代码一般会涉及创建一个新的函数,利用*args 和**kwargs 来接受任意的参数。如下所示,

import time 
from functools import wraps 
 
def timethis(func): 
    ''' 
    Decorator that reports the execution time. 
    ''' 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        start = time.time() 
        result = func(*args, **kwargs) 
        end = time.time() 
        print(func.__name__, end-start) 
        return result 
    return wrapper


@timethis 
def countdown(n): 
	''' 
	Counts down 
	''' 
	while n > 0: 
	n -= 1

countdown(10000)

# countdown 0.004553794860839844

在 wrapper 函数中需要调用被装饰的函数 func 作为输入,并返回其结果。新创建的 wrapper 函数会作为装饰器的结果返回,并取代 func 的函数。

装饰器一般来说不会修改调用签名,也不会修改被包装函数返回的结果。这里对*args 和**kwargs 的使用是为了确保可以接受任何形式的输入参数。装饰器的返回值几乎总是同调用 func(*args, **kwargs) 的结果一致。

@wraps(func) 可以用来保存函数的元数据。每当定义一个装饰器时,应该总是记得为底层的包装函数添加 functools 库中的 @wraps 装饰器。否则会丢失如函数名、文档字符串、函数注解以及调用签名的元数据。

print(countdown.__name__)
print(countdown.__doc__)
print(countdown.__annotations__)
"""
countdown 0.049870967864990234
countdown
    Counts down
{}
"""

@wraps 装饰器的一个重要特性就是可以通过__wrapped__属性来访问被包装的函数

@timethis 
def countdown(n): 
	''' 
	Counts down 
	''' 
	print(123)
	while n > 0: 
	n -= 1

countdown(10000)
print(countdown.__wrapped__(1000000))
"""
123
None
"""

__wrapped__属性的存在同样使得装饰器函数可以合适地将底层被包装函数的签名暴露出来。

from inspect import signature
print(signature(countdown))
"""
(n)
"""

装饰器参数设置

可接受参数的装饰器

from functools import wraps
import logging
logging.basicConfig(level=logging.DEBUG)

def logged(level, name=None, message=None):
    def decorate(func):
        logname = name if name else func.__module__ 
        log = logging.getLogger(logname) 
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
    return decorate

@logged(logging.DEBUG)
def add(x, y):
    return x + y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

if __name__ == '__main__':
    add(1,2)
    spam()

logged 装饰器可以接受三个参数:levelname 和 message,是一个嵌套的装饰器,最外层的装饰器 decorate 返回内部函数 wrapperwrapper 函数包装了原始的被装饰函数 func,并在函数调用时添加了日志记录的功能。

最后应用的是 @logged(logging.DEBUG) 装饰器,将 add 函数包装在 logged(logging.DEBUG)(add) 中,:

add = logged(logging.DEBUG)(add)

在这个过程中,首先执行内部的 decorate 函数,返回一个内部函数 wrapper,@wraps 被应用在 wrapper 函数上,用于保留被装饰函数的元信息(如函数名、文档字符串等),从而避免由于装饰器改变函数签名而导致的问题。@wraps(func) 装饰器相当于执行了以下代码:

wrapper = wraps(func)(wrapper)

wraps(func) 返回一个装饰器函数,该函数接受一个函数 func 作为参数,并返回一个装饰器,用于将原始函数的元信息复制到包装函数中。然后将 wrapper 函数作为参数传递给装饰器函数,相当于执行了以下代码:

wrapper = decorate(wrapper)

在 decorate 函数中,wrapper 函数被包装在一个新的函数中并返回,因此,在最终的结果中,被装饰的函数 add 和 spam 的元信息被保留下来,并根据传入的参数创建了一个日志记录器 log,然后返回一个内部函数 wrapper

wrapper 函数通过调用 log.log(level, logmsg) 记录日志信息,然后再调用原始函数 func(*args, **kwargs),并返回函数的执行结果。

比较好理解的一种方法是逆推回去,add()logged() 装饰,logged() 返回了 decorate()decorate() 返回了 wrapper()
wrapper()wraps() 装饰,构成了完整的装饰链
image

可修改参数的装饰器

前置知识:

  • nonlocal 关键字用于在函数内部访问嵌套作用域中的变量,并将其标记为非局部变量。在 Python 中,函数内部可以定义嵌套函数,嵌套函数可以访问外部函数的变量,但是不能对其进行修改。如果需要修改外部函数的变量,可以使用 nonlocal 关键字将其标记为非局部变量,从而在内部函数中对其进行修改。
  • partial 也叫偏函数,该函数用于创建一个新的函数对象,它是一个原函数的包装,可以在调用时指定部分参数,从而生成一个新的函数。具体来说,partial 函数接受一个函数对象和一些参数,返回一个新的函数对象,这个新的函数对象可以像原函数一样被调用,但是它在调用时会自动填充部分参数。这个新的函数对象仍然可以被当做一个函数来使用,也可以被当做一个可调用对象来使用。
  • setattr 函数用于给一个对象动态绑定一个属性,即给对象添加一个新的属性或者修改一个已有的属性的值。具体来说,setattr 函数接受三个参数,一个是对象,一个是属性名称,一个是属性值。它会将属性名称和属性值绑定到对象上,从而动态地添加一个新的属性或者修改一个已有的属性的值。
from functools import wraps, partial
import logging 
 
logging.basicConfig(level=logging.DEBUG) 

def attach_wrapper(obj, func=None): 
    if func is None: 
        return partial(attach_wrapper, obj) # 此处返回偏函数
    setattr(obj, func.__name__, func) 
    return func

def logged(level, name=None, message=None):
    def decorate(func): 
        logname = name if name else func.__module__ 
        log = logging.getLogger(logname) 
        logmsg = message if message else func.__name__ 
 
        @wraps(func) 
        def wrapper(*args, **kwargs): 
            log.log(level, logmsg) 
            return func(*args, **kwargs)
    
        @attach_wrapper(wrapper)
        def set_level(newlevel): 
            nonlocal level 
            level = newlevel
        
        @attach_wrapper(wrapper) 
        def set_message(newmsg): 
            nonlocal logmsg 
            logmsg = newmsg
        
        return wrapper
    return decorate

if __name__ == '__main__':
    @logged(logging.DEBUG) 
    def add(x, y): 
        return x + y
   
    add(2, 3) 
    add.set_message('Add called') 
    add(2, 3) 
    add.set_level(logging.WARNING) 
    add(2, 3)
"""
DEBUG:__main__:add
DEBUG:__main__:Add called
WARNING:__main__:Add called
"""

attach_wrapper 函数是一个通用的装饰器,它可以将一个函数绑定到另一个对象上(即被装饰的函数 set_level 和 set_message),从而实现动态属性绑定的功能。该函数接受两个参数,一个是对象 obj,另一个是函数 func,如果 func 参数为空,则返回一个偏函数,否则将 func 函数绑定到对象 obj 上,并返回 func 函数。在绑定函数时,使用 setattr 函数将函数名和函数对象绑定到对象 obj 上,从而实现动态属性绑定的功能。

set_message set_level 两个函数首先使用 @attach_wrapper(wrapper) 装饰器将它们绑定到 wrapper 函数上,从而可以通过 wrapper 函数的属性来调用它们。然后通过使用 nonlocal 关键字声明 level 和 logmsg 变量,使得在函数内部可以访问 wrapper 函数的变量。最后在函数内部修改 level 和 logmsg 变量的值,从而动态修改日志记录器的级别和日志信息。这样在下一次调用被装饰的函数时,就会使用新的日志级别和日志信息来输出日志。

可选参数的装饰器

from functools import wraps, partial 
import logging 
logging.basicConfig(level=logging.DEBUG) 

def logged(func=None, *, level=logging.DEBUG, name=None, message=None): 
    if func is None: 
        return partial(logged, level=level, name=name, message=message) 
 
    logname = name if name else func.__module__ 
    log = logging.getLogger(logname) 
    logmsg = message if message else func.__name__ 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        log.log(level, logmsg) 
        return func(*args, **kwargs) 
    return wrapper 
 
# Example use 
@logged 
def add(x, y): 
    return x + y 
 
@logged(level=logging.CRITICAL, name='example') 
def spam(): 
    print('Spam!')

if __name__ == '__main__':
    add(1,2)
    spam()

在装饰器中,被包装的函数必须作为可选参数,其他的参数都要通过关键字来指定。此外当传递了参数后装饰器应该返回一个新函数,要包装的函数就作为参数传递给这个新函数,可以采用偏函数 partial()来解决。

装饰器强制检查函数参数类型

from inspect import signature 
from functools import wraps 
 
def typeassert(*ty_args, **ty_kwargs): 
    def decorate(func): 
        # If in optimized mode, disable type checking 
        if not __debug__:  # 
            return func 
 
        # Map function argument names to supplied types 
        sig = signature(func) 
        bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments # 使用签名的 bind_partial()方法来对提供的类型到参数名做部分绑定

        @wraps(func) 
        def wrapper(*args, **kwargs): 
            bound_values = sig.bind(*args, **kwargs)  # 参数的值绑定,
            # Enforce type assertions across supplied arguments 
            for name, value in bound_values.arguments.items():  # 如果参数的值不是绑定的类型,抛出异常
                 if name in bound_types:  # 这里其实只针对 bound_types 中提到的参数类型,如果未提供则不作用
                     if not isinstance(value, bound_types[name]): 
                       raise TypeError( 
                         'Argument {} must be {}'.format(name, bound_types[name]) 
                         ) 
            return func(*args, **kwargs) 
        return wrapper 
    return decorate
if not __debug__:  
	return func 

该部分代码禁止由装饰器添加的功能,通过设置全局变量__debug__为 False 让装饰器函数返回那个未经过包装的函数。

inspect.signature()函数允许从一个可调用对象中提取出参数签名信息,使用签名的 bind_partial()方法来对提供的类型到参数
名做部分绑定,在绑定中,缺失的参数被简单地忽略掉了

下面绑定过程中最重要的部分就是创建了有序字典 bound_types.arguments。这个字典将参数名以函数签名中相同的顺序映射到所提供的值上。在装饰器中,这个映射包含了打算强制施行的类型断言。

from inspect import signature

def spam(x, y, z=42): 
    pass

sig = signature(spam)
print(sig.parameters)
bound_types = sig.bind_partial(int, z=int)
print(bound_types.arguments)
"""
OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=42">)])
OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
"""

在由装饰器构建的包装函数中用到了 sig.bind()方法。bind()和 bind_partial()一样,只是它不允许出现缺失的参数。

bound_values = sig.bind(1, 2, 3)
print(bound_values.arguments)
"""
OrderedDict([('x', 1), ('y', 2), ('z', 3)])
"""

选择装饰器而不是函数注解的原因

@typeassert 
def spam(x:int, y, z:int = 42): 
    print(x,y,z)
  • 函数的每个参数只能赋予一个单独的注解
  • 装饰器可通用于任何函数

类与装饰器

在类中定义装饰器

在类中定义一个装饰器,并将其作用于其他的函数或者方法上。需要理清装饰器将以什么方式来应用,即是以实例方法还是以类方法的形式应用。

from functools import wraps 
 
class A: 
    # 实例方法
    def decorator1(self, func): 
        @wraps(func) 
        def wrapper(*args, **kwargs): 
            print('Decorator 1') 
            return func(*args, **kwargs) 
        return wrapper 
 
    # 类方法
    @classmethod 
    def decorator2(cls, func): 
        @wraps(func) 
        def wrapper(*args, **kwargs): 
             print('Decorator 2') 
             return func(*args, **kwargs) 
        return wrapper

if __name__ == "__main__":
    a = A()  # 需要先实例化
    @a.decorator1
    def spam(): 
        pass

    @A.decorator2 # 直接使用类名
    def grok():
        pass

标准库内建的装饰器@property 实际上是一个拥有 getter()、setter()和 deleter()方法的类,每个方法都可作为一个装饰器。

class Person: 
    # Create a property instance 
    first_name = property() 
 
    # Apply decorator methods 
    @first_name.getter 
    def first_name(self): 
        return self._first_name 
 
    @first_name.setter 
    def first_name(self, value): 
        if not isinstance(value, str): 
            raise TypeError('Expected a string') 
        self._first_name = value

定义这种形式的原因为多个装饰器方法都在操纵 property实例的状态。

针对 self 和 cls 参数,最外层的装饰器函数比如 decorator1()或 decorator2()需要提供一个 self 或 cls 参数(因为它们是类的一部分),但内层定义的包装函数一般不需要包含额外的参数。

继承问题
想把定义在类 A 中的装饰器施加于定义在子类 B 中的方法上

class B(A): 
    @A.decorator2 
    def bar(self): 
        pass

需要注意,装饰器必须定义为类方法,而且必须显式地给出父类 A 的名称而不是@B.decoator2。

装饰器定义为类

用装饰器来包装函数,希望得到的结果是一个可调用的实例。需要装饰器在类中和类外都能够使用,通过在类中实现__call__()和__get__()方法。

import types 
from functools import wraps

class Profiled:
    def __init__(self, func):
        wraps(func)(self)
        self.ncalls = 0
    
    def __call__(self, *args, **kwargs):
        self.ncalls += 1
        return self.__wrapped__(*args,**kwargs)
    
    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self,instance)

if __name__ == "__main__":
    @Profiled 
    def add(x, y): 
        return x + y 
    
    class Spam: 
        @Profiled 
        def bar(self, x): 
            print(self, x)
    
    print(add(1,2))
    print(add.ncalls)

    s = Spam()
    print(s.bar(1))
    print(s.bar(2))

"""
3
1
<__main__.Spam object at 0x000002DCF5340B20> 1
None
<__main__.Spam object at 0x000002DCF5340B20> 2
None
"""

对 functools.wraps()函数的使用和在普通装饰器中的目的一样,从被包装的函数中拷贝重要的元数据到可调用实例中。

每当函数实现的方法需要在类中进行查询时,作为描述符协议的一部分,__get__()方法都会被调用,在这种情况下,__get__()的目的是用来创建一个绑定方法对象(最终会给方法提供 self 参数)。type. MethodType()手动创建了一个绑定方法在这里使用。绑定方法只会在使用到实例的时候才创建。如果在类中访问该方法,__get__()的 instance 参数就设为 None,直接返回 Profiled 实例本身。通过这样方式可以获取实例的 ncalls 属性。

可以使用闭包和 nonlocal 变量代替,以函数属性的形式访问 ncalls

import types 
from functools import wraps 
 
def profiled(func): 
    ncalls = 0 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        nonlocal ncalls 
        ncalls += 1 
        return func(*args, **kwargs) 
    wrapper.ncalls = lambda: ncalls 
    return wrapper 
 
# Example 
@profiled 
def add(x, y): 
    return x + y

装饰器作用到类和静态方法

@classmethod 和@staticmethod 并不会实际创建可直接调用的对象。相反它们创建的是特殊的描述符对象。如果尝试在另一个装饰器中像函数那样使用它们,装饰器就会崩溃。因此需要确保确保这些装饰器出现在 @classmethod 和@staticmethod 之前。

import time 
from functools import wraps 
 
# A simple decorator 
def timethis(func): 
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        start = time.time() 
        r = func(*args, **kwargs) 
        end = time.time() 
        print(end-start) 
        return r 
    return wrapper 
 
# Class illustrating application of the decorator to different kinds of methods 
class Spam: 
    @timethis 
    def instance_method(self, n): 
        print(self, n) 
        while n > 0: 
            n -= 1 
 
    @classmethod 
    @timethis 
    def class_method(cls, n): 
        print(cls, n) 
        while n > 0: 
            n -= 1
            
    @staticmethod 
    @timethis 
    def static_method(n): 
        print(n) 
        while n > 0: 
            n -= 1

由装饰器的顺序可知,装饰顺序为由下而上,因此会先使用 timethis 装饰,之后使用 classmethod 和 staticmethod。

参考:《Python Cookbook 第三版》

posted @ 2023-07-18 14:47  JICEY  阅读(24)  评论(0编辑  收藏  举报