大家好呀,我是阿潘。
首先,每个开发人员的目标都是让事情正常进行。慢慢地,我们担心可读性和可扩展性。这是我们第一次开始考虑装饰器的时候。
装饰器是为函数提供额外行为的绝佳方式。
使用装饰器,你会惊讶地发现可以减少代码重复并提高可读性。
以下是我在几乎每个数据密集型项目中使用的五个最常用的方法。
1.重试装饰器
在数据科学项目和软件开发项目中,有很多我们依赖外部系统的情况。事情并不总是在我们的控制之中。
当意外事件发生时,我们可能希望我们的代码等待一段时间,让外部系统自行纠正并重新运行。
我更喜欢在 python 装饰器中实现这个重试逻辑,这样我就可以注释任何函数来应用重试行为。
这是重试装饰器的代码。
import time
from functools import wraps
def retry(max_tries=3, delay_seconds=1):
def decorator_retry(func):
@wraps(func)
def wrapper_retry(*args, **kwargs):
tries = 0
while tries < max_tries:
try:
return func(*args, **kwargs)
except Exception as e:
tries += 1
if tries == max_tries:
raise e
time.sleep(delay_seconds)
return wrapper_retry
return decorator_retry
@retry(max_tries=5, delay_seconds=2)
def call_dummy_api():
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
return response
在上面的代码中,我们尝试获取 API 响应。如果失败,我们将重试相同的任务 5 次。在每次重试之间,我们等待 2 秒。
2.缓存函数结果
我们代码库的某些部分很少改变它们的行为。然而,它可能会占用我们很大一部分计算能力。在这种情况下,我们可以使用装饰器来缓存函数调用。
如果输入相同,该函数将只运行一次。在随后的每次运行中,结果将从缓存中提取。因此,我们不必一直执行昂贵的计算。
def memoize(func):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
else:
result = func(*args)
cache[args] = result
return result
return wrapper
装饰器使用字典,存储函数参数,并返回值。当我们执行此功能时,装饰器将检查字典中的先前结果。只有在之前没有存储值时才会调用实际函数。
下面是一个计算斐波那契数列的函数。由于这是一个循环函数,所以调用的同一个函数会执行多次。但是通过缓存,我们可以加快这个过程。
@memoize
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
以下是使用和不使用缓存时此函数的执行时间。请注意,缓存版本只需要几分之一毫秒即可运行,而非缓存版本几乎需要一分钟。
Function slow_fibonacci took 53.05560088157654 seconds to run.
Function fast_fibonacci took 7.772445678710938e-05 seconds to run.
使用字典来保存以前的执行数据是一种直接的方法。但是,有一种更复杂的方法来存储缓存数据。您可以使用内存数据库,例如 Redis。
3.计时功能
这一点并不奇怪。在处理数据密集型函数时,我们渴望了解运行需要多长时间。
通常的做法是收集两个时间戳,一个在函数的开头,另一个在函数的结尾。然后我们可以计算持续时间并将其与返回值一起打印。
但是一次又一次地为多个函数这样做是一件麻烦事。
相反,我们可以让装饰者来做。我们可以注释任何需要打印持续时间的函数。
这是一个 Python 装饰器示例,它在调用函数时打印函数的运行时间:
import time
def timing_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function {func.__name__} took {end_time - start_time} seconds to run.")
return result
return wrapper
您可以使用此装饰器来计时函数的执行:
@timing_decorator
def my_function():
# some code here
time.sleep(1) # simulate some time-consuming operation
return
调用该函数将打印运行所需的时间。
my_function()
>>> Function my_function took 1.0019128322601318 seconds to run.
4.记录函数调用这个在很大程度上是前一个装饰器的扩展。但它有一些特殊用途。
如果您遵循软件设计原则,您会喜欢单一职责原则。这实质上意味着每个功能将承担其唯一的责任。
当您以这种方式设计代码时,您还希望记录函数的执行信息。这就是日志装饰器派上用场的地方。
下面的例子说明了这一点。
import logging
import functools
logging.basicConfig(level=logging.INFO)
def log_execution(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Executing {func.__name__}")
result = func(*args, **kwargs)
logging.info(f"Finished executing {func.__name__}")
return result
return wrapper
@log_execution
def extract_data(source):
# extract data from source
data = ...
return data
@log_execution
def transform_data(data):
# transform data
transformed_data = ...
return transformed_data
@log_execution
def load_data(data, target):
# load data into target
...
def main():
# extract data
data = extract_data(source)
# transform data
transformed_data = transform_data(data)
# load data
load_data(transformed_data, target)
上面的代码是 ETL 管道的简化版本。我们有三个独立的函数来处理每个提取、转换和加载。我们已经使用我们的 log_execution 装饰器包装了它们中的每一个。
现在,无论何时执行代码,您都会看到类似这样的输出:
INFO:root:Executing extract_data
INFO:root:Finished executing extract_data
INFO:root:Executing transform_data
INFO:root:Finished executing transform_data
INFO:root:Executing load_data
INFO:root:Finished executing load_data
我们还可以在这个装饰器中打印执行时间。但我希望将它们都放在不同的装饰器中。这样,我就可以选择将哪一个(或两者)用于一个函数。
以下是如何在单个函数上使用多个装饰器。
@log_execution
@timing_decorator
def my_function(x, y):
time.sleep(1)
return x + y
5.通知装饰器
最后,生产系统中一个非常有用的装饰器是通知装饰器。
再一次,即使重试几次,即使是经过良好测试的代码库也会失败。当发生这种情况时,我们需要通知相关人员以迅速采取行动。
如果您曾经构建过数据管道并希望它能永远正常工作,那么这并不是什么新鲜事。
每当内部函数执行失败时,以下装饰器都会发送一封电子邮件。在您的案例中,它不一定是电子邮件通知。您可以将其配置为发送 Teams/slack 通知。
import smtplib
import traceback
from email.mime.text import MIMEText
def email_on_failure(sender_email, password, recipient_email):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
# format the error message and traceback
err_msg = f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
# create the email message
message = MIMEText(err_msg)
message['Subject'] = f"{func.__name__} failed"
message['From'] = sender_email
message['To'] = recipient_email
# send the email
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
smtp.login(sender_email, password)
smtp.sendmail(sender_email, recipient_email, message.as_string())
# re-raise the exception
raise
return wrapper
return decorator
@email_on_failure(sender_email='your_email@gmail.com', password='your_password', recipient_email='recipient_email@gmail.com')
def my_function():
# code that might fail
结论装饰器是将新行为应用于我们的函数的一种非常方便的方法。没有它们,就会有很多代码重复。
在这篇文章中,我讨论了我最常用的装饰器。您可以根据您的特定需求扩展这些。例如,您可以使用 Redis 服务器来存储缓存响应而不是字典。这将使您能够更好地控制数据,例如持久性。或者您可以调整代码以逐步增加重试装饰器中的等待时间。
在我所有的项目中,我都使用了这些装饰器的某些版本。尽管它们的行为略有不同,但这些是我经常使用装饰器的共同目标。
我希望这篇文章对你有所帮助。
本篇文章翻译自:https://towardsdatascience.com/python-decorators-for-data-science-6913f717669a