創建您自己的 Python 裝飾器

WBOY
發布: 2023-09-03 17:37:07
原創
506 人瀏覽過

创建您自己的 Python 装饰器

概述

在深入了解 Python 裝飾器一文中,我介紹了 Python 裝飾器的概念,演示了許多很酷的裝飾器,並解釋瞭如何使用它們。

在本教程中,我將向您展示如何編寫自己的裝飾器。正如您將看到的,編寫自己的裝飾器可以為您提供大量控制權並啟用許多功能。如果沒有裝飾器,這些功能將需要大量容易出錯且重複的樣板文件,從而使程式碼變得混亂,或者需要完全外部的機制(例如程式碼產生)。

如果您對裝飾器一無所知,請快速回顧一下。裝飾器是可呼叫物件(具有 call() 方法的函數、方法、類別或物件),它接受可呼叫物件作為輸入並傳回可呼叫物件作為輸出。通常,傳回的可調用物件在呼叫輸入可呼叫物件之前和/或之後執行某些操作。您可以使用 @ 語法來套用裝飾器。大量範例即將推出...

Hello World 裝飾器

讓我們從「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 註解 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)),最後回傳decorated< /strong> 功能。

寫函數與方法裝飾器

寫函數和方法裝飾器沒有差別。裝飾器的定義是相同的。輸入可呼叫將是常規函數或綁定方法。

讓我們驗證一下。這是一個裝飾器,它在調用它之前僅列印輸入可調用和類型。這是裝飾器執行某些操作並繼續呼叫原始可調用物件的典型情況。

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)
 
42

A().foo()
 
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,那麼它會在剩餘的時間裡休眠,然後返回。

為了測試它,我將建立幾個呼叫乘法的函數,並用不同的延遲來裝飾它們。

@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 装饰器,它仅调用其输入可调用函数,但保留输入函数中的所有信息:函数名称、其所有属性(如果内部装饰器添加了一些自定义属性)及其文档字符串。

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'
登入後複製

使用@wraps装饰器

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

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_1awesome_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 中的类装饰器通过自定义完整类的行为添加了一个全新的维度。

以上是創建您自己的 Python 裝飾器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!