我曾经在 科里定律的变量 ( 一 ) - 缘起,透视严重的变量稀缺问题 中提到,认识科里定律是通过一篇名为 关于 Python 装饰器,你应该知道的知识
这篇文章还是挺有意思的,索性我就把它翻译为中文吧
Python 装饰器是一个强大的概念,允许我们使用一个函数 「 包装 」 另一个函数
除了正常的职责之外,装饰器的另类使用想法是抽象出你想要一个功能或类做的东西,这可能有很多原因,例如 代码重用 和坚持 科里原则
通过学习如何编写自己的装饰器,我们可以显着提高自己代码的可读性,因为它们可以更改函数的行为方式,而无需实际更改代码 ( 例如添加日志记录行 )
它们是 Python 中相当常用的工具,对于使用诸如 flask
或click
之类的框架的人来说很熟悉
虽然很多人只知道如何使用它们,而不知道如何编写自己的装饰器
这篇文章是由朋友 @Timber 带给我们的客串,如果你有兴趣为我们写作,请随时在 Twitter 上与我们联系
它 ( 装饰器 ) 怎样工作 ?
首先,让我们在 Python
中展示一个装饰器的例子,这是一个非常基本的装饰器的例子
@my_decorator def hello(): print('hello')
当我们在 Python
中定义函数时,该函数将成为一个对象,也就是说,Python 中,任何函数都是一个对象,可调用的对象
上面的函数 hello
是一个函数对象,@my_decorator
实际上是一个能够使用 hello
对象并将另一个对象返回给解释器的函数
装饰器返回的对象就是所谓的 hello
。从本质上讲,它就像你要编写自己的普通函数一样,例如 hello = decorate ( hello )
装饰可以接收一个函数作为参数 - 它可以使用任何它想要的 - 然后返回另一个对象
如果需要,装饰器可以吞下函数 ( 也就是不返回该函数 ) ,或返回不是函数的函数
编写自己的装饰器
如上所述,装饰器只是一个传递函数的函数,并返回一个对象
所以,要开始编写装饰器,我们只需要定义一个函数
def my_decorator(f): return 5
任何函数都可以用作装饰器。在这个例子中,装饰器接收一个函数,并返回一个不同的对象。它只是完全吞下传递给它的函数,并且总是返回 5
@my_decorator def hello(): print('hello')
>>> hello() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable 'int' object is not callable
因为我们的装饰器返回一个 int
而不是一个可调用的,所以它不能作为函数调用
请记住,装饰器的返回值替换了 hello
>>> hello 5
在大多数情况下,我们希望装饰器返回的对象实际上模仿我们装饰的函数。这意味着装饰器返回的对象本身需要是一个函数
例如,假设我们只想在每次调用函数时打印,我们可以编写一个打印该信息的函数,然后调用该函数。但是该函数需要由装饰器返回
这通常会导致函数嵌套,例如
def mydecorator(f): # f is the function passed to us from python def log_f_as_called(): print(f'{f} was called.') f() return log_f_as_called
正如你所见,我们定义了一个嵌套的函数,而装饰器函数则返回刚刚定义的嵌套函数。这样,函数 hello
仍然可以像标准函数一样被调用,调用者不需要知道它是否被装饰
我们现在可以将 hello
定义如下
@mydecorator def hello(): print('hello')
我们会得到如下的输出
>>> hello() <function hello at 0x7f27738d7510> was called. hello
注意:<function hello at 0x7f27738d7510>
引用内的数字对每个运行来说都不同,它代表内存地址
正确包装函数
如果需要,可以多次装饰一个函数。这种情况下,装饰器会产生链式效应。基本上,顶部装饰器从前者传递对象,依此类推。例如,如果我们有以下代码
@a @b @c def hello(): print('hello')
解释器本质上是执行 hello = a(b(c(hello)))
并且所有装饰器将相互包装
您可以使用我们现有的装饰器自己测试,并使用它两次
@mydecorator @mydecorator def hello(): print('hello') >>> hello() <function mydec.<locals>.a at 0x7f277383d378> was called. <function hello at 0x7f2772f78ae8> was called. hello
您将注意到第一个装饰器,包裹第二个装饰器,并单独打印
你可能注意到这里的一个有趣的事情是,第一行打印了 <function mydec.<locals>.a at 0x7f277383d378>
而不是第二行打印,而我们所期待的是:<function hello at 0x7f2772f78ae8>.
这是因为装饰器返回的对象是一个新函数,而不是 hello
。这对于我们这个简单的例子来说很好,但是经常会破坏可能试图反省函数属性的测试和事情
如果你的想法是装饰器像它装饰的函数一样,它还需要模仿该函数。幸运的是,Python 标准库 functools
模块中有一个名为 wraps
的装饰器
import functools def mydecorator(f): @functools.wraps(f) # we tell wraps that the function we are wrapping is f def log_f_as_called(): print(f'{f} was called.') f() return log_f_as_called @mydecorator @mydecorator def hello(): print('hello') >>> hello() <function hello at 0x7f27737c7950> was called. <function hello at 0x7f27737c7f28> was called. hello
现在,我们的新函数就像它的包装/装饰一样。但是,我们仍然依赖于它什么都不返回,并且不接受任何输入的事实
如果我们想要更通用,我们需要传入参数并返回相同的值。我们可以修改我们的函数让它看起来像这样
import functools def mydecorator(f): @functools.wraps(f) # wraps is a decorator that tells our function to act like f def log_f_as_called(*args, **kwargs): print(f'{f} was called with arguments={args} and kwargs={kwargs}') value = f(*args, **kwargs) print(f'{f} return value {value}') return value return log_f_as_called
现在我们每次调用函数时都会打印,包括函数接收的所有输入以及返回的内容。现在,你可以简单地装饰任何现有函数,并在其所有输入和输出上进行调试日志记录,而无需手动编写日志记录代码
给装饰器添加变量
如果我们使用装饰器来处理我们想要发布的任何代码,而不仅仅是本地代码,那么可能希望用 logging
语句替换所有 print
语句。这种情况下,我们需要定义日志级别。假设我们默认使用 debug
日志级别,但这也可能取决于函数
我们可以为装饰器本身提供变量,以定义它应该如何表现。例如
@debug(level='info') def hello(): print('hello')
上面的代码将允许我们指定此特定函数应该在 info
级别而不是 debug
级别进行日志记录。这在 Python
中中是通过编写一个返回装饰器的函数实现的
是的,装饰者也是一个函数。所以这基本上是说 hello = debug('info')(hello)
。这个双括号可能看起来很时髦,但基本上,debug
是函数,它返回一个函数
为了将它添加到我们现有的装饰器中,我们需要再嵌套一次,现在使我们的代码看起来如下所示
import functools def debug(level): def mydecorator(f) @functools.wraps(f) def log_f_as_called(*args, **kwargs): logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}') value = f(*args, **kwargs) logger.log(level, f'{f} return value {value}') return value return log_f_as_called return mydecorator
上面的更改将 debug
变为一个函数,该函数返回一个使用正确日志记录级别的装饰器,这变得有点难看,并且过度嵌套
我想做一些小技巧来解决这个问题,就是添加一个 kwarg 参数 level
并且默认只为 debug
并返回一个 partial
partial
是一个 「 非完整函数调用 」,它包含一个函数和一些参数,因此它们作为一个对象传递而不实际调用该函数
import functools def debug(f=None, *, level='debug'): if f is None: return functools.partial(debug, level=level) @functools.wraps(f) # we tell wraps that the function we are wrapping is f def log_f_as_called(*args, **kwargs): logger.log(level, f'{f} was called with arguments={args} and kwargs={kwargs}') value = f(*args, **kwargs) logger.log(level, f'{f} return value {value}') return value return log_f_as_called
现在装饰器可以正常工作
@debug def hello(): print('hello')
然后就可以使用 debug
级别记录日志,或者,覆盖日志级别
@debug('warning') def hello(): print('hello')