Dalam artikel Melihat lebih dekat pada penghias Python, saya memperkenalkan konsep penghias Python, menunjukkan banyak penghias yang hebat dan menerangkan cara menggunakannya.
Dalam tutorial ini, saya akan menunjukkan kepada anda cara menulis penghias anda sendiri. Seperti yang anda akan lihat, menulis penghias anda sendiri memberi anda banyak kawalan dan membolehkan banyak fungsi. Tanpa penghias, ciri ini memerlukan banyak pelat dandang yang mudah ralat dan berulang, mengacaukan kod atau memerlukan mekanisme luaran sepenuhnya (seperti penjanaan kod).
Jika anda tidak tahu apa-apa tentang penghias, berikut ialah penyegaran pantas. Penghias ialah objek boleh panggil (fungsi, kaedah, kelas atau objek yang mempunyai kaedah panggilan()) yang menerima objek boleh panggil sebagai input dan mengembalikan objek boleh panggil sebagai output. Biasanya, boleh panggil yang dikembalikan melakukan beberapa operasi sebelum dan/atau selepas memanggil input boleh dipanggil. Anda boleh menggunakan sintaks @
Mari mulakan dengan "Hello world!" Penghias ini akan menggantikan sepenuhnya mana-mana fungsi boleh panggil yang dihiasi dengan fungsi yang hanya mencetak "Hello World!"
def hello_world(f): def decorated(*args, **kwargs): print 'Hello World!' return decorated
Itu sahaja. Mari lihat dalam tindakan dan terangkan bahagian yang berbeza dan cara ia berfungsi. Katakan kita mempunyai fungsi berikut, yang mengambil dua nombor dan mencetak produknya:
def multiply(x, y): print x * y
Jika anda menelefon, anda akan mendapat apa yang anda harapkan:
(6, 7) 42
Mari kita anotasi fungsi @hello_world
注释 multiply 函数,用 hello_world 装饰器来装饰它。
@hello_world def 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)
multiply dengan @hello_world
dan hiasinya dengan < /strong>hello_world
def print_callable(f): def decorated(*args, **kwargs): print f, type(f) return f(*args, **kwargs) return decorated
multiply
dengan sebarang hujah (termasuk jenis data yang salah atau bilangan hujah yang salah), hasilnya sentiasa mencetak "Hello World!".
@print_callable def multiply(x, y): print x * y class A(object): @print_callable def foo(self): print 'foo() here'
hello_world
. Jika kita menganalisis struktur penghiashello_world
, maka anda akan melihat bahawa ia menerima input boleh panggilf
(tidak digunakan dalam penghias ringkas ini), yang mentakrifkan fungsi bersarang bernamadecorated(*args, **kwargs)
), dan akhirnya mengembalikan fungsi berhias
.Fungsi penulisan dan penghias kaedah
Tiada perbezaan antara fungsi penulisan dan penghias kaedah. Definisi penghias adalah sama. Input yang boleh dipanggil akan menjadi fungsi biasa atau kaedah terikat.Mari sahkannya. Ini ialah penghias yang hanya mencetak input boleh dipanggil dan taip sebelum memanggilnya. Ini adalah kes biasa di mana penghias melakukan sesuatu dan terus memanggil boleh panggil asal.
multiply(6, 7) 42 A().foo() foo() here
Mari kita lihat ia beraksi. Saya akan menghiasi fungsi dan kaedah pendaraban kami.
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(1) def slow_multiply(x, y): multiply(x, y) @minimum_runtime(3) def slower_multiply(x, y): multiply(x, y)
Bezanya sekarang ialah penghias itu sendiri menerima parameter t untuk menentukan masa berjalan minimum, dan fungsi berbeza boleh dihiasi dengan masa larian minimum yang berbeza. Selain itu, anda akan perasan bahawa apabila memperkenalkan parameter penghias, dua tahap sarang diperlukan:
import time funcs = [multiply, slow_multiply, slower_multiply] for f in funcs: start = time.time() f(6, 7) print f, time.time() - start
t
yang mewakili masa jalan minimum bagi yang boleh dipanggil berhias. Input boleh panggil #🎜🎜#f#🎜🎜# "ditolak ke bawah" ke fungsi #🎜🎜# penghias #🎜🎜# bersarang dan parameter boleh panggil input "ditolak ke bawah" ke fungsi bersarang lain #🎜🎜# Peranti pembalut #🎜🎜#.#🎜🎜# #🎜🎜#Logik sebenar berlaku di dalam fungsi #🎜🎜# #🎜🎜#. Rakam masa mula, panggil yang boleh dipanggil asal #🎜🎜#f#🎜🎜# dengan hujahnya dan simpan hasilnya. Kemudian ia menyemak masa larian dan jika kurang daripada minimum #🎜🎜#t#🎜🎜# maka ia tidur untuk sepanjang masa dan kemudian kembali. #🎜🎜# #🎜🎜#Untuk mengujinya, saya akan mencipta beberapa fungsi yang memanggil pendaraban dan menghiasinya dengan kelewatan yang berbeza. #🎜🎜#@minimum_runtime(1) def slow_multiply(x, y): multiply(x, y) @minimum_runtime(3) def slower_multiply(x, y): multiply(x, y)
现在,我将直接调用 multiply 以及较慢的函数并测量时间。
import time funcs = [multiply, slow_multiply, slower_multiply] for f in funcs: start = time.time() f(6, 7) print f, time.time() - start
这是输出:
42 1.59740447998e-05 42 1.00477004051 42 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 装饰器,它仅调用其输入可调用函数,但保留输入函数中的所有信息:函数名称、其所有属性(如果内部装饰器添加了一些自定义属性)及其文档字符串。 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 @passthrough def Foo(): print 'Foo() here'
此功能非常有用,以至于标准库在 functools 模块中有一个名为“wraps”的特殊装饰器,可以帮助编写与其他装饰器配合良好的适当装饰器。您只需在装饰器中使用 @wraps(f) 装饰返回的函数即可。看看使用 wraps 时 passthrough 看起来有多简洁:
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()
现在,如果有人添加带有错误的 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_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 中的类装饰器通过自定义完整类的行为添加了一个全新的维度。
Atas ialah kandungan terperinci Buat penghias Python anda sendiri. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!