>  기사  >  백엔드 개발  >  나만의 Python 데코레이터 만들기

나만의 Python 데코레이터 만들기

WBOY
WBOY원래의
2023-09-03 17:37:07511검색

创建您自己的 Python 装饰器

개요

Python 데코레이터 자세히 살펴보기 기사에서 Python 데코레이터의 개념을 소개하고, 멋진 데코레이터를 많이 시연하고, 사용 방법을 설명했습니다.

이 튜토리얼에서는 자신만의 데코레이터를 작성하는 방법을 보여 드리겠습니다. 앞으로 보게 되겠지만 자신만의 데코레이터를 작성하면 많은 제어력과 많은 기능이 활성화됩니다. 데코레이터가 없으면 이러한 기능에는 코드를 복잡하게 만드는 오류가 발생하기 쉽고 반복적인 상용구가 많이 필요하거나 완전히 외부 메커니즘(예: 코드 생성)이 필요합니다.

데코레이터에 대해 아무것도 모른다면 여기에서 간단히 복습해 보세요. 데코레이터는 콜러블을 입력으로 받아들이고 콜러블을 출력으로 반환하는 콜러블(call() 메소드가 있는 함수, 메소드, 클래스 또는 객체)입니다. 일반적으로 반환된 콜러블은 입력 콜러블을 호출하기 전 및/또는 후에 일부 작업을 수행합니다. @ 구문을 사용하여 데코레이터를 적용할 수 있습니다. 곧 많은 예시가 나올 예정입니다...

안녕하세요 월드 데코레이터

"Hello world!"부터 시작해 보겠습니다. 이 데코레이터는 데코레이팅된 호출 가능한 함수를 "Hello World!"만 인쇄하는 함수로 완전히 대체합니다.

으아아아

그렇습니다. 실제로 작동하는 모습을 보고 다양한 부분과 작동 방식을 설명하겠습니다. 두 개의 숫자를 받아 그 결과를 인쇄하는 다음 함수가 있다고 가정해 보겠습니다.

으아아아

전화하면 기대한 대로 얻을 수 있습니다.

으아아아

@hello_worldmultiply 함수에 주석을 달고 hello_world 데코레이터로 장식해 보겠습니다.

으아아아

이제 임의의 인수(잘못된 데이터 유형 또는 잘못된 인수 수 포함)를 사용하여 multiply를 호출하면 결과는 항상 "Hello World!"를 인쇄합니다.

으아아아

알겠습니다. 어떻게 작동하나요? 원래 곱하기 기능은 hello_world 데코레이터 내부의 중첩된 데코레이션 함수로 완전히 대체되었습니다. hello_world 데코레이터의 구조를 분석하면 콜러블을 입력 f(이 간단한 데코레이터에서는 사용되지 않음)으로 받아들이는 것을 볼 수 있습니다. 매개변수 및 키워드 인수()를 입력하고 마지막으로 장식defdecorated(*args, **kwargs) 함수를 반환합니다. 함수 및 메소드 데코레이터 작성

작성 함수와 메소드 데코레이터에는 차이가 없습니다. 데코레이터 정의는 동일합니다. 입력 호출 가능 항목은 일반 함수 또는 바인딩된 메서드입니다.

확인해보자. 이는 호출하기 전에 입력 호출 가능 항목과 유형을 인쇄하는 데코레이터입니다. 이는 데코레이터가 작업을 수행하고 원래 콜러블을 계속 호출하는 일반적인 경우입니다.

으아아아

마지막 줄은 일반적인 방식으로 호출 가능한 입력을 호출하고 결과를 반환한다는 점에 유의하세요. 이 데코레이터는 작동 중인 애플리케이션에서 어떤 함수나 메서드라도 꾸밀 수 있고 데코레이팅된 함수가 원래 함수를 호출하고 이전에는 몇 가지 부작용만 있기 때문에 애플리케이션은 계속 작동하기 때문에 방해가 되지 않습니다.

실제 모습을 살펴보겠습니다. 곱셈 함수와 방법을 꾸며보겠습니다.

으아아아

함수와 메소드를 호출하면 콜러블이 인쇄되고 원래 작업을 수행합니다.

으아아아

매개변수가 있는 데코레이터

데코레이터는 매개변수도 허용할 수 있습니다. 데코레이터 작업을 구성하는 이 기능은 매우 강력하므로 여러 상황에서 동일한 데코레이터를 사용할 수 있습니다.

코드가 너무 빨라서 상사가 다른 팀원을 보기 좋지 않게 만들기 때문에 속도를 늦추라고 요구한다고 가정해 보세요. 함수가 실행되는 데 걸리는 시간을 측정하는 데코레이터를 작성해 보겠습니다. 특정 시간(초

t

) 미만이 걸리면 t초가 만료될 때까지 기다렸다가 반환됩니다. 이제 차이점은 데코레이터 자체가 최소 실행 시간을 결정하기 위해 매개변수

t

를 허용하고, 다양한 기능이 다양한 최소 실행 시간으로 장식될 수 있다는 것입니다. 또한 데코레이터 매개변수를 도입할 때 두 가지 수준의 중첩이 필요하다는 점을 알 수 있습니다. 으아아아 열어보자. 데코레이터 자체 -

minimum_runtime

함수는 데코레이팅된 콜러블의 최소 실행 시간을 나타내는 매개변수 t를 사용합니다. 입력 호출 가능 f은 중첩된 데코레이터 함수로 "푸시다운"되고 입력 호출 가능 인수는 다른 중첩 함수 래퍼 .로 "푸시다운"됩니다. 强>실제 논리는

wrapper

함수 내에서 발생합니다. 시작 시간을 기록하고 인수를 사용하여 원래 호출 가능 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 装饰器,它仅调用其输入可调用函数,但保留输入函数中的所有信息:函数名称、其所有属性(如果内部装饰器添加了一些自定义属性)及其文档字符串。 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'

使用@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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.