zoukankan      html  css  js  c++  java
  • python装饰器1:函数装饰器详解

    先混个眼熟

    谁可以作为装饰器(可以将谁编写成装饰器):

    1. 函数
    2. 方法
    3. 实现了__call__的可调用类

    装饰器可以去装饰谁(谁可以被装饰):

    1. 函数
    2. 方法

    基础:函数装饰器的表现方式

    假如你已经定义了一个函数funcA(),在准备定义函数funcB()的时候,如果写成下面的格式:

    @funcA
    def funcB():...
    

    表示用函数funcA()装饰函数funcB()。当然,也可以认为是funcA包装函数funcB。它等价于:

    def funcB():...
    
    funcB = funcA(funcB)
    

    也就是说,将函数funcB作为函数funcA的参数,funcA会重新返回另一个可调用的对象(比如函数)并赋值给funcB。

    所以,funcA要想作为函数装饰器,需要接收函数作为参数,并且返回另一个可调用对象(如函数)。例如:

    def funcA(F):
        ...
        ...
        return Callable
    

    注意,函数装饰器返回的可调用对象并不一定是原始的函数F,可以是任意其它可调用对象,比如另一个函数。但最终,这个返回的可调用对象都会被赋值给被装饰的函数变量(上例中的funcB)。

    函数可以同时被多个装饰器装饰,后面的装饰器以前面的装饰器处理结果为基础进行处理:

    @decorator1
    @decorator2
    def func():...
    
    # 等价于
    func = decorator1(decorator2(func))
    

    当调用被装饰后的funcB时,将自动将funcB进行装饰,并调用装饰后的对象。所以,下面是等价的调用方式:

    funcB()          # 调用装饰后的funcB
    funcA(funcB)()
    

    了解完函数装饰器的表现后,大概也能猜到了,装饰器函数可以用来扩展、增强另外一个函数。实际上,内置函数中staticmethod()、classmethod()和property()都是装饰器函数,可以用来装饰其它函数,在后面会学到它们的用法。

    两个简单的例子

    例如,函数f()返回一些字符串,现在要将它的返回结果转换为大写字母。可以定义一个函数装饰器来增强函数f()。

    def toupper(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            return result.upper()
        return wrapper
    
    @toupper
    def f(x: str):    # 等价于f = toupper(f)
        return x
    
    res = f("abcd")
    print(res)
    

    上面toupper()装饰f()后,调用f("abcd")的时候,等价于执行toupper(f)("abcd"),参数"abcd"传递给装饰器中的wrapper()中的*args,在wrapper中又执行了f("abcd"),使得原本属于f()的整个过程都完整了,最后返回result.upper(),这部分是对函数f()的扩展部分。

    注意,上面的封装函数wrapper()中使用了*args **kwargs,是为了确保任意参数的函数都能正确执行下去。

    再比如要计算一个函数autodown()的执行时长,可以额外定义一个函数装饰器timecount()。

    import time
    
    # 函数装饰器
    def timecount(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            print(func.__name__, end - start)
            return result
        return wrapper
    
    # 装饰函数
    @timecount
    def autodown(n: int):
        while n > 0:
            n -= 1
    
    # 调用被装饰的函数
    autodown(100000)
    autodown(1000000)
    autodown(10000000)
    

    执行结果:

    autodown 0.004986763000488281
    autodown 0.05684685707092285
    autodown 0.5336081981658936
    

    上面wrapper()中的return是多余的,是因为这里装饰的autodown()函数自身没有返回值。但却不应该省略这个return,因为timecount()可以去装饰其它可能有返回值的函数。

    @functools.wraps

    前面的装饰器代码逻辑上没有什么问题,但是却存在隐藏的问题:函数的元数据信息丢了。比如doc、注解等。

    比如下面的代码:

    @timecount
    def autodown(n: int):
        ''' some docs '''
        while n > 0:
            n -= 1
    
    print(autodown.__name__)
    print(autodown.__doc__)
    print(autodown.__annotations__)
    

    执行结果为:

    wrapper
    None
    {}
    

    所以,必须要将被装饰函数的元数据保留下来。可以使用functools模块中的wraps()装饰一下装饰器中的wrapper()函数。如下:

    import time
    from functools import wraps
    
    def timecount(func):
        @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
    

    现在,再去查看autodown函数的元数据信息,将会得到被保留下来的内容:

    autodown
     some doc
    {'n': <class 'int'>}
    

    所以,wraps()的简单用法是:向wraps()中传递的func参数,那么func的元数据就会被保留下来。

    上面@wraps(func)装饰wrapper的过程等价于:

    def wrapper(*args, **kwargs):...
    wrapper = wraps(func)(wrapper)
    

    请注意这一点,因为在将类作为装饰器的时候,经常会在__init__(self, func)里这样使用:

    class cls:
        def __init__(self, func):
            wraps(func)(self)
            ...
        def __call__(self, *args, **kwargs):
            ...
    

    解除装饰

    函数被装饰后,如何再去访问未被装饰状态下的这个函数?@wraps还有一个重要的特性,可以通过被装饰对象的__wrapped__属性来直接访问被装饰对象。例如:

    autodown.__wrapped__(1000000)
    
    new_autodown = autodown.__wrapped__
    new_autodown(1000000)
    

    上面的调用不会去调用装饰后的函数,所以不会输出执行时长。

    注意,如果函数被多个装饰器装饰,那么通过__wrapped__,将只会解除第一个装饰过程。例如:

    @decorator1
    @decorator2
    @decorator3
    def f():...
    

    当访问f.__wrapped__()的时候,只有decorator1被解除,剩余的所有装饰器仍然有效。注意,python 3.3之前是略过所有装饰器。

    下面是一个多装饰的示例:

    from functools import wraps
    
    
    def decorator1(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("in decorator1")
            return func(*args, **kwargs)
        return wrapper
    
    
    def decorator2(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("in decorator2")
            return func(*args, **kwargs)
        return wrapper
    
    
    def decorator3(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("in decorator3")
            return func(*args, **kwargs)
        return wrapper
    
    
    @decorator1
    @decorator2
    @decorator3
    def addNum(x, y):
        return x+y
    

    返回结果:

    in decorator1
    in decorator2
    in decorator3
    5
    in decorator2
    in decorator3
    5
    

    如果不使用functools的@wraps的__wrapped__,想要手动去引用原始函数,需要做的工作可能会非常多。所以,如有需要,直接使用__wrapped__去调用未被装饰的函数比较好。

    另外,并不是所有装饰器中都使用了@wraps

    带参数的函数装饰器

    函数装饰器也是可以带上参数的。

    @decorator(x,y,z)
    def func():...
    

    它等价于:

    func = decorator(x,y,z)(func)
    

    它并不是"天生"就这样等价的,而是根据编码规范编写装饰器的时候,通常会这样。其实带参数的函数装饰器写起来有点绕:先定义一个带有参数的外层函数,它是外在的函数装饰器,这个函数内包含了真正的装饰器函数,而这个内部的函数装饰器的内部又包含了被装饰的函数封装。也就是函数嵌套了一次又一次。

    所以,结构大概是这样的:

    def out_decorator(some_args):
        ...SOME CODE...
        def real_decorator(func):
            ...SOME CODE...
            def wrapper(*args, **kwargs):
                ...SOME CODE WITH func...
            return wrapper
        return real_decorator
    
    # 等价于func = out_decorator(some_args)(func)
    @out_decorator(some_args)
    def func():...     
    

    下面是一个简单的例子:

    from functools import wraps
    
    def out_decorator(x, y, z):
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                print(x)
                print(y)
                print(z)
                return func(*args, **kwargs)
            return wrapper
        return decorator
    
    
    @out_decorator("xx", "yy", "zz")
    def addNum(x, y):
        return x+y
    
    print(addNum(2, 3))
    

    参数随意的装饰器

    根据前面介绍的两种情况,装饰器可以带参数、不带参数,所以有两种装饰的方式,要么是下面的(1),要么是下面的(2)。

    @decorator         # (1)
    @decorator(x,y,z)  # (2)
    

    所以,根据不同的装饰方式,需要编写是否带参数的不同装饰器。

    但是现在想要编写一个将上面两种参数方式统一起来的装饰器。

    可能第一想法是让装饰器参数默认化:

    def out_decorator(arg1=X, arg2=Y...):
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                ...
            return wrapper
        return decorator
    

    现在可以用下面两种方式来装饰:

    @out_decorator()
    @out_decorator(arg1,arg2)
    

    虽然上面两种装饰方式会正确进行,但这并非合理做法,因为下面这种最通用的装饰方式会错误:

    @out_decorator
    

    为了解决这个问题,回顾下前面装饰器是如何等价的:

    # 等价于 func = decorator(func)
    @decorator
    def func():...
    
    # 等价于 func = out_decorator(x, y, z)(func)
    @out_decorator(x, y, z)
    def func():...
    

    上面第二种方式中,out_decorator(x,y,z)才是真正返回的内部装饰器。所以,可以修改下装饰器的编写方式,将func也作为out_decorator()的其中一个参数:

    from functools import wraps,partial
    
    def decorator(func=None, arg1=X, arg2=Y):
        # 如果func为None,说明触发的带参装饰器
        # 直接返回partial()封装后的装饰器函数
        if func is None:
            decorator_new = partial(decorator, arg1=arg1, arg2=arg2)
            return decorator_new
            #return partial(decorator, arg1=arg1, arg2=arg2)
        
        # 下面是装饰器的完整装饰内容
        @wraps(func)
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    

    上面使用了functools模块中的partial()函数,它可以返回一个新的将某些参数"冻结"后的函数,使得新的函数无需指定这些已被"冻结"的参数,从而减少参数的数量。如果不知道这个函数,参考partial()用法说明

    现在,可以统一下面3种装饰方式:

    @decorator()
    @decorator(arg1=x,arg2=y)
    @decorator
    

    前两种装饰方式,等价的调用方式是decorator()(func)decorator(arg1=x,arg2=y)(func),它们的func都为None,所以都会通过partial()返回通常的装饰方式@decorator所等价的形式。

    需要注意的是,因为上面的参数结构中包含了func=None作为第一个参数,所以带参数装饰时,必须使用keyword格式来传递参数,不能使用位置参数。

    下面是一个简单的示例:

    from functools import wraps, partial
    
    
    def decorator(func=None, x=1, y=2, z=3):
        if func is None:
            return partial(decorator, x=x, y=y, z=z)
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("x: ", x)
            print("y: ", y)
            print("z: ", z)
            return func(*args, **kwargs)
        return wrapper
    

    下面3种装饰方式都可以:

    @decorator
    def addNum(a, b):
        return a + b
    print(addNum(2, 3))
    
    print("=" * 40)
    
    @decorator()
    def addNum(a, b):
        return a + b
    print(addNum(2, 3))
    
    print("=" * 40)
    
    # 必须使用关键字参数进行装饰
    @decorator(x="xx", y="yy", z="zz")
    def addNum(a, b):
        return a + b
    print(addNum(2, 3))
    

    返回结果:

    x:  1
    y:  2
    z:  3
    5
    ====================
    x:  1
    y:  2
    z:  3
    5
    ====================
    x:  xx
    y:  yy
    z:  zz
    5
    
  • 相关阅读:
    How To : OCR / Vote disk Maintenance Operations: (ADD/REMOVE/REPLACE/MOVE)
    循序渐进解读Oracle AWR性能分析报告
    为11gR2 Grid Infrastructure增加新的public网络
    12C开始oracle实现了SCALABLE LGWR多进程并行写redo log
    万字详解Oracle架构、原理、进程,学会世间再无复杂架构
    PowerShell 连接SQL Server 数据库
    Oracle 性能分析与诊断|跟踪诊断&优化SQL 语句
    安装oracle 11gr2 rac on solaris
    oracle EOS
    K8S集群认证之RBAC
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/10198247.html
Copyright © 2011-2022 走看看