目录
概述
Hello World 装饰器
编写函数和方法装饰器
带参数的装饰器
对象装饰器
在基于函数的装饰器和基于对象的装饰器之间进行选择
行为良好的装饰器
使用@wraps装饰器
编写类装饰器
结论
首页 后端开发 Python教程 创建您自己的 Python 装饰器

创建您自己的 Python 装饰器

Sep 03, 2023 pm 05:37 PM

创建您自己的 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中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何解决Linux终端中查看Python版本时遇到的权限问题? 如何解决Linux终端中查看Python版本时遇到的权限问题? Apr 01, 2025 pm 05:09 PM

Linux终端中查看Python版本时遇到权限问题的解决方法当你在Linux终端中尝试查看Python的版本时,输入python...

如何在10小时内通过项目和问题驱动的方式教计算机小白编程基础? 如何在10小时内通过项目和问题驱动的方式教计算机小白编程基础? Apr 02, 2025 am 07:18 AM

如何在10小时内教计算机小白编程基础?如果你只有10个小时来教计算机小白一些编程知识,你会选择教些什么�...

如何在使用 Fiddler Everywhere 进行中间人读取时避免被浏览器检测到? 如何在使用 Fiddler Everywhere 进行中间人读取时避免被浏览器检测到? Apr 02, 2025 am 07:15 AM

使用FiddlerEverywhere进行中间人读取时如何避免被检测到当你使用FiddlerEverywhere...

在Python中如何高效地将一个DataFrame的整列复制到另一个结构不同的DataFrame中? 在Python中如何高效地将一个DataFrame的整列复制到另一个结构不同的DataFrame中? Apr 01, 2025 pm 11:15 PM

在使用Python的pandas库时,如何在两个结构不同的DataFrame之间进行整列复制是一个常见的问题。假设我们有两个Dat...

Uvicorn是如何在没有serve_forever()的情况下持续监听HTTP请求的? Uvicorn是如何在没有serve_forever()的情况下持续监听HTTP请求的? Apr 01, 2025 pm 10:51 PM

Uvicorn是如何持续监听HTTP请求的?Uvicorn是一个基于ASGI的轻量级Web服务器,其核心功能之一便是监听HTTP请求并进�...

Python中如何通过字符串动态创建对象并调用其方法? Python中如何通过字符串动态创建对象并调用其方法? Apr 01, 2025 pm 11:18 PM

在Python中,如何通过字符串动态创建对象并调用其方法?这是一个常见的编程需求,尤其在需要根据配置或运行...

在Linux终端中使用python --version命令时如何解决权限问题? 在Linux终端中使用python --version命令时如何解决权限问题? Apr 02, 2025 am 06:36 AM

Linux终端中使用python...

如何绕过Investing.com的反爬虫机制获取新闻数据? 如何绕过Investing.com的反爬虫机制获取新闻数据? Apr 02, 2025 am 07:03 AM

攻克Investing.com的反爬虫策略许多人尝试爬取Investing.com(https://cn.investing.com/news/latest-news)的新闻数据时,常常�...

See all articles