Python装饰器是什么,python装饰器的作用和功能

  Python装饰器是什么,python装饰器的作用和功能

  Python很早就在PEP-318中引入了装饰器——。作为一种简化函数和方法定义的机制,这些函数和方法必须在初始定义后进行修改。

  这样做的最初动机之一是使用classmethod和staticmethod等函数来转换方法的原始定义,但它们需要额外的一行代码来修改函数的原始定义。

  一般来说,每次我们要对一个函数应用变换时,都要用修饰符function调用它,然后重新赋值给函数最初定义时的名字。

  比如,假设有一个函数叫original,其上有一个函数改变original的行为(叫modifier),那么我们要这样写:

  定义原始(.):

  .

  Original=modifier(original)(相关免费学习推荐:python视频教程)

  请注意我们是如何更改函数并将它重新分配给相同的名称的。这很混乱,容易出错(假设有人忘记了重新分配函数,或者重新分配了函数,但不是在函数定义之后的那一行,而是在更远的地方),而且很麻烦。为此,Python语言增加了一些语法支持。

  前面的示例可以改写如下:

  @修饰符

  定义原始(.):

  .这意味着decorator只是语法糖,用来调用decorator之后的内容作为decorator本身的第一个参数,结果将是decorator返回的内容。

  为了与Python的术语保持一致,在我们的例子中,modifier称为decorator,original是一个修饰函数,通常也称为wrapper对象。

  虽然这个函数最初被认为是用于方法和函数的,但实际的语法允许它修饰任何类型的对象,所以我们将研究应用于函数、方法、生成器和类的decorators。

  最后需要注意的是,虽然装饰器的名称是正确的(毕竟装饰器实际上是在改变、扩展或处理包装器函数),但不应该与装饰器设计模式混淆。

  5.1.1装饰器函数

  函数可能是可以修饰的Python对象的最简单的表示。我们可以在函数上使用decorators来应用各种逻辑。——我们可以验证参数、检查前提条件、完全改变行为、修改它们的签名、缓存结果(创建原始函数的内存版本)等。

  例如,我们将创建一个实现重试机制的基本装饰器,控制一个特定的域级异常并重试一定次数:

  #装饰者_功能_1.py

  class ControlledException(异常):

  程序域上的一般异常。

  定义重试(操作):

  @wraps(操作)

  def wrapped(*args,**kwargs):

  last_raised=无

  重试次数_限制=3

  for _ in范围(重试次数_限制):

  尝试:

  返回操作(*args,**kwargs)

  除了e:

  logger.info(重试%s ,操作。__qualname__)

  最后一次提出=e

  提高最后_提高

  Return wrapped现在可以忽略@wrap的使用,这将在另一节讨论。在for循环中使用“_”,意味着这个数被赋给了一个我们目前不感兴趣的变量,因为它在for循环中没有使用(在Python中,把被忽略的值命名为“_”是一个常见的习惯用法)。

  Retry decorator不接收任何参数,因此它可以很容易地应用于任何函数,如下所示:

  @重试

  定义运行_操作(任务):

  “运行一个特定的任务,模拟执行过程中的一些失败。”

  Return task.run()正如开头所解释的,run_operation上面的@retry的定义只是Python提供的语法糖,用来实际执行run _ operation=retry(run _ operation)。

  在这个有限的例子中,我们可以看到如何用装饰器创建一个通用的重试操作。在某些条件下(在本例中,它被表示为可能与超时相关的异常),该操作将允许更多

  次调用装饰后的代码。

  5.1.2装饰类

  类也可以被装饰(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_field

  class 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 dataclass

  from datetime import datetime

  @Serialization(

   username=show_original,

   password=hide_field,

   ip=show_original,

   timestamp=format_time,

  )

  @dataclass

  class LoginEvent:

   username: str

   password: str

   ip: str

   timestamp: datetime

5.1.3其他类型的装饰器

  既然我们已经知道了装饰器的@语法的实际含义,就可以得出这样的结论:可以装饰的不仅是函数、方法或类;实际上,任何可以定义的东西(如生成器、协同程序甚至是装饰过的对象)都可以装饰,这意味着装饰器可以堆叠起来。

  前面的示例展示了如何链接装饰器。我们先定义类,然后将@dataclass应用于该类——它将该类转换为数据类,充当这些属性的容器。之后,通过@Serialization把逻辑应用到该类上,从而生成一个新类,其中添加了新的serialize()方法。

  装饰器另一个好的用法是用于应该用作协同程序的生成器。我们将在第7章中探讨生成器和协同程序的细节,其主要思想是,在向新创建的生成器发送任何数据之前,必须通过调用next()将后者推进到下一个yield语句。这是每个用户都必须记住的手动过程,因此很容易出错。我们可以轻松创建一个装饰器,使其接收生成器作为参数,调用next(),然后返回生成器。

  5.1.4将参数传递给装饰器

  至此,我们已经将装饰器看作Python中的一个强大工具。如果我们可以将参数传递给装饰器,使其逻辑更加抽象,那么其功能可能会更加强大。

  有几种实现装饰器的方法可以接收参数,但是接下来我们只讨论最常见的方法。第一种方法是将装饰器创建为带有新的间接层的嵌套函数,使装饰器中的所有内容深入一层。第二种方法是为装饰器使用一个类。

  通常,第二种方法更倾向于可读性,因为从对象的角度考虑,其要比3个或3个以上使用闭包的嵌套函数更容易。但是,为了完整起见,我们将对这两种方法进行探讨,以便你可以选择使用最适合当前问题的方法。

  1.带有嵌套函数的装饰器

  粗略地说,装饰器的基本思想是创建一个返回函数的函数(通常称为高阶函数)。在装饰器主体中定义的内部函数将是实际被调用的函数。

  现在,如果希望将参数传递给它,就需要另一间接层。第一个函数将接收参数,在该函数中,我们将定义一个新函数(它将是装饰器),而这个新函数又将定义另一个新函数,即装饰过程返回的函数。这意味着我们将至少有3层嵌套函数。

  如果你到目前为止还不明白上述内容的含义,也不用担心,待查看下面给出的示例之后,就会明白了。

  第一个示例是,装饰器在一些函数上实现重试功能。这是个好主意,只是有个问题:实现不允许指定重试次数,只允许在装饰器中指定一个固定的次数。

  现在,我们希望能够指出每个示例有多少次重试,也许甚至可以为这个参数添加一个默认值。为了实现这个功能,我们需要用到另一层嵌套函数——先用于参数,然后用于装饰器本身。

  这是因为如下代码:

  

@retry(arg1, arg2,... )
必须返回装饰器,因为@语法将把计算结果应用到要装饰的对象上。从语义上讲,它可以翻译成如下内容:

  

<original_function> = retry(arg1, arg2, ....)(<original_function>)
除了所需的重试次数,我们还可以指明希望控制的异常类型。支持新需求的新版本代码可能是这样的:

  

RETRIES_LIMIT = 3

  def 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=(ZeropisionError, AttributeError)

  )

  def run_with_custom_parameters(task):

   return task.run()

