原文towardsdatascience.com/python-decoration-is-very-useful-but-when-to-use-them-e4f5faab43ec作为 Python 中最独特的特点之一装饰器可能并不容易理解。然而无论你是数据科学家、数据工程师还是 Web 开发者它都将为我们的开发提供极大的便利。几年前我写了一篇关于 Python 装饰器的教程文章。如果你想要了解一些关于 Python 装饰器的基础知识这篇文章非常推荐。Python 装饰器的最简单教程在这篇文章中我将总结 Python 装饰器的五种不同使用场景。希望这能给你一些关于何时以及为什么我们应该使用装饰器的想法。1. 快速入门 – 函数装饰器让我们从 Python 装饰器的基本使用模式开始即函数装饰器。假设我们正在开发一个数据分析应用程序。我们想知道某些数据处理函数运行需要多长时间。请查看以下模拟的数据处理函数。defprocess_data(data):time.sleep(2)# Simulate the elapse time for data processingprint(fDone, data is processed.)data[1,2,3]# Simple dummy data为了演示目的我们可以创建一个简单的列表作为模拟数据。我们还可以跳过真实的“数据处理”实现并使用休眠函数来模拟函数中发生的事情。这也模拟了我们想要测量的“经过时间”。现在让我们先不考虑装饰器。我们可以简单地编写如下代码来测量process_data()函数的性能。importtime start_timetime.time()process_data(data)end_timetime.time()print(fThe function executed in{end_time-start_time:.3f}seconds)我们首先需要导入time模块。然后我们可以获取运行函数之前的起始时间戳接着运行函数。函数执行完毕后我们可以获取结束时间戳。这两个时间戳之间的差异将是经过的时间。我还将其四舍五入到小数点后三位以提高可读性。为什么在这里需要装饰器上述示例效果很好。但是如果我们有多个像process_data()这样的函数需要多次测量我们是否应该在应用程序的每个地方都写上开始时间和结束时间使用 Python 的装饰器我们不必重复自己。要编写一个装饰器以实现与上述完全相同的结果我们只需将代码放入嵌套函数中如下所示。defperformance_metrics(func):defwrapper(*args):start_timetime.time()func(*args)end_timetime.time()print(f{func.__name__}executed in{end_time-start_time:.3f}seconds)returnwrapper在这个嵌套函数中参数func指的是装饰器将要装饰的函数。在我们的例子中这意味着process_data()函数。然后在wrapper()函数中我们定义装饰器应该做什么。基本上代码与我们在上一个示例中做的脚本完全相同只是我们必须使用func来调用原始函数。在wrapper(*args)函数中我们只是从process_data(data)函数中获取所有变量并将其传递给func以确保我们使用其原始参数执行它。如果您不理解*args的魔法请查看下面的文章。你真的了解 Python 中的*args 吗因此我们准备好了可以使用的performance_metrics装饰器。下面的代码展示了如何使用它。performance_metricsdefprocess_data(data):time.sleep(2)print(fDone, data is processed.)process_data(data)太好了所以无论是process_data()函数还是其他任何东西我们都可以添加这个装饰器它将帮助我们测量执行时间并输出。2. 方法装饰器现在让我们看看 Python 装饰器的第二种用法。没有什么神奇的在我们展示了我们可以在 Python 函数上使用装饰器之后很自然地会问我们是否可以在类方法上使用它。显然答案是肯定的。假设我们必须在类内部编写一个方法来做某事但我们仍然想测量它的性能。为了实现这一点我们可以直接将装饰器添加到类方法上。是的不用担心命名空间这是正确的方法。classDataProcessor:performance_metricsdefprocess_data(self,data):time.sleep(2)print(fDone, data is processed.)# Test codeprocessorDataProcessor()print(processor.process_data(data))3. 类装饰器现在如果我们想在一个类的所有方法上使用performance_metrics装饰器怎么办是的这当然是可以做到的。当然我不会告诉你一个一个地添加装饰器到方法上。这是一个具有灵活性的选项但我想要展示的是类装饰器。我们可以定义一个类级别装饰器如下。defperformance_for_all_methods(cls):forname,methodincls.__dict__.items():ifcallable(method):setattr(cls,name,performance_metrics(method))returncls请注意我们在这个类装饰器performance_for_all_methods内部使用了performance_metrics函数装饰器。参数cls代表它装饰的类。在 Python 中我们可以从类中获取所有定义例如属性和方法。这个概念与其他编程语言如 Java中的“反射”类似。从字典中我们可以获取工具的名称和工具本身。然后我们需要检查这个工具是否是一个方法因为它可能是一个属性或其他东西。我们只需要检查它是否可以通过运行callable(method)来调用。然后我们可以使用setattr()函数“程序化”地将performance_metrics装饰器添加到类的每个方法上。现在让我们定义一个带有装饰器的示例类和虚拟方法。defperformance_for_all_methods(cls):forname,methodincls.__dict__.items():ifcallable(method):setattr(cls,name,performance_metrics(method))returnclsperformance_for_all_methodsclassDataAnalysis:defclean_data(self,data):time.sleep(1)print(fDone, data is cleaned.)defanalyze_data(self,data):time.sleep(3)print(fDone, data is analyzed.)# Testing the decorated classanalysisDataAnalysis()analysis.clean_data(data)analysis.analyze_data(data)工作得很好请注意这个演示旨在向您展示我们可以在类级别装饰器中做很多可定制的事情所以不要局限于示例中的用例。尝试自己探索更多用例4. 堆叠装饰器你知道我们可以给一个函数添加多个装饰器吗我想称之为“堆叠装饰器”希望这能说得通。假设我们还想添加一个装饰器以确保记录错误。为了演示目的我将只使用 stdout 日志记录而不是写入文件。importlogging# Setup basic configuration for logginglogging.basicConfig(levellogging.DEBUG,format%(asctime)s - %(levelname)s - %(message)s)# The new decorator for loggingdeflog_exceptions(func):defwrapper(*args):try:func(*args)exceptExceptionase:logging.exception(fAn error occurred in{func.__name__}:{str(e)})raise# Re-raise the exception after loggingreturnwrapper在上面的代码中我们定义了一个新的装饰器称为log_exceptions。在wrapper()函数中我们将函数的执行放在一个 try-except 块中以捕获任何可能发生的错误。如果有错误使用logging模块以特定格式记录异常。在实践中我们应该将错误记录到文件中但为了这个演示让我们让它简单一些。一个重要的亮点是我们需要添加raise语句来重新引发错误。否则错误将被完全处理整个过程将被视为成功。现在让我们在process_data()函数中添加一个额外的模拟以模拟如果数据为空将引发自定义错误的情况。performance_metricslog_exceptionsdefprocess_data(data):# Simulate a condition that could raise an errorifnotdata:raiseValueError(No data provided)# Simulate data processing delaytime.sleep(2)print(fData processed with{len(data)}records)看我们已经添加了performance_metrics装饰器和log_exceptions装饰器。在函数内部我们添加了对无数据场景的额外模拟。让我们先使用有效数据测试它。它成功了结果与之前的例子保持不变。然而如果我们故意将一个空的data传递给函数第二个log_exceptions将显示其影响。process_data([])请注意截图中的绿色框中突出显示的内容是日志输出的确切内容。5. 参数化装饰器现在假设我们不想在数据为空时抛出错误而只是给出警告。然而我们希望使用装饰器中的参数来控制警告行为。也就是说如果我们启用此功能装饰器将检测数据是否为空并给出警告。或者如果此功能被禁用则不显示警告。看看我们如何定义一个装饰器来做到这一点。importtimeimportlogging logging.basicConfig(levellogging.DEBUG,format%(asctime)s - %(levelname)s - %(message)s)defperformance_metrics(warn_on_emptyTrue):defdecorator(func):defwrapper(*args):ifwarn_on_emptyandnotargs[0]:logging.warning(fNo data provided to{func.__name__}. Execution halted.)print(Execution halted due to no data.)start_timetime.time()resultfunc(*args)end_timetime.time()elapsed_timeend_time-start_timeprint(f{func.__name__}executed in{elapsed_time:.3f}seconds)returnresultreturnwrapperreturndecorator请不要被 3 级嵌套函数吓到。如果你看看两个内部函数decorator(func)和wrapper(*args)它们与我们上面定义的原始performance_metric装饰器完全一样。当然为了满足新的要求我们必须在wrapper(*args)函数中添加以下功能。那就是添加 if 条件来检查布尔变量warn_on_empty是否为真意味着警告功能已启用并且第一个参数在我们的例子中这将将是data变量为空。我们可以回到使用我们原来的process_data(data)函数。同时别忘了添加我们新的带参数的装饰器并启用它。performance_metrics(warn_on_emptyTrue)defprocess_data(data):time.sleep(2)print(fDone, data is processed.)让我们先使用有效数据运行该函数。与之前的行为没有不同。装饰器只会输出性能指标。现在让我们尝试将一个空列表传递给函数。警告信息显示因为数据为空。这正是我们想要的。如果你愿意我们还可以通过将warn_on_empty设置为 false 来测试禁用该功能。performance_metrics(warn_on_emptyFalse)defprocess_data(data):time.sleep(2)print(fDone, data is processed.)# Test codeprocess_data([])太好了参数按预期工作。如果你现在理解了这一点请随意做你的作业尝试将参数化装饰器用于其他目的。例如添加一个超时参数如果数据处理时间超过一定秒数则终止线程。摘要在这篇文章中我介绍了 Python 装饰器的五种常见和典型用法。从基本的函数装饰器到堆叠和参数化装饰器。希望这能提供一些关于我们何时可以或应该使用装饰器的想法。当然我并不是在说服你到处使用装饰器。当我们应该使用它来降低复杂性并使我们的代码整洁有序还是它可能会产生更多开销或降低可读性时这将由你自己的判断来决定。祝编码愉快