懒人必备,五个高效Python装饰器

2023-09-18 15:22:07 浏览数 (2)

到目前为止,Python是我最喜欢的编程语言,因为它的语法简单,在机器学习和网络开发等各个领域的应用也很强大。

虽然我已经写了很多的代码,但除非绝对必要,我很少使用装饰器,比如使用@staticmethod装饰器来表示一个类中的静态方法。

然而,在最近的一次合并申请审查中,我的同事在我的一个函数中引入了一个定时器装饰器,这改变了我的看法。这激发了我对装饰器所能提供的众多其他功能的好奇心,提升了代码的清洁度和可读性。

因此,在这篇短文中,我们将探讨Python包装器的概念,并介绍五个可以改善我们Python开发过程的例子。

Python包装器

Python 封装器是添加到另一个函数中的函数,然后可以添加额外的功能或修改其行为,而不直接改变其源代码。它们通常以装饰器的形式实现,这是一种特殊的函数,将另一个函数作为输入,并对其功能进行一些修改。

封装器函数在各种情况下都很有用:

  • 功能扩展(Functionality Extension):我们可以通过用装饰器包装我们的函数来增加诸如日志、性能测量或缓存等功能。
  • 代码可重用性:我们可以将一个封装函数甚至一个类应用于多个实体,你可以避免代码的重复,并确保不同组件的行为一致。
  • 行为修改:我们可以拦截输入参数,例如,验证输入变量,而不需要许多assert行。

例子

让我告诉你一些例子,这些例子使包装器成为我们日常工作中的必备品:

1 — Timer

这个封装器函数测量一个函数的执行时间,并打印出已用的时间。它对于剖析和优化代码非常有用。

代码语言:javascript复制
import time

def timer(func):
    def wrapper(*args, **kwargs):
        # start the timer
        start_time = time.time()
        # call the decorated function
        result = func(*args, **kwargs)
        # remeasure the time
        end_time = time.time()
        # compute the elapsed time and print it
        execution_time = end_time - start_time
        print(f"Execution time: {execution_time} seconds")
        # return the result of the decorated function execution
        return result
    # return reference to the wrapper function
    return wrapper