2.装饰器对象

  前面的示例需要用到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这个魔法方法中,我们定义了装饰器的逻辑,就像通常所做的那样——包装了原始函数,返回一个新的函数,其中包含所要的逻辑。

  5.1.5充分利用装饰器

  本节介绍一些充分利用装饰器的常见模式。在有些常见的场景中使用装饰器是个非常好的选择。

  可用于应用程序的装饰器数不胜数,下面仅列举几个最常见或相关的。

  (1)转换参数。更改函数的签名以公开更好的API,同时封装关于如何处理和转换参数的详细信息。

  (2)跟踪代码。记录函数及其参数的执行情况。

  (3)验证参数

  (4)实现重试操作

  (5)通过把一些(重复的)逻辑移到装饰器中来简化类

  接下来详细讨论前两个应用程序。

  1.转换参数

  前文提到,装饰器可以用来验证参数(甚至在DbC的概念下强制一些前置条件或后置条件),因此你可能已经了解到,这是一些处理或者操控参数时使用装饰器的常用方法。

  特别是,在某些情况下,我们会发现自己反复创建类似的对象,或者应用类似的转换,而我们希望将这些转换抽象掉。大多数时候,我们可以通过简单地用装饰器实现这一点。

  2.跟踪代码

  在本节中讨论跟踪时,我们将提到一些更通用的内容,这些内容与处理所要监控的函数的执行有关,具体是指:

  (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知识的软件工程的从业人员。

  

相关免费学习推荐:python教程(视频)

  

以上就是Python中的装饰器是什么?装饰器是如何工作的?的详细内容,更多请关注盛行IT软件开发工作室其它相关文章!

  

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: