关于 Python 装饰器,你应该知道的知识

yufei       6 年, 4 月 前       691

我曾经在 科里定律的变量 ( 一 ) - 缘起,透视严重的变量稀缺问题 中提到,认识科里定律是通过一篇名为 关于 Python 装饰器,你应该知道的知识

这篇文章还是挺有意思的,索性我就把它翻译为中文吧


Python 装饰器是一个强大的概念,允许我们使用一个函数 「 包装 」 另一个函数

除了正常的职责之外,装饰器的另类使用想法是抽象出你想要一个功能或类做的东西,这可能有很多原因,例如 代码重用 和坚持 科里原则

通过学习如何编写自己的装饰器,我们可以显着提高自己代码的可读性,因为它们可以更改函数的行为方式,而无需实际更改代码 ( 例如添加日志记录行 )

它们是 Python 中相当常用的工具,对于使用诸如 flaskclick 之类的框架的人来说很熟悉

虽然很多人只知道如何使用它们,而不知道如何编写自己的装饰器

这篇文章是由朋友 @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')
目前尚无回复
简单教程 = 简单教程,简单编程
简单教程 是一个关于技术和学习的地方
现在注册
已注册用户请 登入
关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.