PHP前端开发

创建您自己的 Python 装饰器

百变鹏仔 12小时前 #Python
文章标签 自己的

概述

在深入了解 Python 装饰器一文中,我介绍了 Python 装饰器的概念,演示了许多很酷的装饰器,并解释了如何使用它们。

在本教程中,我将向您展示如何编写自己的装饰器。正如您将看到的,编写自己的装饰器可以为您提供很多控制权并启用许多功能。如果没有装饰器,这些功能将需要大量容易出错且重复的样板文件,从而使代码变得混乱,或者需要完全外部的机制(例如代码生成)。

如果您对装饰器一无所知,请快速回顾一下。装饰器是一个可调用对象(具有 call() 方法的函数、方法、类或对象),它接受可调用对象作为输入并返回可调用对象作为输出。通常,返回的可调用对象在调用输入可调用对象之前和/或之后执行某些操作。您可以使用 @ 语法来应用装饰器。大量示例即将推出...

Hello World 装饰器

让我们从“Hello world!”装饰器开始。这个装饰器将完全用只打印“Hello World!”的函数替换任何装饰的可调用函数。

立即学习“Python免费学习笔记(深入)”;

def hello_world(f):    def decorated(*args, **kwargs):        print 'Hello World!'    return decorated

就是这样。让我们看看它的实际效果,然后解释不同的部分及其工作原理。假设我们有以下函数,它接受两个数字并打印它们的乘积:

def multiply(x, y):    print x * y

如果你调用,你会得到你所期望的:

(6, 7)42

让我们用 @hello_world 注释 multiply 函数,用 hello_world 装饰器来装饰它。

@hello_worlddef multiply(x, y):    print x * y

现在,当您使用任何参数(包括错误的数据类型或错误的参数数量)调用multiply时,结果始终是打印“Hello World!”。

multiply(6, 7)Hello World!multiply()Hello World!multiply('zzz')Hello World!

好的。它是如何工作的?原来的multiply函数被hello_world装饰器内的嵌套装饰函数完全取代。如果我们分析 hello_world 装饰器的结构,那么您会看到它接受可调用的输入 f (在这个简单的装饰器中没有使用),它定义了一个嵌套名为 decorated 的函数,它接受参数和关键字参数的任意组合 (defdecorated(*args, **kwargs)),最后返回 decorated 功能。

编写函数和方法装饰器

编写函数和方法装饰器没有区别。装饰器的定义是相同的。输入可调用将是常规函数或绑定方法。

让我们验证一下。这是一个装饰器,它在调用它之前仅打印输入可调用和类型。这是装饰器执行某些操作并继续调用原始可调用对象的典型情况。

def print_callable(f):    def decorated(*args, **kwargs):        print f, type(f)        return f(*args, **kwargs)    return decorated

注意最后一行以通用方式调用输入可调用并返回结果。这个装饰器是非侵入性的,因为您可以装饰正在工作的应用程序中的任何函数或方法,并且应用程序将继续工作,因为装饰后的函数会调用原始函数,并且之前只会产生一些副作用。

让我们看看它的实际效果。我将装饰我们的乘法函数和方法。

@print_callabledef multiply(x, y):    print x * yclass A(object):    @print_callable    def foo(self):        print 'foo() here'

当我们调用函数和方法时,会打印可调用的内容,然后它们执行原始任务:

multiply(6, 7) 42A().foo() foo() here

带参数的装饰器

装饰器也可以接受参数。这种配置装饰器操作的能力非常强大,允许您在许多上下文中使用相同的装饰器。

假设你的代码太快了,你的老板要求你放慢速度,因为你让其他团队成员看起来很糟糕。让我们编写一个装饰器来测量函数运行的时间,如果它运行的时间少于一定的秒数t,它将等到t秒到期然后返回。

现在不同的是,装饰器本身接受一个参数t来确定最小运行时间,并且不同的函数可以用不同的最小运行时间来装饰。另外,您会注意到,在引入装饰器参数时,需要两层嵌套:

