zoukankan      html  css  js  c++  java
  • Python 装饰器(zz)

     

     

    Python 工匠:使用装饰器的技巧

    前言

    这是“Python 工匠系列的第篇文章。[查看系列所有文章]

    装饰器*DecoratorPython 里的一种特殊工具,它为我们提供了一种在函数外部修改函数的灵活能力。它有点像一顶画着独一无二 @ 符号的神奇帽子,只要将它戴在函数头顶上,就能悄无声息的改变函数本身的行为。

    你可能已经和装饰器打过不少交道了。在做面向对象编程时,我们就经常会用到 @staticmethod  @classmethod 两个内置装饰器。此外,如果你接触过 click 模块,就更不会对装饰器感到陌生。click 最为人所称道的参数定义接口 @click.option(...) 就是利用装饰器实现的。

    除了用装饰器,我们也经常需要自己写一些装饰器。在这篇文章里,我将从 最佳实践  常见错误 两个方面,来与你分享有关装饰器的一些小知识。

    最佳实践

    1. 尝试用类来实现装饰器

    绝大多数装饰器都是基于函数和 闭包 实现的,但这并非制造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器(@decorator)形式使用只有一个要求:decorator 必须是一个可被调用(callable)的对象

    使用callable 可以检测某个对象是否可被调用

    >>>deffoo(): pass

    ...

    >>>type(foo)

    <class'function'>

    >>>callable(foo)

    True

    函数自然是可被调用的对象。但除了函数外,我们也可以让任何一个类(class)变得可被调用callable)。办法很简单,只要自定义类的 __call__ 魔法方法即可。

    class Foo:

        def __call__(self):

            print("Hello, __call___")

     

    foo = Foo()

     

    # OUTPUT: True

    print(callable(foo))

    调用foo 实例

    # OUTPUT: Hello, __call__

    foo()

    基于这个特性,我们可以很方便的使用类来实现装饰器。

    下面这段代码,会定义一个名为 @delay(duration) 的装饰器,使用它装饰过的函数在每次执行前,都会等待额外的 duration 秒。同时,我们也希望为用户提供无需等待马上执行的 eager_call 接口。

    importtime

    importfunctools

     

     

    classDelayFunc:

        def__init__(self, duration, func):

            self.duration =duration

            self.func =func

     

        def__call__(self, *args, **kwargs):

            print(f'Wait for {self.duration}seconds...')

            time.sleep(self.duration)

            returnself.func(*args, **kwargs)

     

        defeager_call(self, *args, **kwargs):

            print('Call without delay')

            returnself.func(*args, **kwargs)

     

     

    defdelay(duration):

        """装饰器:推迟某个函数的执行。同时提供.eager_call 方法立即执行

        """

        此处为了避免定义额外函数,直接使用functools.partial 帮助构造

        # DelayFunc 实例

        returnfunctools.partial(DelayFunc, duration)

    如何使用装饰器的样例代码:

    @delay(duration=2)

    def add(a, b):

        return a + b

     

     

    这次调用将会延迟

    add(1, 2)

    这次调用将会立即执行

    add.eager_call(1, 2)

    @delay(duration) 就是一个基于类来实现的装饰器。当然,如果你非常熟悉Python 里的函数和闭包,上面的 delay装饰器其实也完全可以只用函数来实现。所以,为什么我们要用类来做这件事呢?

    与纯函数相比,我觉得使用类实现的装饰器在特定场景下有几个优势:

    • 实现有状态的装饰器时,操作类属性比操作闭包内变量更符合直觉、不易出错
    • 实现为函数扩充接口的装饰器时,使用类包装函数,比直接为函数对象追加属性更易于维护
    • 更容易实现一个同时兼容装饰器与上下文管理器协议的对象(参考 unitest.mock.patch

    2. 使用wrapt 模块编写更扁平的装饰器

    在写装饰器的过程中,你有没有碰到过什么不爽的事情?不管你有没有,反正我有。我经常在写代码的时候,被下面两件事情搞得特别难受:

    1. 实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读
    2. 因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上

    比如,在下面的例子里,我实现了一个生成随机数并注入为函数参数的装饰器。

    importrandom

     

     

    defprovide_number(min_num, max_num):

        """装饰器:随机生成一个在[min_num, max_num] 范围的整数,追加为函数的第一个位置参数

        """

        defwrapper(func):

            defdecorated(*args, **kwargs):

                num =random.randint(min_num, max_num)

                num 作为第一个参数追加后调用函数

                returnfunc(num, *args, **kwargs)

            returndecorated

        returnwrapper

        

     

     

    @provide_number(1100)

    defprint_random_number(num):

        print(num)

     

    输出1-100 的随机整数

    # OUTPUT: 72

    print_random_number()

    @provide_number 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:**嵌套层级深、无法在类方法上使用。**如果直接用它去装饰类方法,会出现下面的情况:

    class Foo:

        @provide_number(1, 100)

        def print_random_number(self, num):

            print(num)

     

    # OUTPUT: <__main__.Foo object at 0x104047278>

    Foo().print_random_number()

    Foo 类实例中的 print_random_number 方法将会输出类实例 self ,而不是我们期望的随机数 num

    之所以会出现这个结果,是因为类方法*method和函数function*二者在工作机制上有着细微不同。如果要修复这个问题,provider_number 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 *args 里面的类实例 self 变量,才能正确的将 num 作为第一个参数注入。

    这时,就应该是 wrapt 模块闪亮登场的时候了。wrapt 模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造 provide_number 装饰器,完美解决*“嵌套层级深无法通用”*两个问题,

    importwrapt

     

    defprovide_number(min_num, max_num):

        @wrapt.decorator

        defwrapper(wrapped, instance, args, kwargs):

            参数含义:

            #

            # - wrapped:被装饰的函数或类方法

            # - instance

            #   -如果被装饰者为普通类方法,该值为类实例

            #   -如果被装饰者为classmethod 类方法,该值为类

            #   -如果被装饰者为类/函数/静态方法,该值为None

            #

            # - args:调用时的位置参数(注意没有符号)

            # - kwargs:调用时的关键字参数(注意没有** 符号)

            #

            num =random.randint(min_num, max_num)

            无需关注wrapped 是类方法或普通函数,直接在头部追加参数

            args =(num,) +args

            returnwrapped(*args, **kwargs)

        returnwrapper

        

    <...应用装饰器部分代码省略...>

        

    # OUTPUT: 48

    Foo().print_random_number()

    使用 wrapt 模块编写的装饰器,相比原来拥有下面这些优势:

    • 嵌套层级少:使用 @wrapt.decorator 可以将两层嵌套减少为一层
    • 更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况
    • 更灵活:针对 instance 值进行条件判断后,更容易让装饰器变得通用

    常见错误

    1. “装饰器并不是装饰器模式

    设计模式是一个在计算机世界里鼎鼎大名的词。假如你是一名Java 程序员,而你一点设计模式都不懂,那么我打赌你找工作的面试过程一定会度过的相当艰难。

    但写Python 时,我们极少谈起设计模式。虽然Python 也是一门支持面向对象的编程语言,但它的 鸭子类型 设计以及出色的动态特性决定了,大部分设计模式对我们来说并不是必需品。所以,很多Python 程序员在工作很长一段时间后,可能并没有真正应用过几种设计模式。

    不过 装饰器模式(Decorator Pattern 是个例外。因为Python 装饰器装饰器模式有着一模一样的名字,我不止一次听到有人把它们俩当成一回事,认为使用装饰器就是在实践装饰器模式。但事实上,它们是两个完全不同的东西。

    装饰器模式是一个完全基于面向对象衍生出的编程手法。它拥有几个关键组成:一个统一的接口定义若干个遵循该接口的类类与类之间一层一层的包装。最终由它们共同形成一种*“装饰”*的效果。

    Python 里的装饰器面向对象没有任何直接联系,**它完全可以只是发生在函数和函数间的把戏。**事实上,装饰器并没有提供某种无法替代的功能,它仅仅就是一颗语法糖而已。下面这段使用了装饰器的代码:

    @log_time

    @cache_result

    deffoo(): pass

    基本完全等同于下面这样:

    def foo(): pass

     

    foo = log_time(cache_result(foo))

    装饰器最大的功劳,在于让我们在某些特定场景时,可以写出更符合直觉、易于阅读的代码。它只是一颗,并不是某个面向对象领域的复杂编程模式。

    Hint: Python 官网上有一个 实现了装饰器模式的例子,你可以读读这个例子来更好的了解它。

    2. 记得用functools.wraps() 装饰内层函数

    下面是一个简单的装饰器,专门用来打印函数调用耗时:

    importtime

     

     

    deftimer(wrapped):

        """装饰器:记录并打印函数耗时"""

        defdecorated(*args, **kwargs):

            st =time.time()

            ret =wrapped(*args, **kwargs)

            print('execution take: {}seconds'.format(time.time() -st))

            returnret

        returndecorated

     

     

    @timer

    defrandom_sleep():

        """随机睡眠一小会"""

        time.sleep(random.random())

    timer 装饰器虽然没有错误,但是使用它装饰函数后,函数的原始签名就会被破坏。也就是说你再也没办法正确拿到 random_sleep 函数的名称、文档内容了,所有签名都会变成内层函数 decorated 的值:

    print(random_sleep.__name__)

    输出'decorated'

    print(random_sleep.__doc__)

    输出None

    这虽然只是个小问题,但在某些时候也可能会导致难以察觉的bug。幸运的是,标准库 functools 为它提供了解决方案,你只需要在定义装饰器时,用另外一个装饰器再装饰一下内层 decorated 函数就行。

    听上去有点绕,但其实就是新增一行代码而已:

    deftimer(wrapped):

        wrapper 函数的真实签名赋值到decorated 

        @functools.wraps(wrapped)

        defdecorated(*args, **kwargs):

            # <...> 已省略

        returndecorated

    这样处理后,timer 装饰器就不会影响它所装饰的函数了。

    print(random_sleep.__name__)

    输出'random_sleep'

    print(random_sleep.__doc__)

    输出'随机睡眠一小会'

    3. 修改外层变量时记得使用nonlocal

    装饰器是对函数对象的一个高级应用。在编写装饰器的过程中,你会经常碰到内层函数需要修改外层函数变量的情况。就像下面这个装饰器一样:

    importfunctools

     

    defcounter(func):

        """装饰器:记录并打印调用次数"""

        count =0

        @functools.wraps(func)

        defdecorated(*args, **kwargs):

            次数累加

            count +=1

            print(f"Count: {count}")

            returnfunc(*args, **kwargs)

        returndecorated

     

    @counter

    deffoo():

        pass

     

    foo()

    为了统计函数调用次数,我们需要在 decorated 函数内部修改外层函数定义的 count 变量的值。但是,上面这段代码是有问题的,在执行它时解释器会报错:

    Traceback (most recent call last):

      File "counter.py", line 22, in <module>

        foo()

      File "counter.py", line 11, in decorated

        count += 1

    UnboundLocalError: local variable 'count' referenced before assignment

    这个错误是由 counter  decorated 函数互相嵌套的作用域引起的。

    当解释器执行到 count += 1 时,并不知道 count 是一个在外层作用域定义的变量,它把 count 当做一个局部变量,并在当前作用域内查找。最终却没有找到有关 count 变量的任何定义,然后抛出错误。

    为了解决这个问题,我们需要通过 nonlocal 关键字告诉解释器:“count 变量并不属于当前的local 作用域,去外面找找吧,之前的错误就可以得到解决。

    defdecorated(*args, **kwargs):

        nonlocalcount

        count +=1

        # <... 已省略...>

    Hint:如果要了解更多有关nonlocal 关键字的历史,可以查阅 PEP-3104

    总结

    在这篇文章里,我与你分享了有关装饰器的一些技巧与小知识。

    一些要点总结:

    • 一切callable 的对象都可以被用来实现装饰器
    • 混合使用函数与类,可以更好的实现装饰器
    • wrapt 模块很有用,用它可以帮助我们用更简单的代码写出复杂装饰器
    • “装饰器”只是语法糖,它不是“装饰器模式”
    • 装饰器会改变函数的原始签名,你需要 functools.wraps
    • 在内层函数修改外层函数的变量时,需要使用 nonlocal 关键字

    看完文章的你,有没有什么想吐槽的?请留言或者在 项目Github Issues 告诉我吧。

    >>>下一篇【9.一个关于模块的小故事】

    <<<上一篇【7.编写地道循环的两个建议】

     

  • 相关阅读:
    虚拟机VMware 8安装Linux系统详细过程图解
    NSMutableArray 对象的一些方法
    NSString类中 哦见过的方法
    c语言的 地址运算符 & 和 指针运算符* 的区别
    const 静态的变量
    float scale = [[UIScreen mainScreen] scale]
    core Data 中 TIMESTAMP 类型
    NSPredicate
    xcode中解决 nslog 控制台 显示不出来
    按字符串读取文件内容NSString stringWithContentsOfFile: (NSString类方法)
  • 原文地址:https://www.cnblogs.com/cx2016/p/12926213.html
Copyright © 2011-2022 走看看