为了在Python中创建装饰器,我们需要定义一个叫做timer的函数,该函数接收一个叫做func的参数,表示它是一个装饰器函数。在定时器函数中,我们定义另一个叫做`wrapper'的函数,它接收通常传递给我们想要装饰的函数的参数。

在wrapper函数中,我们使用提供的参数调用所需的函数。我们可以这样做:result = func(*args, **kwargs)

最后,包装器函数返回被装饰的函数的执行结果。装饰器函数应该返回一个引用到我们刚刚创建的封装器函数。

要利用装饰器,你可以使用@符号将其应用于所需的函数。

代码语言:javascript复制
@timer
def train_model():
    print("Starting the model training function...")
    # simulate a function execution by pausing the program for 5 seconds    # 喜欢就关注@公众号:数据STUDIO
    time.sleep(5) 
    print("Model training completed!")

train_model() 
代码语言:javascript复制
Starting the model training function…
Model Training completed!
Execution time: 5.006425619125366 seconds

2 — Debugger

可以创建一个额外的有用的包装函数,通过打印每个函数的输入和输出来促进调试。这种方法使我们能够深入了解各种函数的执行流程,而不必用多个打印语句来干扰我们的应用程序。

代码语言:javascript复制
def debug(func):
    def wrapper(*args, **kwargs):
        # print the fucntion name and arguments
        print(f"Calling {func.__name__} with args: {args} kwargs: {kwargs}")
        # call the function
        result = func(*args, **kwargs)
        # print the results
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

我们可以使用__name__参数来获得被调用函数的名称,然后使用argskwargs参数来打印传递给函数的内容。

代码语言:javascript复制
@debug
def add_numbers(x, y):
    return x   y
add_numbers(7, y=5,)  
# Output: Calling add_numbers with args: (7) kwargs: {'y': 5} n add_numbers returned: 12

3 — 异常处理程序

封装器的 exception_handler 将捕获在 divide 函数中引发的任何异常,并对其进行相应处理。

我们可以根据你的要求定制包装函数中的异常处理方式,例如记录异常或执行额外的错误处理步骤。

代码语言:javascript复制
def exception_handler(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            # Handle the exception
            print(f"An exception occurred: {str(e)}")
            # Optionally, perform additional error handling or logging
            # Reraise the exception if needed
    return wrapper

这对于简化我们的代码和建立一个统一的处理异常和记录错误的程序变得非常有用。

代码语言:javascript复制
@exception_handler
def divide(x, y):
    result = x / y
    return result
divide(10, 0)  
# Output: An exception occurred: division by zero

4 — Input Validator

这个封装函数根据指定的条件或数据类型验证一个函数的输入参数。它可以用来确保输入数据的正确性和一致性。

另一种方法是在我们想要验证输入数据的函数内创建无数的assert行,来实现这一目的。

为了给装饰添加验证,我们需要用另一个函数来包装装饰函数,该函数接收一个或多个验证函数作为参数。这些验证函数负责检查输入值是否符合某些标准或条件。

validate_input函数本身现在作为一个装饰器。在封装函数中,inputkeyword的参数会根据提供的验证函数进行检查。如果任何参数没有通过验证,就会引发一个 "ValueError",并显示无效参数的信息。

代码语言:javascript复制
def validate_input(*validations):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i, val in enumerate(args):
                if i < len(validations):
                    if not validations[i](val):
                        raise ValueError(f"Invalid argument: {val}")
            for key, val in kwargs.items():
                if key in validations[len(args):]:
                    if not validations[len(args):][key](val):
                        raise ValueError(f"Invalid argument: {key}={val}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

为了调用验证的输入,我们需要定义验证函数。例如,可以使用两个验证函数。第一个函数(lambda x: x > 0)检查参数x是否大于0,第二个函数(lambda y: isinstance(y, str))检查参数y是否属于字符串类型。

确保验证函数的顺序与它们要验证的参数的顺序相一致是很重要的。

代码语言:javascript复制
@validate_input(lambda x: x > 0, lambda y: isinstance(y, str))
def divide_and_print(x, message):
    print(message)
    return 1 / x

divide_and_print(5, "Hello!")  # Output: Hello! 1.0

5 — Retry

这个封装器会重试一个函数的执行,并在重试之间有一定的延迟。在处理网络或API调用时,它可能会因为临时问题而偶尔失败,因此很有用。

为了实现这一点,我们可以为我们的装饰器定义另一个包装函数,与我们之前的例子类似。然而,这次我们不是将验证函数作为输入变量,而是传递特定的参数,如max_attempsdelay

当装饰函数被调用时,wrapper函数被调用。它记录了尝试的次数(从0开始)并进入一个while循环。循环尝试执行装饰后的函数,如果成功,立即返回结果。然而,如果发生异常,它就会增加尝试计数器,并打印出一条错误信息,指出尝试次数和发生的具体异常。然后,它使用time.sleep等待指定的延迟,然后再次尝试该函数。

代码语言:javascript复制
import time

def retry(max_attempts, delay=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts  = 1
                    print(f"Attempt {attempts} failed: {e}")
                    time.sleep(delay)
            print(f"Function failed after {max_attempts} attempts")
        return wrapper
    return decorator

为了调用该函数,我们可以指定最大的尝试次数和每次调用函数之间的时间长度(秒)。

代码语言:javascript复制
@retry(max_attempts=3, delay=2)
def fetch_data(url):
    print("Fetching the data..")
    # raise timeout error to simulate a server not responding..
    raise TimeoutError("Server is not responding.")
fetch_data("https://example.com/data")  
# Retries 3 times with a 2-second delay between attempts

总结

Python包装器是强大的工具,可以提升你的Python编程体验。通过使用包装器,你可以简化复杂的任务,改善代码的可读性,并提高生产力。

在这篇文章中,我们探讨了五个Python包装器的例子:

  • 计时器包装器
  • 调试器封装器
  • 异常处理程序包装器
  • 输入验证器包装器
  • 函数重试封装器

在你的项目中加入这些包装器将帮助你写出更干净、更有效的Python代码,并使你的编程技巧更上一层楼。

0 人点赞