import timedef minimum_runtime(t):    def decorated(f):        def wrapper(*args, **kwargs):            start = time.time()            result = f(*args, **kwargs)            runtime = time.time() - start            if runtime <p>让我们打开它。装饰器本身——函数<strong>minimum_runtime</strong>采用一个参数<strong>t</strong>,它表示装饰后的可调用函数的最短运行时间。输入可调用<strong>f</strong>被“下推”到嵌套<strong>装饰</strong>函数,输入可调用参数被“下推”到另一个嵌套函数<strong>包装器强&gt;.</strong></p><p>实际逻辑发生在<strong>包装器</strong>函数内部。记录开始时间,使用其参数调用原始可调用 <strong>f</strong>,并存储结果。然后检查运行时,如果它小于最小<strong>t</strong>,那么它会在剩下的时间里休眠,然后返回。</p><p>为了测试它,我将创建几个调用乘法的函数,并用不同的延迟来装饰它们。</p><pre class="brush:python;toolbal:false;">@minimum_runtime(1)def slow_multiply(x, y):    multiply(x, y)    @minimum_runtime(3)def slower_multiply(x, y):    multiply(x, y)

现在,我将直接调用 multiply 以及较慢的函数并测量时间。

import timefuncs = [multiply, slow_multiply, slower_multiply]for f in funcs:    start = time.time()    f(6, 7)    print f, time.time() - start

这是输出:

42 1.59740447998e-0542 1.0047700405142 3.00489807129

正如您所看到的,原始乘法几乎没有花费任何时间,并且较慢的版本确实根据提供的最小运行时间进行了延迟。

另一个有趣的事实是,执行的装饰函数是包装器,如果您遵循装饰的定义,这是有意义的。但这可能是一个问题,特别是当我们处理堆栈装饰器时。原因是许多装饰器还会检查其输入可调用对象并检查其名称、签名和参数。以下部分将探讨此问题并提供最佳实践建议。

对象装饰器

您还可以使用对象作为装饰器或从装饰器返回对象。唯一的要求是它们有一个 __call__() 方法,因此它们是可调用的。下面是一个基于对象的装饰器的示例,它计算其目标函数被调用的次数:

class Counter(object):    def __init__(self, f):        self.f = f        self.called = 0    def __call__(self, *args, **kwargs):        self.called += 1        return self.f(*args, **kwargs)

这是在行动:

@Counterdef bbb():    print 'bbb'bbb()bbbbbb()bbbbbb()bbbprint bbb.called3

在基于函数的装饰器和基于对象的装饰器之间进行选择

这主要是个人喜好问题。嵌套函数和函数闭包提供了对象提供的所有状态管理。有些人对类和对象感觉更自在。

在下一节中,我将讨论行为良好的装饰器,而基于对象的装饰器需要一些额外的工作才能表现良好。

行为良好的装饰器

通用装饰器通常可以堆叠。例如:

@decorator_1@decorator_2def foo():    print 'foo() here'

当堆叠装饰器时,外部装饰器(本例中为decorator_1)将接收内部装饰器(decorator_2)返回的可调用对象。如果decorator_1在某种程度上依赖于原始函数的名称、参数或文档字符串,并且decorator_2是简单实现的,那么decorator_2将看不到原始函数中的正确信息,而只能看到decorator_2返回的可调用信息。

例如,下面是一个装饰器,它验证其目标函数的名称是否全部小写:

def check_lowercase(f):    def decorated(*args, **kwargs):        assert f.func_name == f.func_name.lower()        f(*args, **kwargs)    return decorated

让我们用它来装饰一个函数:

@check_lowercasedef Foo():    print 'Foo() here'

调用 Foo() 会产生断言:

In [51]: Foo()---------------------------------------------------------------------------AssertionError                            Traceback (most recent call last)ipython-input-51-bbcd91f35259 in module()----&gt; 1 Foo()ipython-input-49-a80988798919 in decorated(*args, **kwargs)      1 def check_lowercase(f):      2     def decorated(*args, **kwargs):----&gt; 3         assert f.func_name == f.func_name.lower()      4     return decorated

但是,如果我们将 check_lowercase 装饰器堆叠在像 hello_world 这样返回名为“decorated”的嵌套函数的装饰器上,结果会非常不同:

@check_lowercase@hello_worlddef Foo():    print 'Foo() here'Foo()Hello World!    

check_lowercase 装饰器没有引发断言,因为它没有看到函数名称“Foo”。这是一个严重的问题。装饰器的正确行为是尽可能多地保留原始函数的属性。

让我们看看它是如何完成的。现在,我将创建一个 shell 装饰器,它仅调用其输入可调用函数,但保留输入函数中的所有信息:函数名称、其所有属性(如果内部装饰器添加了一些自定义属性)及其文档字符串。 p>

def passthrough(f):    def decorated(*args, **kwargs):        f(*args, **kwargs)    decorated.__name__ = f.__name__    decorated.__name__ = f.__module__    decorated.__dict__ = f.__dict__    decorated.__doc__ = f.__doc__        return decorated

