Python很早就引入了装饰器——在PEP-318中,做为一种简化函数和方法定义方式的机制,这些函数和方法在初始定义以后必须进行修改。python
这样作的最初动机之一是,使用classmethod和staticmethod等函数来转换方法的原始定义,可是它们须要额外的一行代码来修改函数的初始定义。编程
通常来讲,每次必须对函数应用转换时,咱们必须使用modifier函数调用它,而后将它从新分配到函数初始定义时的名称中。设计模式
例如,假设有一个叫做original的函数,在它上面有一个改变original行为的函数(叫做modifier),那么咱们必须这样写:缓存
def original(...): ... original = modifier(original)
请注意咱们是如何更改函数并将其从新分配到相同的名称中去的。这是使人困惑的,很容易出错(假设有人忘记从新分配函数,或者从新分配了函数,但不在函数定义以后的行中,而是在更远的地方),并且很麻烦。出于这个缘由,Python语言增长了一些语法支持。ruby
前面的示例能够改写为以下样式:闭包
@modifierdef original(...): ...
这意味着装饰器只是语法糖,用于调用装饰器以后的内容做为装饰器自己的第一个参数,结果将是装饰器返回的内容。架构
为了与Python的术语一致,在咱们的示例中modifier称为装饰器,original是装饰函数,一般也被称为包装对象。app
虽然该功能最初被认为是用于方法和函数的,但实际的语法容许它修饰任何类型的对象,所以咱们将研究应用于函数、方法、生成器和类的装饰器。dom
最后一点须要注意的是,虽然装饰器的名称是正确的(毕竟,装饰器其实是在对包装函数进行更改、扩展或处理),但不要将它与装饰器设计模式混淆。ide
函数多是对能够装饰的Python对象的最简单的表示形式。咱们能够在函数上使用装饰器来应用各类逻辑——咱们能够验证参数、检查前置条件、彻底改变行为、修改其签名、缓存结果(建立原始函数的内存版本)等。
例如,咱们将建立一个实现retry机制的基本装饰器,控制一个特定的域级异常并重试必定的次数:
# decorator_function_1.pyclass ControlledException(Exception): """A generic exception on the program's domain."""def retry(operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None RETRIES_LIMIT = 3 for _ in range(RETRIES_LIMIT): try: return operation(*args, **kwargs) except ControlledException as e: logger.info("retrying %s", operation.__qualname__) last_raised = e raise last_raised return wrapped
如今能够忽略@wrap的使用,由于它将在另外一节中讨论。在for循环中使用“_”,意味着这个数字被分配给一个咱们目前不感兴趣的变量,由于它不在for循环中使用(在Python中,将被忽略的值命名为“_”是一个常见的习惯用法)。
retry装饰器不接收任何参数,因此它能够很容易地应用于任何函数,以下所示:
@retrydef run_operation(task): """Run a particular task, simulating some failures on its execution.""" return task.run()
正如一开始所解释的,在run_operation之上@retry的定义只是Python提供的语法糖,用于实际执行run_operation = retry(run_operation)。
在这个有限的示例中,咱们能够看到如何用装饰器建立一个通用的retry操做,在某些肯定的条件下(在本示例中,表示为可能与超时相关的异常),该操做将容许屡次调用装饰后的代码。
类也能够被装饰(PEP-3129),其装饰方法与语法函数的装饰方法相同。惟一的区别是,在为装饰器编写代码时,咱们必须考虑到所接收的是一个类,而不是一个函数。
一些实践者可能会认为装饰类是至关复杂的事情,这样的场景可能会损害可读性,由于咱们将在类中声明一些属性和方法,可是在幕后,装饰器可能会应用一些变化,从而呈现一个彻底不一样的类。
这种评定是正确的,但只有在装饰类技术被严重滥用的状况下成立。客观上,这与装饰功能没有什么不一样;毕竟,类和函数同样,都只是Python生态系统中的一种类型的对象而已。在5.4节中,咱们将再次审视这个问题的优缺点,可是这里只探索装饰器的优势,尤为是适用于类的装饰器的优势。
(1)重用代码和DRY原则的全部好处。类装饰器的一个有效状况是,强制多个类符合特定的接口或标准(经过只在将应用于多个类的装饰器中进行一次检查)。
(2)能够建立更小或更简单的类——这些类稍后将由装饰器进行加强。
(3)若是使用装饰器,那么须要应用到特定类上的转换逻辑将更容易维护,而不会使用更复杂的(一般是不鼓励使用的)方法,如元类。
在装饰器的全部可能应用程序中,咱们将探索一个简单的示例,以了解装饰器能够用于哪些方面。记住,这不是类装饰器的惟一应用程序类型,并且给出的代码还能够有许多其余解决方案。全部这些解决方案都有优缺点,之因此选择装饰器,是为了说明它们的用处。
回顾用于监视平台的事件系统,如今须要转换每一个事件的数据并将其发送到外部系统。然而,在选择如何发送数据时,每种类型的事件可能都有本身的特殊性。
特别是,登陆事件可能包含敏感信息,例如咱们但愿隐藏的凭据。时间戳等其余领域的字段可能也须要一些转换,由于咱们但愿以特定的格式显示它们。符合这些要求的第一次尝试很简单,就像有一个映射到每一个特定事件的类,并知道如何序列化它那样:
class LoginEventSerializer: def __init__(self, event): self.event = event def serialize(self) -> dict: return { "username": self.event.username, "password": "**redacted**", "ip": self.event.ip, "timestamp": self.event.timestamp.strftime("%Y-%m-%d %H:%M"), }class LoginEvent: SERIALIZER = LoginEventSerializer def __init__(self, username, password, ip, timestamp): self.username = username self.password = password self.ip = ip self.timestamp = timestamp def serialize(self) -> dict: return self.SERIALIZER(self).serialize()
在这里,咱们声明一个类。该类将直接映射到登陆事件,其中包含它的一些逻辑——隐藏密码字段,并根据须要格式化时间戳。
虽然这是可行的,可能开始看起来是一个不错的选择,但随着时间的推移,若要扩展系统,就会发现一些问题。
(1)类太多。随着事件数量的增多,序列化类的数量将以相同的量级增加,由于它们是一一映射的。
(2)解决方案不够灵活。若是咱们须要重用部分组件(例如,须要把密码藏在也有相似需求的另外一个类型的事件中),就不得不将其提取到一个函数,但也要从多个类中调用它,这意味着咱们没有重用那么多代码。
(3)样板文件。serialize()方法必须出如今全部事件类中,同时调用相同的代码。尽管咱们能够将其提取到另外一个类中(建立mixin),但这彷佛没有很好地使用继承。
另外一种解决方案是可以动态构造一个对象:给定一组过滤器(转换函数)和一个事件实例,该对象可以经过将过滤器应用于其字段的方式序列化它。而后,咱们只须要定义转换每种字段类型的函数,并经过组合这些函数建立序列化器。
一旦有了这个对象,咱们就能够装饰类以添加serialize()方法。该方法只会调用这些序列化对象自己:
def hide_field(field) -> str: return "**redacted**"def format_time(field_timestamp: datetime) -> str: return field_timestamp.strftime("%Y-%m-%d %H:%M")def show_original(event_field): return event_fieldclass EventSerializer: def __init__(self, serialization_fields: dict) -> None: self.serialization_fields = serialization_fields def serialize(self, event) -> dict: return { field: transformation(getattr(event, field)) for field, transformation in self.serialization_fields.items() }class Serialization: def __init__(self, **transformations): self.serializer = EventSerializer(transformations) def __call__(self, event_class): def serialize_method(event_instance): return self.serializer.serialize(event_instance) event_class.serialize = serialize_method return event_class @Serialization( username=show_original, password=hide_field, ip=show_original, timestamp=format_time, )class LoginEvent: def __init__(self, username, password, ip, timestamp): self.username = username self.password = password self.ip = ip self.timestamp = timestamp
注意,装饰器让你更容易知道如何处理每一个字段,而没必要查看另外一个类的代码。仅经过读取传递给类装饰器的参数,咱们就知道用户名和IP地址将保持不变,密码将被隐藏,时间戳将被格式化。
如今,类的代码不须要定义serialize()方法,也不须要从实现它的mixin类进行扩展,由于这些都将由装饰器添加。实际上,这多是建立类装饰器的惟一理由,由于若是不是这样的话,序列化对象多是LoginEvent的一个类属性,可是它经过向该类添加一个新方法来更改类,这使得建立该类装饰器变得不可能。
咱们还可使用另外一个类装饰器,经过定义类的属性来实现init方法的逻辑,但这超出了本例的范围。
经过使用Python 3.7+ 中的这个类装饰器(PEP-557),能够按更简洁的方式重写前面的示例,而不使用init的样板代码,以下所示:
from dataclasses import dataclassfrom datetime import datetime@Serialization( username=show_original, password=hide_field, ip=show_original, timestamp=format_time,)@dataclassclass LoginEvent: username: str password: str ip: str timestamp: datetime
既然咱们已经知道了装饰器的@语法的实际含义,就能够得出这样的结论:能够装饰的不只是函数、方法或类;实际上,任何能够定义的东西(如生成器、协同程序甚至是装饰过的对象)均可以装饰,这意味着装饰器能够堆叠起来。
前面的示例展现了如何连接装饰器。咱们先定义类,而后将@dataclass应用于该类——它将该类转换为数据类,充当这些属性的容器。以后,经过@Serialization把逻辑应用到该类上,从而生成一个新类,其中添加了新的serialize()方法。
装饰器另外一个好的用法是用于应该用做协同程序的生成器。咱们将在第7章中探讨生成器和协同程序的细节,其主要思想是,在向新建立的生成器发送任何数据以前,必须经过调用next()将后者推动到下一个yield语句。这是每一个用户都必须记住的手动过程,所以很容易出错。咱们能够轻松建立一个装饰器,使其接收生成器做为参数,调用next(),而后返回生成器。
至此,咱们已经将装饰器看做Python中的一个强大工具。若是咱们能够将参数传递给装饰器,使其逻辑更加抽象,那么其功能可能会更增强大。
有几种实现装饰器的方法能够接收参数,可是接下来咱们只讨论最多见的方法。第一种方法是将装饰器建立为带有新的间接层的嵌套函数,使装饰器中的全部内容深刻一层。第二种方法是为装饰器使用一个类。
一般,第二种方法更倾向于可读性,由于从对象的角度考虑,其要比3个或3个以上使用闭包的嵌套函数更容易。可是,为了完整起见,咱们将对这两种方法进行探讨,以便你能够选择使用最适合当前问题的方法。
粗略地说,装饰器的基本思想是建立一个返回函数的函数(一般称为高阶函数)。在装饰器主体中定义的内部函数将是实际被调用的函数。
如今,若是但愿将参数传递给它,就须要另外一间接层。第一个函数将接收参数,在该函数中,咱们将定义一个新函数(它将是装饰器),而这个新函数又将定义另外一个新函数,即装饰过程返回的函数。这意味着咱们将至少有3层嵌套函数。
若是你到目前为止还不明白上述内容的含义,也不用担忧,待查看下面给出的示例以后,就会明白了。
第一个示例是,装饰器在一些函数上实现重试功能。这是个好主意,只是有个问题:实现不容许指定重试次数,只容许在装饰器中指定一个固定的次数。
如今,咱们但愿可以指出每一个示例有多少次重试,也许甚至能够为这个参数添加一个默认值。为了实现这个功能,咱们须要用到另外一层嵌套函数——先用于参数,而后用于装饰器自己。
这是由于以下代码:
@retry(arg1, arg2,... )
必须返回装饰器,由于@语法将把计算结果应用到要装饰的对象上。从语义上讲,它能够翻译成以下内容:
<original_function> = retry(arg1, arg2, ....)(<original_function>)
除了所需的重试次数,咱们还能够指明但愿控制的异常类型。支持新需求的新版本代码多是这样的:
RETRIES_LIMIT = 3def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None): allowed_exceptions = allowed_exceptions or (ControlledException,) def retry(operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None for _ in range(retries_limit): try: return operation(*args, **kwargs) except allowed_exceptions as e: logger.info("retrying %s due to %s", operation, e) last_raised = e raise last_raised return wrapped return retry
下面是这个装饰器如何应用于函数的一些示例,其中显示了它接收的不一样选项:
# decorator_parametrized_1.py@with_retry()def run_operation(task): return task.run()@with_retry(retries_limit=5)def run_with_custom_retries_limit(task): return task.run()@with_retry(allowed_exceptions=(AttributeError,))def run_with_custom_exceptions(task): return task.run()@with_retry( retries_limit=4, allowed_exceptions=(ZeroDivisionError, AttributeError) )def run_with_custom_parameters(task): return task.run()
前面的示例须要用到3层嵌套函数。首先,这将是一个用于接收咱们想要使用的装饰器的参数。在这个函数中,其他的函数是使用这些参数和装饰器逻辑的闭包。
更简洁的实现方法是用一个类定义装饰器。在这种状况下,咱们能够在__init__方法中传递参数,而后在名为__call__的魔法方法上实现装饰器的逻辑。
装饰器的代码以下所示:
class WithRetry: def __init__(self, retries_limit=RETRIES_LIMIT, allowed_exceptions=None): self.retries_limit = retries_limit self.allowed_exceptions = allowed_exceptions or(ControlledException,) def __call__(self, operation): @wraps(operation) def wrapped(*args, **kwargs): last_raised = None for _ in range(self.retries_limit): try: return operation(*args, **kwargs) except self.allowed_exceptions as e: logger.info("retrying %s due to %s", operation, e) last_raised = e raise last_raised return wrapped
这个装饰器能够像以前的同样应用,就像这样:
@WithRetry(retries_limit=5)def run_with_custom_retries_limit(task): return task.run()
注意Python语法在这里是如何起做用的,这一点很重要。首先,咱们建立对象,这样在应用@操做以前,对象已经建立好了,而且其参数传递给它了,用这些参数初始化这个对象,如init方法中定义的那样。在此以后,咱们将调用@操做,这样该对象将包装名为run_with_custom_reries_limit的函数,而这意味着它将被传递给call这个魔法方法。
在call这个魔法方法中,咱们定义了装饰器的逻辑,就像一般所作的那样——包装了原始函数,返回一个新的函数,其中包含所要的逻辑。
本节介绍一些充分利用装饰器的常见模式。在有些常见的场景中使用装饰器是个很是好的选择。
可用于应用程序的装饰器数不胜数,下面仅列举几个最多见或相关的。
(1)转换参数。更改函数的签名以公开更好的API,同时封装关于如何处理和转换参数的详细信息。
(2)跟踪代码。记录函数及其参数的执行状况。
(3)验证参数。
(4)实现重试操做。
(5)经过把一些(重复的)逻辑移到装饰器中来简化类。
接下来详细讨论前两个应用程序。
前文提到,装饰器能够用来验证参数(甚至在DbC的概念下强制一些前置条件或后置条件),所以你可能已经了解到,这是一些处理或者操控参数时使用装饰器的经常使用方法。
特别是,在某些状况下,咱们会发现本身反复建立相似的对象,或者应用相似的转换,而咱们但愿将这些转换抽象掉。大多数时候,咱们能够经过简单地用装饰器实现这一点。
在本节中讨论跟踪时,咱们将提到一些更通用的内容,这些内容与处理所要监控的函数的执行有关,具体是指:
(1)实际跟踪函数的执行(例如,经过记录函数执行的行);
(2)监控函数的一些指标(如CPU使用量或内存占用);
(3)测量函数的运行时间;
(4)函数被调用时的日志,以及传递给它的参数。
咱们将在5.2节剖析一个简单的装饰器示例,该示例记录了函数的执行状况,包括函数名和运行时间。
本文摘自《编写整洁的Python代码》
本书介绍Python软件工程的主要实践和原则,旨在帮助读者编写更易于维护和更整洁的代码。全书共10章:第1章介绍Python语言的基础知识和搭建Python开发环境所需的主要工具;第2章描述Python风格代码,介绍Python中的第一个习惯用法;第3章总结好代码的通常特征,回顾软件工程中的通常原则;第4章介绍一套面向对象软件设计的原则,即SOLID原则;第5章介绍装饰器,它是Python的**特性之一;第6章探讨描述符,介绍如何经过描述符从对象中获取更多的信息;第7章和第8章介绍生成器以及单元测试和重构的相关内容;第9章回顾Python中最多见的设计模式;第10章再次强调代码整洁是实现良好架构的基础。
本书适合全部Python编程爱好者、对程序设计感兴趣的人,以及其余想学习更多Python知识的软件工程的从业人员。