Python代码整洁之道--使用装饰器改进代码

本文为英文书籍 Clean Code in Python Chapter 5 Using Decorators to Improve Our Code 学习笔记,建议直接看原书html

  • 了解Python中装饰器的工做原理
  • 学习如何实现应用于函数和类的装饰器
  • 有效地实现装饰器,避免常见的执行错误
  • 分析如何用装饰器避免代码重复(DRY)
  • 研究装饰器如何为关注点分离作出贡献
  • 优秀装饰器实例分析
  • 回顾常见状况、习惯用法或模式,了解什么时候装饰器是正确的选择

虽然通常见到装饰器装饰的是方法和函数,但实际容许装饰任何类型的对象,所以咱们将探索应用于函数、方法、生成器和类的装饰器。python

还要注意,不要将装饰器与装饰器设计模式(Decorator Pattern)混为一谈。设计模式

函数装饰

函数多是能够被装饰的Python对象中最简单的表示形式。咱们能够在函数上使用装饰器来达成各类逻辑——能够验证参数、检查前提条件、彻底改变行为、修改签名、缓存结果(建立原始函数的存储版本)等等。缓存

做为示例,咱们将建立实现重试机制的基本装饰器,控制特定的域级异常(domain-level exception)并重试必定次数:bash

# decorator_function_1.py
import logging
from functools import wraps

logger = logging.getLogger(__name__)


class ControlledException(Exception):
    """A generic exception on the program's domain."""
    pass


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

复制代码

能够暂时忽略@wraps,以后再介绍
retry装饰器使用例子:app

@retry
def run_operation(task):
   """Run a particular task, simulating some failures on its execution."""
   return task.run()
复制代码

由于装饰器只是提供的一种语法糖,实际上等于run_operation = retry(run_operation)
比较经常使用的超时重试,即可以这样实现。dom

定义一个带参数的装饰器

咱们用一个例子详细阐述下接受参数的处理过程。 假设你想写一个装饰器,给函数添加日志功能,同时容许用户指定日志的级别和其余的选项。 下面是这个装饰器的定义和使用示例:ide

from functools import wraps
import logging

def logged(level, name=None, message=None):
    """ Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren't specified, they default to the function's module and name. """
    def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
    return decorate

# Example use
@logged(logging.DEBUG)
def add(x, y):
    return x + y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

复制代码

初看起来,这种实现看上去很复杂,可是核心思想很简单。 最外层的函数 logged() 接受参数并将它们做用在内部的装饰器函数上面。 内层的函数 decorate() 接受一个函数做为参数,而后在函数上面放置一个包装器。 这里的关键点是包装器是可使用传递给 logged() 的参数的。函数

定义一个接受参数的包装器看上去比较复杂主要是由于底层的调用序列。特别的,若是你有下面这个代码:学习

@decorator(x, y, z)
def func(a, b):
    pass
复制代码

装饰器处理过程跟下面的调用是等效的;

def func(a, b):
    pass
func = decorator(x, y, z)(func)
decorator(x, y, z) 的返回结果必须是一个可调用对象,它接受一个函数做为参数并包装它
复制代码

类装饰

有些人认为,装饰类是比较复杂的事情,并且这样的方案可能危及可读性。由于咱们在类中声明一些属性和方法,可是装饰器可能会改变它们的行为,呈现出彻底不一样的类。

在这种技术被严重滥用的状况下,这种评价是正确的。客观地说,这与装饰函数没有什么不一样;毕竟,类只是Python生态系统中的另外一种类型的对象,就像函数同样。咱们将在标题为“装饰器和关注点分离”的章节中一块儿回顾这个问题的利弊,可是如今,咱们将探讨类的装饰器的好处:

  • 代码重用和DRY。一个恰当的例子是,类装饰器强制多个类符合某个特定的接口或标准(经过在装饰器中仅检查一次,而能应用于多个类)
  • 能够建立更小或更简单的类,而经过装饰器加强这些类
  • 类的转换逻辑将更容易维护,而不是使用更复杂(一般是理所固然不被鼓励的)的方法,好比元类

回顾监视平台的事件系统,咱们如今须要转换每一个事件的数据并将其发送到外部系统。 可是,在选择如何发送数据时,每种类型的事件可能都有本身的特殊性。

特别是,登陆的事件可能包含敏感信息,如登陆信息须要隐藏, 时间戳等其余字段也可能须要特定的格式显示。

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()
复制代码

在这里,咱们声明一个类,该类将直接映射到登陆事件,包含其逻辑——隐藏密码字段,并根据须要格式化时间戳。

虽然这种方法可行,并且看起来是个不错的选择,可是随着时间的推移,想要扩展咱们的系统,就会发现一些问题:

  • 类太多:随着事件数量的增长,序列化类的数量将以相同的数量级增加,由于它们是一一映射的。
  • 解决方案不够灵活:若是须要重用组件的一部分(例如,咱们须要在另外一种事件中隐藏密码),则必须将其提取到一个函数中,还要从多个类中重复调用它,这意味着咱们没有作到代码重用。
  • Boilerplate:serialize()方法必须出如今全部事件类中,调用相同的代码。虽然咱们能够将其提取到另外一个类中(建立mixin),但它彷佛不是继承利用的好方式( Although we can extract this into another class (creating a mixin), it does not seem like a good use of inheritance.)。

另外一种解决方案是,给定一组过滤器(转换函数)和一个事件实例,可以动态构造对象,该对象可以经过滤器对其字段序列化。而后,咱们只须要定义转换每种类型的字段的函数,而且经过组合这些函数中的许多函数来建立序列化程序。

一旦有了这个对象,咱们就能够装饰类,以便添加serialize()方法,该方法将只调用这些Serialization对象自己:

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
复制代码

待续。。。

相关文章
相关标签/搜索