现在,堆叠在passthrough装饰器之上的装饰器将像直接装饰目标函数一样工作。

@check_lowercase@passthroughdef Foo():    print 'Foo() here'

使用@wraps装饰器

此功能非常有用,以至于标准库在 functools 模块中有一个名为“wraps”的特殊装饰器,可以帮助编写与其他装饰器配合良好的适当装饰器。您只需在装饰器中使用 @wraps(f) 装饰返回的函数即可。看看使用 wrapspassthrough 看起来有多简洁:

from functools import wrapsdef passthrough(f):    @wraps(f)    def decorated(*args, **kwargs):        f(*args, **kwargs)    return decorated

我强烈建议始终使用它,除非您的装饰器旨在修改其中一些属性。

编写类装饰器

类装饰器是在 Python 3.0 中引入的。他们对整个班级进行操作。定义类时和创建任何实例之前会调用类装饰器。这使得类装饰器几乎可以修改类的每个方面。通常您会添加或修饰多个方法。

让我们直接跳到一个奇特的示例:假设您有一个名为“AwesomeClass”的类,其中包含一堆公共方法(名称不以下划线开头的方法,例如 init),并且您有一个基于单元测试的测试类,名为“AwesomeClassTest”。 AwesomeClass 不仅很棒,而且非常关键,您要确保如果有人向 AwesomeClass 添加新方法,他们也会向 AwesomeClassTest 添加相应的测试方法。这是 AwesomeClass:

class AwesomeClass:    def awesome_1(self):        return 'awesome!'    def awesome_2(self):        return 'awesome! awesome!'

这是 AwesomeClassTest:

from unittest import TestCase, mainclass AwesomeClassTest(TestCase):    def test_awesome_1(self):        r = AwesomeClass().awesome_1()        self.assertEqual('awesome!', r)            def test_awesome_2(self):        r = AwesomeClass().awesome_2()        self.assertEqual('awesome! awesome!', r)if __name__ == '__main__':            main()

现在,如果有人添加带有错误的 awesome_3 方法,测试仍然会通过,因为没有调用 awesome_3 的测试。

如何确保每个公共方法始终都有一个测试方法?好吧,当然,你编写一个类装饰器。 @ensure_tests 类装饰器将装饰 AwesomeClassTest 并确保每个公共方法都有相应的测试方法。

def ensure_tests(cls, target_class):    test_methods = [m for m in cls.__dict__ if m.startswith('test_')]    public_methods = [k for k, v in target_class.__dict__.items()                       if callable(v) and not k.startswith('_')]    # Strip 'test_' prefix from test method names    test_methods = [m[5:] for m in test_methods]    if set(test_methods) != set(public_methods):        raise RuntimeError('Test / public methods mismatch!')    return cls

这看起来不错,但有一个问题。类装饰器只接受一个参数:被装饰的类。 Ensure_tests 装饰器需要两个参数:类和目标类。我找不到一种方法来让类装饰器具有类似于函数装饰器的参数。没有恐惧。 Python 有 functools.partial 函数专门用于这些情况。

@partial(ensure_tests, target_class=AwesomeClass)class AwesomeClassTest(TestCase):    def test_awesome_1(self):        r = AwesomeClass().awesome_1()        self.assertEqual('awesome!', r)    def test_awesome_2(self):        r = AwesomeClass().awesome_2()        self.assertEqual('awesome! awesome!', r)        if __name__ == '__main__':    main()        

运行测试会成功,因为所有公共方法 awesome_1awesome_2 都有相应的测试方法 test_awesome_1 test_awesome_2

----------------------------------------------------------------------Ran 2 tests in 0.000sOK

让我们添加一个没有相应测试的新方法awesome_3,然后再次运行测试。

class AwesomeClass:    def awesome_1(self):        return 'awesome!'    def awesome_2(self):        return 'awesome! awesome!'    def awesome_3(self):        return 'awesome! awesome! awesome!'

再次运行测试会产生以下输出:

python3 a.pyTraceback (most recent call last):  File "a.py", line 25, in module    class AwesomeClassTest(TestCase):  File "a.py", line 21, in ensure_tests    raise RuntimeError('Test / public methods mismatch!')RuntimeError: Test / public methods mismatch!

类装饰器检测到不匹配并大声清晰地通知您。

结论

编写 Python 装饰器非常有趣,可以让您以可重用的方式封装大量功能。要充分利用装饰器并以有趣的方式组合它们,您需要了解最佳实践和习惯用法。 Python 3 中的类装饰器通过自定义完整类的行为添加了一个全新的维度。