python 在函数失败时保存函数中所有中间变量的装饰器/包

e3bfsja2  于 2023-01-16  发布在  Python
关注(0)|答案(1)|浏览(172)

我发现自己经常会遇到这样的问题。我有一个函数

def compute(input):
    result = two_hour_computation(input)
    result = post_processing(result)
    return result

并且post_processing(result)失败。现在要做的显而易见的事情是将函数更改为

import pickle

def compute(input):
    result = two_hour_computation(input)
    pickle.dump(result, open('intermediate_result.pickle', 'wb'))
    result = post_processing(result)
    return result

但是我通常不记得我所有的函数都是这样写的。我希望我有一个像这样的装饰器:

@return_intermediate_results_if_something_goes_wrong
def compute(input):
    result = two_hour_computation(input)
    result = post_processing(result)
    return result

有这样的东西吗?我在谷歌上找不到。

4ioopgfo

4ioopgfo1#

函数的"外部"在运行时无论如何都不能访问函数内部局部变量的状态,所以这不能用装饰器来解决。
无论如何,我认为捕捉错误和保存有价值的中间结果的责任应该由程序员显式地完成,如果你"忘记"做这件事,那对你来说一定不是那么重要。
也就是说,类似于 "在A、B或C引发异常的情况下执行X" 的情况是上下文管理器的典型用例。您可以编写自己的上下文管理器,充当中间结果的桶(代替变量),并在异常退出时执行一些save操作。
大概是这样的

from __future__ import annotations
from types import TracebackType
from typing import Generic, Optional, TypeVar

T = TypeVar("T")

class Saver(Generic[T]):
    def __init__(self, initial_value: Optional[T] = None) -> None:
        self._value = initial_value

    def __enter__(self) -> Saver[T]:
        return self

    def __exit__(
        self,
        exc_type: Optional[type[BaseException]],
        exc_val: Optional[BaseException],
        exc_tb: Optional[TracebackType],
    ) -> None:
        if exc_type is not None:
            self.save()

    def save(self) -> None:
        print(f"saved {self.value}!")

    @property
    def value(self) -> T:
        if self._value is None:
            raise RuntimeError
        return self._value

    @value.setter
    def value(self, value: T) -> None:
        self._value = value

显然,您可以这样做,而不是将print(f"saved {self.value}!")放在save中:

with open('intermediate_result.pickle', 'wb') as f:
            pickle.dump(self.value, f)

现在,您需要记住的是将这些操作 Package 在with-语句中,并将中间结果赋给上下文管理器的value属性。

def x_times_2(x: float) -> float:
    return x * 2

def one_over_x_minus_2(x: float) -> float:
    return 1 / (x - 2)

def main() -> None:
    with Saver(1.) as s:
        s.value = x_times_2(s.value)
        s.value = one_over_x_minus_2(s.value)
    print(s.value)

if __name__ == "__main__":
    main()

输出:

saved 2.0!
Traceback (most recent call last):
  [...]
    return 1 / (x - 2)
           ~~^~~~~~~~~
ZeroDivisionError: float division by zero

正如您所看到的,中间计算值2.0被"保存"了,即使下一个函数引发了异常。
值得注意的是,在这个例子中,上下文管理器只在遇到异常时才调用save,而不是在上下文"和平"退出时调用。当然,如果你愿意,你可以将此设置为无条件的。
这可能不像仅仅在函数上添加装饰器那么方便,但是它完成了工作。而且IMO事实上,你仍然必须有意识地在这个上下文中 Package 你的重要行动是一件好事,因为它教会你特别注意这些事情。
顺便说一句,这是在Python中实现数据库事务之类的典型方法(例如,在SQLAlchemy中)。

PS

公平地说,我可能应该稍微限定一下我的初始语句。当然,你 * 可以 * 在你的函数中只使用non-localstate,尽管通常有充分的理由不鼓励这样做。简单地说,如果在你的例子中result是一个全局变量(并且您在函数中声明了global result),事实上,这个问题可以通过一个装饰器来解决,但是我不推荐这种方法,因为全局状态是一个反模式。(而且它仍然要求您记住每次使用为该作业指定的任何全局变量。)

相关问题