不懂装饰器,就不是真正会 Python

"\u003Cimg src=\"http:\u002F\u002Fp1.pstatp.com\u002Flarge\u002Fpgc-image\u002FRS8VXcPEb5aC9S\" img_width=\"960\" img_height=\"540\" alt=\"不懂装饰器,就不是真正会 Python\" inline=\"0\"\u003E\u003Cp\u003E文 | piglei@piglei 公众号 \u003C\u002Fp\u003E\u003Cp\u003E编辑 | EarlGrey\u003C\u002Fp\u003E\u003Cp\u003E推荐 | 编程派公众号\u003C\u002Fp\u003E\u003Cp\u003E装饰器(Decorator) 是 Python 里的一种特殊工具,它为我们提供了一种在函数外部修改函数的灵活能力。它有点像一顶画着独一无二 \u003Ccode\u003E@\u003C\u002Fcode\u003E符号的神奇帽子,只要将它戴在函数头顶上,就能悄无声息的改变函数本身的行为。\u003C\u002Fp\u003E\u003Cp\u003E你可能已经和装饰器打过不少交道了。在做面向对象编程时,我们就经常会用到 \u003Ccode\u003E@staticmethod\u003C\u002Fcode\u003E和\u003Ccode\u003E@classmethod\u003C\u002Fcode\u003E两个内置装饰器。此外,如果你接触过 click 模块,就更不会对装饰器感到陌生。click 最为人所称道的参数定义接口\u003Ccode\u003E@click.option(...)\u003C\u002Fcode\u003E就是利用装饰器实现的。\u003C\u002Fp\u003E\u003Cp\u003E除了用装饰器,我们也经常需要自己写一些装饰器。在这篇文章里,我将从 \u003Ccode\u003E最佳实践\u003C\u002Fcode\u003E和\u003Ccode\u003E常见错误\u003C\u002Fcode\u003E两个方面,来与你分享有关装饰器的一些小知识。\u003C\u002Fp\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003Ch2\u003E最佳实践\u003C\u002Fh2\u003E\u003Ch3\u003E1. 尝试用类来实现装饰器\u003C\u002Fh3\u003E\u003Cp\u003E绝大多数装饰器都是基于函数和 闭包 实现的,但这并非制造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器( \u003Ccode\u003E@decorator\u003C\u002Fcode\u003E)形式使用只有一个要求:\u003Cstrong\u003Edecorator 必须是一个“可被调用(callable)的对象\u003C\u002Fstrong\u003E。\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 使用 callable 可以检测某个对象是否“可被调用”\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E>>> def foo: pass\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E...\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E>>> type(foo)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E<class 'function'>\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E>>> callable(foo)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003ETrue\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E函数自然是“可被调用”的对象。但除了函数外,我们也可以让任何一个类(class)变得“可被调用”(callable)。办法很简单,只要自定义类的 \u003Ccode\u003E__call__\u003C\u002Fcode\u003E魔法方法即可。\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eclass Foo:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef __call__(self):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(\"Hello, __call___\")\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Efoo = Foo\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# OUTPUT: True\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(callable(foo))\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 调用 foo 实例\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# OUTPUT: Hello, __call__\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Efoo\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E基于这个特性,我们可以很方便的使用类来实现装饰器。\u003C\u002Fp\u003E\u003Cp\u003E下面这段代码,会定义一个名为 \u003Ccode\u003E@delay(duration)\u003C\u002Fcode\u003E的装饰器,使用它装饰过的函数在每次执行前,都会等待额外的\u003Ccode\u003Eduration\u003C\u002Fcode\u003E秒。同时,我们也希望为用户提供无需等待马上执行的\u003Ccode\u003Eeager_call\u003C\u002Fcode\u003E接口。\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eimport time\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eimport functools\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eclass DelayFunc:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef __init__(self, duration, func):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eself.duration = duration\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eself.func = func\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef __call__(self, *args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(f'Wait for {self.duration} seconds...')\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Etime.sleep(self.duration)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn self.func(*args, **kwargs)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef eager_call(self, *args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint('Call without delay')\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn self.func(*args, **kwargs)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef delay(duration):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 此处为了避免定义额外函数,直接使用 functools.partial 帮助构造\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# DelayFunc 实例\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn functools.partial(DelayFunc, duration)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E如何使用装饰器的样例代码:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@delay(duration=2)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef a\u003Ci class=\"chrome-extension-mutihighlight chrome-extension-mutihighlight-style-2\"\u003Edd\u003C\u002Fi\u003E(a, b):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn a + b\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 这次调用将会延迟 2 秒\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ea\u003Ci class=\"chrome-extension-mutihighlight chrome-extension-mutihighlight-style-2\"\u003Edd\u003C\u002Fi\u003E(1, 2)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 这次调用将会立即执行\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ea\u003Ci class=\"chrome-extension-mutihighlight chrome-extension-mutihighlight-style-2\"\u003Edd\u003C\u002Fi\u003E.eager_call(1, 2)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003Ccode\u003E@delay(duration)\u003C\u002Fcode\u003E就是一个基于类来实现的装饰器。当然,如果你非常熟悉 Python 里的函数和闭包,上面的\u003Ccode\u003Edelay\u003C\u002Fcode\u003E装饰器其实也完全可以只用函数来实现。所以,为什么我们要用类来做这件事呢?\u003C\u002Fp\u003E\u003Cp\u003E与纯函数相比,我觉得使用类实现的装饰器在\u003Cstrong\u003E特定场景\u003C\u002Fstrong\u003E下有几个优势:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E实现有状态的装饰器时,操作类属性比操作闭包内变量更符合直觉、不易出错\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E实现为函数扩充接口的装饰器时,使用类包装函数,比直接为函数对象追加属性更易于维护\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E更容易实现一个同时兼容装饰器与上下文管理器协议的对象(参考 unitest.mock.patch)\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003Ch3\u003E2. 使用 wrapt 模块编写更扁平的装饰器\u003C\u002Fh3\u003E\u003Cp\u003E在写装饰器的过程中,你有没有碰到过什么不爽的事情?不管你有没有,反正我有。我经常在写代码的时候,被下面两件事情搞得特别难受:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E比如,在下面的例子里,我实现了一个生成随机数并注入为函数参数的装饰器。\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eimport random\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef provide_number(min_num, max_num):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"装饰器:随机生成一个在 [min_num, max_num] 范围的整数,追加为函数的第一个位置参数\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef wrapper(func):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef decorated(*args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Enum = random.randint(min_num, max_num)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 将 num 作为第一个参数追加后调用函数\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn func(num, *args, **kwargs)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn decorated\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn wrapper\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@provide_number(1, 100)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef print_random_number(num):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(num)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 输出 1-100 的随机整数\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# OUTPUT: 72\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint_random_number\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003Ccode\u003E@provide_number\u003C\u002Fcode\u003E装饰器功能看上去很不错,但它有着我在前面提到的两个问题:\u003Cstrong\u003E嵌套层级深、无法在类方法上使用。\u003C\u002Fstrong\u003E如果直接用它去装饰类方法,会出现下面的情况:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eclass Foo:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@provide_number(1, 100)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef print_random_number(self, num):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(num)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# OUTPUT: <__main__.Foo object at 0x104047278>\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003EFoo.print_random_number\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003Ccode\u003EFoo\u003C\u002Fcode\u003E类实例中的\u003Ccode\u003Eprint_random_number\u003C\u002Fcode\u003E方法将会输出类实例\u003Ccode\u003Eself\u003C\u002Fcode\u003E,而不是我们期望的随机数\u003Ccode\u003Enum\u003C\u002Fcode\u003E。\u003C\u002Fp\u003E\u003Cp\u003E之所以会出现这个结果,是因为类方法(method)和函数(function)二者在工作机制上有着细微不同。如果要修复这个问题, \u003Ccode\u003Eprovider_number\u003C\u002Fcode\u003E装饰器在修改类方法的位置参数时,必须聪明的跳过藏在\u003Ccode\u003E*args\u003C\u002Fcode\u003E里面的类实例\u003Ccode\u003Eself\u003C\u002Fcode\u003E变量,才能正确的将\u003Ccode\u003Enum\u003C\u002Fcode\u003E作为第一个参数注入。\u003C\u002Fp\u003E\u003Cp\u003E这时,就应该是 wrapt 模块闪亮登场的时候了。\u003Ccode\u003Ewrapt\u003C\u002Fcode\u003E模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造\u003Ccode\u003Eprovide_number\u003C\u002Fcode\u003E装饰器,完美解决“嵌套层级深”和“无法通用”两个问题,\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eimport wrapt\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef provide_number(min_num, max_num):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@wrapt.decorator\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef wrapper(wrapped, instance, args, kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 参数含义:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E#\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - wrapped:被装饰的函数或类方法\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - instance:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - 如果被装饰者为普通类方法,该值为类实例\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - 如果被装饰者为 classmethod 类方法,该值为类\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - 如果被装饰者为类\u002F函数\u002F静态方法,该值为 None\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E#\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - args:调用时的位置参数(注意没有 * 符号)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# - kwargs:调用时的关键字参数(注意没有 ** 符号)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E#\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Enum = random.randint(min_num, max_num)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 无需\u003Ci class=\"chrome-extension-mutihighlight chrome-extension-mutihighlight-style-1\"\u003E关注\u003C\u002Fi\u003E wrapped 是类方法或普通函数,直接在头部追加参数\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eargs = (num,) + args\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn wrapped(*args, **kwargs)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn wrapper\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E<... 应用装饰器部分代码省略 ...>\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# OUTPUT: 48\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003EFoo.print_random_number\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E使用 \u003Ccode\u003Ewrapt\u003C\u002Fcode\u003E模块编写的装饰器,相比原来拥有下面这些优势:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E嵌套层级少:使用 \u003Ccode\u003E@wrapt.decorator\u003C\u002Fcode\u003E可以将两层嵌套减少为一层\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E更灵活:针对 \u003Ccode\u003Einstance\u003C\u002Fcode\u003E值进行条件判断后,更容易让装饰器变得通用\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003Ch2\u003E常见错误\u003C\u002Fh2\u003E\u003Ch3\u003E1. “装饰器”并不是“装饰器模式”\u003C\u002Fh3\u003E\u003Cp\u003E“设计模式”是一个在计算机世界里鼎鼎大名的词。假如你是一名 Java 程序员,而你一点设计模式都不懂,那么我打赌你找工作的面试过程一定会度过的相当艰难。\u003C\u002Fp\u003E\u003Cp\u003E但写 Python 时,我们极少谈起“设计模式”。虽然 Python 也是一门支持面向对象的编程语言,但它的 鸭子类型 设计以及出色的动态特性决定了,大部分设计模式对我们来说并不是必需品。所以,很多 Python 程序员在工作很长一段时间后,可能并没有真正应用过几种设计模式。\u003C\u002Fp\u003E\u003Cp\u003E不过 “装饰器模式(Decorator Pattern)” 是个例外。因为 Python 的“装饰器”和“装饰器模式”有着一模一样的名字,我不止一次听到有人把它们俩当成一回事,认为使用“装饰器”就是在实践“装饰器模式”。但事实上,\u003Cstrong\u003E它们是两个完全不同的东西。\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E“装饰器模式”是一个完全基于“面向对象”衍生出的编程手法。它拥有几个关键组成:\u003Cstrong\u003E一个统一的接口定义\u003C\u002Fstrong\u003E、\u003Cstrong\u003E若干个遵循该接口的类\u003C\u002Fstrong\u003E、\u003Cstrong\u003E类与类之间一层一层的包装\u003C\u002Fstrong\u003E。最终由它们共同形成一种“装饰”的效果。\u003C\u002Fp\u003E\u003Cp\u003E而 Python 里的“装饰器”和“面向对象”没有任何直接联系,\u003Cstrong\u003E它完全可以只是发生在函数和函数间的把戏。\u003C\u002Fstrong\u003E事实上,“装饰器”并没有提供某种无法替代的功能,它仅仅就是一颗“语法糖”而已。下面这段使用了装饰器的代码:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@log_time\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@cache_result\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef foo: pass\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E基本完全等同于下面这样:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef foo: pass\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Efoo = log_time(cache_result(foo))\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003Cstrong\u003E装饰器最大的功劳,在于让我们在某些特定场景时,可以写出更符合直觉、易于阅读的代码\u003C\u002Fstrong\u003E。它只是一颗“糖”,并不是某个面向对象领域的复杂编程模式。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003EHint: 在 Python 官网上有一个 实现了装饰器模式的例子,你可以读读这个例子来更好的了解它。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003Ch3\u003E2. 记得用 functools.wraps 装饰内层函数\u003C\u002Fh3\u003E\u003Cp\u003E下面是一个简单的装饰器,专门用来打印函数调用耗时:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eimport time\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef timer(wrapped):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"装饰器:记录并打印函数耗时\"\"\"\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef decorated(*args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Est = time.time\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eret = wrapped(*args, **kwargs)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint('execution take: {} seconds'.format(time.time - st))\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn ret\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn decorated\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@timer\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef random_sleep:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"随机睡眠一小会\"\"\"\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Etime.sleep(random.random)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003Ccode\u003Etimer\u003C\u002Fcode\u003E装饰器虽然没有错误,但是使用它装饰函数后,函数的原始签名就会被破坏。也就是说你再也没办法正确拿到\u003Ccode\u003Erandom_sleep\u003C\u002Fcode\u003E函数的名称、文档内容了,所有签名都会变成内层函数\u003Ccode\u003Edecorated\u003C\u002Fcode\u003E的值:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(random_sleep.__name__)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 输出 'decorated'\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(random_sleep.__doc__)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 输出 None\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E这虽然只是个小问题,但在某些时候也可能会导致难以察觉的 bug。幸运的是,标准库 \u003Ccode\u003Efunctools\u003C\u002Fcode\u003E为它提供了解决方案,你只需要在定义装饰器时,用另外一个装饰器再装饰一下内层\u003Ccode\u003Edecorated\u003C\u002Fcode\u003E函数就行。\u003C\u002Fp\u003E\u003Cp\u003E听上去有点绕,但其实就是新增一行代码而已:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef timer(wrapped):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 将 wrapper 函数的真实签名赋值到 decorated 上\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@functools.wraps(wrapped)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef decorated(*args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# <...> 已省略\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn decorated\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E这样处理后, \u003Ccode\u003Etimer\u003C\u002Fcode\u003E装饰器就不会影响它所装饰的函数了。\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(random_sleep.__name__)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 输出 'random_sleep'\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(random_sleep.__doc__)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 输出 '随机睡眠一小会'\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003Ch3\u003E3. 修改外层变量时记得使用 nonlocal\u003C\u002Fh3\u003E\u003Cp\u003E装饰器是对函数对象的一个高级应用。在编写装饰器的过程中,你会经常碰到内层函数需要修改外层函数变量的情况。就像下面这个装饰器一样:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eimport functools\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef counter(func):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E\"\"\"装饰器:记录并打印调用次数\"\"\"\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ecount = 0\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@functools.wraps(func)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef decorated(*args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# 次数累加\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ecount += 1\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Eprint(f\"Count: {count}\")\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn func(*args, **kwargs)\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ereturn decorated\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E@counter\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef foo:\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Epass\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cbr\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Efoo\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E为了统计函数调用次数,我们需要在 \u003Ccode\u003Edecorated\u003C\u002Fcode\u003E函数内部修改外层函数定义的\u003Ccode\u003Ecount\u003C\u002Fcode\u003E变量的值。但是,上面这段代码是有问题的,在执行它时解释器会报错:\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003ETraceback (most recent call last):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003EFile \"counter.py\", line 22, in <module>\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Efoo\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003EFile \"counter.py\", line 11, in decorated\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ecount += 1\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003EUnboundLocalError: local variable 'count' referenced before assignment\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cp\u003E这个错误是由 \u003Ccode\u003Ecounter\u003C\u002Fcode\u003E与\u003Ccode\u003Edecorated\u003C\u002Fcode\u003E函数互相嵌套的作用域引起的。\u003C\u002Fp\u003E\u003Cp\u003E当解释器执行到 \u003Ccode\u003Ecount+=1\u003C\u002Fcode\u003E时,并不知道\u003Ccode\u003Ecount\u003C\u002Fcode\u003E是一个在外层作用域定义的变量,它把\u003Ccode\u003Ecount\u003C\u002Fcode\u003E当做一个局部变量,并在当前作用域内查找。最终却没有找到有关\u003Ccode\u003Ecount\u003C\u002Fcode\u003E变量的任何定义,然后抛出错误。\u003C\u002Fp\u003E\u003Cp\u003E为了解决这个问题,我们需要通过 \u003Ccode\u003Enonlocal\u003C\u002Fcode\u003E关键字告诉解释器:\u003Cstrong\u003E“count 变量并不属于当前的 local 作用域,去外面找找吧”\u003C\u002Fstrong\u003E,之前的错误就可以得到解决。\u003C\u002Fp\u003E\u003Cpre\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Edef decorated(*args, **kwargs):\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Enonlocal count\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003Ecount += 1\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Ccode\u003E# <... 已省略 ...>\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003C\u002Fpre\u003E\u003Cblockquote\u003E\u003Cp\u003EHint:如果要了解更多有关 nonlocal 关键字的历史,可以查阅 PEP-3104\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E\u003C\u002Fp\u003E\u003Ch2\u003E总结\u003C\u002Fh2\u003E\u003Cp\u003E在这篇文章里,我与你分享了有关装饰器的一些技巧与小知识。\u003C\u002Fp\u003E\u003Cp\u003E一些要点总结:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E一切 callable 的对象都可以被用来实现装饰器\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E混合使用函数与类,可以更好的实现装饰器\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003Ewrapt 模块很有用,用它可以帮助我们用更简单的代码写出复杂装饰器\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E“装饰器”只是语法糖,它不是“装饰器模式”\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E装饰器会改变函数的原始签名,你需要 \u003Ccode\u003Efunctools.wraps\u003C\u002Fcode\u003E\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E在内层函数修改外层函数的变量时,需要使用 \u003Ccode\u003Enonlocal\u003C\u002Fcode\u003E关键字\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧。\u003C\u002Fp\u003E\u003Cp\u003E回复下方\u003Cstrong\u003E「关键词」\u003C\u002Fstrong\u003E,获取优质资源\u003C\u002Fp\u003E\u003Cp\u003E回复关键词「 \u003Cstrong\u003Epybook03\u003C\u002Fstrong\u003E」,立即获取主页君与小伙伴一起翻译的《Think Python 2e》电子版\u003C\u002Fp\u003E\u003Cp\u003E回复关键词「\u003Cstrong\u003Epybooks02\u003C\u002Fstrong\u003E」,立即获取 O'Reilly 出版社推出的免费 Python 相关电子书合集\u003C\u002Fp\u003E\u003Cp\u003E回复关键词「\u003Cstrong\u003E书单02\u003C\u002Fstrong\u003E」,立即获取主页君整理的 10 本 Python 入门书的电子版\u003C\u002Fp\u003E\u003Cimg src=\"http:\u002F\u002Fp1.pstatp.com\u002Flarge\u002Fpgc-image\u002FRVJkQfkBd9DBuh\" img_width=\"960\" img_height=\"355\" alt=\"不懂装饰器,就不是真正会 Python\" inline=\"0\"\u003E\u003Cp\u003E你想要的 IT 电子资源,这里可能都有\u003C\u002Fp\u003E\u003Cp\u003EPython 或将超越 C、Java,成为最受欢迎的语言\u003C\u002Fp\u003E\u003Cp\u003EPython 容器使用的 5 个技巧和 2 个误区\u003C\u002Fp\u003E\u003Cp\u003E如何写出优雅的 Python 函数?\u003C\u002Fp\u003E\u003Cp\u003E题图:pexels,CC0 授权。\u003C\u002Fp\u003E"
发表评论
留言与评论(共有 0 条评论)
   
验证码:

相关文章

推荐文章

'); })();