总览
在“ 深入研究Python装饰器”一文中 ,我介绍了Python装饰器的概念,展示了许多很酷的装饰器,并解释了如何使用它们。
在本教程中,我将向您展示如何编写自己的装饰器。 如您所见,编写自己的装饰器可以给您带来很多控制权并启用许多功能。 没有装饰器,这些功能将需要大量容易出错且重复的样板,从而使您的代码或完全外部的机制(如代码生成)变得混乱。
快速回顾一下,如果您对装饰者一无所知。 装饰器是可调用的(带有call ()方法的函数,方法,类或对象),它接受可调用的内容作为输入并返回可调用的内容作为输出。 通常,返回的可调用对象在调用输入可调用对象之前和/或之后会执行某些操作。 您通过使用@来应用装饰器 句法。 大量的例子即将到来...
你好,世界装饰
让我们从一个“ Hello world!”开始。 装饰。 该装饰器将完全替换任何带有可打印“ Hello World!”功能的可调用的装饰器。
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装饰器装饰它,方法是用@hello_world
注释乘法函数。
@hello_world
def multiply(x, y):
print x * y
现在,当您使用任何参数(包括错误的数据类型或错误的参数数量)调用乘法时,结果始终是“ Hello World!”。 打印。
multiply(6, 7)
Hello World!
multiply()
Hello World!
multiply('zzz')
Hello World!
好。 它是如何工作的? 原始的乘法函数完全被hello_world装饰器中的嵌套装饰函数所取代。 如果我们分析hello_world装饰器的结构,那么您会看到它接受输入的可调用f (在此简单装饰器中未使用),它定义了一个称为装饰的嵌套函数,该函数接受参数和关键字参数的任何组合( def decorated(*args, **kwargs)
),最后返回装饰的函数。
编写函数和方法装饰器
编写函数和方法装饰器没有区别。 装饰器的定义将相同。 输入的可调用对象将是常规函数或绑定方法。
让我们验证一下。 这是一个装饰器,它仅在调用之前打印可调用的输入和类型。 这对于装饰器执行某些操作并通过调用原始可调用项继续是非常典型的。
def print_callable(f):
def decorated(*args, **kwargs):
print f, type(f)
return f(*args, **kwargs)
return decorated
注意最后一行以通用方式调用可调用输入并返回结果。 在您可以装饰一个正在运行的应用程序中的任何函数或方法的意义上说,这种装饰器是非侵入式的,并且该应用程序将继续运行,因为装饰后的函数会调用原始函数,并且之前几乎没有副作用。
让我们来看看它的作用。 我将装饰我们的乘法功能和方法。
@print_callable
def multiply(x, y):
print x * y
class A(object):
@print_callable
def foo(self):
print 'foo() here'
当我们调用函数和方法时,可调用对象将被打印,然后它们执行其原始任务:
multiply(6, 7)
<function multiply at 0x103cb6398> <type 'function'>
42
A().foo()
<function foo at 0x103cb6410> <type 'function'>
foo() here
带参数的装饰器
装饰者也可以接受争论。 这种配置装饰器操作的能力非常强大,并允许您在许多情况下使用同一装饰器。
假设您的代码太快了,您的老板要求您将其放慢一点,因为您使其他团队成员看起来很糟糕。 让我们写一个装饰器来衡量一个函数运行了多长时间,如果它运行的时间少于特定的秒数t ,它将一直等到t秒到期然后返回。
现在不同的是,装饰器本身采用一个参数t来确定最小运行时间,并且可以使用不同的最小运行时间来装饰不同的函数。 此外,您会注意到在引入装饰器参数时,需要两个嵌套级别:
import time
def minimum_runtime(t):
def decorated(f):
def wrapper(*args, **kwargs):
start = time.time()
result = f(*args, **kwargs)
runtime = time.time() - start
if runtime < t:
time.sleep(t - runtime)
return result
return wrapper
return decorated
让我们打开包装。 装饰器本身-函数minimum_runtime带有参数t ,它表示装饰的可调用对象的最小运行时间。 输入的可调用f被“压入”嵌套的装饰函数,输入的可调用参数被“压入”另一个嵌套的函数包装器 。
实际的逻辑发生在包装函数内部。 记录开始时间,使用其参数调用原始可调用f ,并存储结果。 然后检查运行时,如果它小于最小值t,则它将在其余时间进入Hibernate状态,然后返回。
为了测试它,我将创建几个函数,它们调用乘法并以不同的延迟来装饰它们。
@minimum_runtime(1)
def slow_multiply(x, y):
multiply(x, y)
@minimum_runtime(3)
def slower_multiply(x, y):
multiply(x, y)
现在,我将直接调用乘法以及较慢的函数并测量时间。
import time
funcs = [multiply, slow_multiply, slower_multiply]
for f in funcs:
start = time.time()
f(6, 7)
print f, time.time() - start
这是输出:
42
<function multiply at 0x103cb6b90> 1.59740447998e-05
42
<function wrapper at 0x103d0bcf8> 1.00477004051
42
<function wrapper at 0x103cb6ed8> 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)
它在起作用:
@Counter
def bbb():
print 'bbb'
bbb()
bbb
bbb()
bbb
bbb()
bbb
print bbb.called
3
在基于函数的装饰器和基于对象的装饰器之间进行选择
这主要是个人喜好问题。 嵌套函数和函数闭包提供对象提供的所有状态管理。 有些人对课堂和对象感到宾至如归。
在下一节中,我将讨论行为良好的修饰符,而基于对象的修饰符需要做一些额外的工作才能使其表现良好。
行为端正的装饰者
通用装饰器通常可以堆叠。 例如:
@decorator_1
@decorator_2
def 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_lowercase
def Foo():
print 'Foo() here'
调用Foo()会导致一个断言:
In [51]: Foo()
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
ipython-input-51-bbcd91f35259 in module()
----> 1 Foo()
ipython-input-49-a80988798919 in decorated(*args, **kwargs)
1 def check_lowercase(f):
2 def decorated(*args, **kwargs):
----> 3 assert f.func_name == f.func_name.lower()
4 return decorated
但是,如果将check_lowercase装饰器叠加在hello_world这样的装饰器上,该装饰器返回一个称为“ decorated”的嵌套函数,则结果将大不相同:
@check_lowercase
@hello_world
def Foo():
print 'Foo() here'
Foo()
Hello World!
check_lowercase装饰器没有引发断言,因为它没有看到函数名'Foo'。 这是一个严重的问题。 装饰器的正确行为是保留尽可能多的原始功能属性。
让我们看看它是如何完成的。 现在,我将创建一个shell装饰器,该装饰器简单地调用其可调用的输入,但保留输入函数中的所有信息:函数名称,其所有属性(如果内部装饰器添加了一些自定义属性)以及其文档字符串。
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
现在,堆叠在直通装饰器顶部的装饰器将像直接装饰目标函数一样工作。
@check_lowercase
@passthrough
def Foo():
print 'Foo() here'
使用@wraps装饰器
此功能非常有用,以至于标准库在functools模块中有一个特殊的装饰器,称为“包装” ,可帮助编写与其他装饰器配合使用的适当装饰器。 您只需在装饰器内部用@wraps(f)装饰返回的函数即可 。 看看使用自动换行时看起来更简洁的传递方式 :
from functools import wraps
def 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, main
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()
现在,如果有人添加了一个带有bug的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
这看起来不错,但是有一个问题。 类装饰器仅接受一个参数:装饰的类。 sure_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_1和awesome_2,都有相应的测试方法,test_awesome_1和test_awesome_2在成功试验的结果。
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
让我们添加一个没有相应测试的新方法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.py
Traceback (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中的类装饰器增加了一个全新的维度。
翻译自: https://code.tutsplus.com/tutorials/write-your-own-python-decorators--cms-25630