如何跟踪 Python 中局部变量的值?

How can I track the values of a local variable in Python?

我的算法循环处理数秒的数据。对于每个第二价值的数据,我 return 一个值,这是涉及多个子模块和子 class 的相当复杂的计算的结果。其中一些 class 每秒都会重新初始化。

出于调试目的,有几次我想绘制其中一个 class 中某个局部变量的值随时间变化的情况。外部的东西必须序列化并记录值,因为 class 只存在一秒钟。每次都是不同的局部变量。

我怎样才能在软件设计方面正确地实现我的目标,而不是每次都花费我几个小时,并且每次我想这样做时都不会编写超过一两行的新代码?

理想情况下,我考虑过的一种解决方案是拥有类似全局 IO 流的东西,或者类似的东西,我会 "just be around" 而不必用它初始化每个 class,所以我可以在代码中的任意位置插入某种 MySerializer << [mylocalvariable, timestamp] 命令,然后当 运行 完成时,我可以检查 MySerializer 是否为空,如果不是,我可以绘制其中的内容。 。 或类似的东西。如果我可以对不同 classes 中的多个局部变量执行此操作,那就更好了。这个解决方案会好吗?我该怎么做?

或者能够以面向方面的方式执行此操作,使用一些外部对象 "looking at the code" 而不更改它,构建该局部变量值的缓冲区,并将它们吐出到一个绘图中结束。我该怎么做?

是否有更好的解决方案?哪种设计模式适合这种情况?

我过去所做的是 return 那个局部变量给拥有函数的人,然后谁又必须 return 它收到的那个值,等等依此类推,一直到顶部。乱七八糟的,每次都要写又要删。

我想到了像这样非常简单的东西:

#the decorator
def debug_function(func):
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print('debug:', res)
        return res

    return wrapper


#proof of concept:
@debug_function
def square(number):
    return number*number

class ClassA:
    def __init__(self):
        self.Number = 42

    @debug_function
    def return_number(self):
        return self.Number


if __name__ == '__main__':
    result = [square(i) for i in range(5)]
    print(result)

    my_obj = ClassA()
    n = my_obj.return_number()
    print(n)

简而言之,编写一个简单的装饰器,在某处记录函数的结果(上面我只将它写到终端,但这可以扩展为使用日志文件或类似文件)。然后你装饰任何你想跟踪的函数,并在函数被调用时得到它的 return 值。在上面的代码中,我展示了它对简单函数和 class 方法的作用。示例代码的结果如下所示:

debug: 0
debug: 1
debug: 4
debug: 9
debug: 16
[0, 1, 4, 9, 16]
debug: 42
42

编辑 2:

我编辑了下面的代码以使用实际函数而不是 __name__ 来存储中间值。这应该会使它更不容易出错。

编辑:

为了在内存中存储值,我会再次尽可能简单,将值存储在列表中。对于上面概述的简单示例,可能一个全局列表对象就足够了。但是,由于您很可能希望一次查看多个函数,因此我宁愿建议将装饰器设计为 class 并将每个函数的一个列表存储在 class 属性中。示例代码中有更多相关信息。

真正的问题是局部变量的存储。为此,您必须更改函数的实际代码。当然,您不想这样做 'by hand',而是希望您的装饰器来处理这个问题。在这里它变得棘手。环顾四周后,我发现了一个名为 bytecode 的包(至少适用于 Python 3.6)。很可能还有其他选择,但我决定选择这个。 bytecode 允许您将 python 字节码转换为人类可读的形式,对其进行修改,然后将其转换回 python 字节码。我不得不承认我在这里有点力不从心,但我所做的是编写一些小函数,查看翻译后的代码并设计一段代码来完成我想要的。

所以,在这个例子中,objective 是装饰要测试的函数,这样装饰器将字符串列表作为参数,其中每个字符串是应该跟踪的变量的名称.然后它将代码添加到函数体中,将所有列出的变量的 final 值打包在一个元组中,并将元组 return 与实际 return 值一起打包。 'wrapper' 函数然后收集跟踪值并将它们附加到特定于函数的值列表中,该列表可以在代码中的任何位置读取。

就这样吧。把实际的装饰器放在自己的文件里,我这里叫它debug_function.py:

from bytecode import Bytecode, Instr

class debug_function(object):
    """
    Decorator that takes a list of variable names as argument. Everytime
    the decorated function is called, the final states of the listed
    variables are logged and can be read any time during code execution.
    """
    _functions = {}
    def __init__(self, varnames):
        self.varnames = varnames


    def __call__(self, func):
        print('logging variables {} of function {}'.format(
            ','.join(self.varnames), func.__name__
        ))
        debug_function._functions[func] = []
        c = Bytecode.from_code(func.__code__)
        extra_code = [
            Instr('STORE_FAST', '_res')
        ]+[
            Instr('LOAD_FAST', name) for name in self.varnames
        ]+[
            Instr('BUILD_TUPLE', len(self.varnames)),
            Instr('STORE_FAST', '_debug_tuple'),
            Instr('LOAD_FAST', '_res'),
            Instr('LOAD_FAST', '_debug_tuple'),
            Instr('BUILD_TUPLE', 2),
            Instr('STORE_FAST', '_result_tuple'),
            Instr('LOAD_FAST', '_result_tuple'),
        ]
        c[-1:-1]= extra_code
        func.__code__=c.to_code()

        def wrapper(*args, **kwargs):
            res, values = func(*args, **kwargs)
            debug_function._functions[func].append(values)
            return res

        return wrapper

    @staticmethod
    def get_values(func):
        return debug_function._functions[func]

然后,让我们再次生成一些要检查的函数,我们用这个装饰器装饰它们。例如,将这些放在 functions.py

from debug_function import debug_function

@debug_function(['c','d'])
def test_func(a,b):
    c = a+b
    d = a-b
    return c+d


class test_class:
    def __init__(self, value):
        self.val = value

    @debug_function(['y'])
    def test_method(self, *args):
        x = sum(args)
        y = 1
        for arg in args:
            y*=arg
        return x+y

最后,调用函数并查看输出。 debug_function 有一个名为 get() 的静态方法,它将您需要的函数作为参数,return 是一个元组列表。这些元组中的每一个都包含您希望在调用该函数后跟踪的所有局部变量的 final 值。这些值的顺序与它们在装饰器语句中列出的顺序相同。使用 'inverse' zip,您可以轻松地将这些元组分开。

from debug_function import debug_function
from functions import test_func, test_class

results = [test_func(i,j) for i in range(5) for j in range(8,12)]
c,d = zip(*debug_function.get_values(test_func))
print('results:', results)
print('intermediate values:')
print('c =', c)
print('d =', d)

my_class = test_class(7)
results2 = [
    my_class.test_method(i,j,4,2) for i in range(5) for j in range(8,12)
]
y, = zip(*debug_function.get_values(test_class.test_method))
print('results:', results2)
print('intermediate values:')
print('y =', y)

调用的输出如下所示:

logging variables c,d of function test_func
logging variables y of function test_method
results: [0, 0, 0, 0, 2, 2, 2, 2, 4, 4, 4, 4, 6, 6, 6, 6, 8, 8, 8, 8]
intermediate values:
c = (8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, 14, 15)
d = (-8, -9, -10, -11, -7, -8, -9, -10, -6, -7, -8, -9, -5, -6, -7, -8, -4, -5, -6, -7)
results: [14, 15, 16, 17, 79, 88, 97, 106, 144, 161, 178, 195, 209, 234, 259, 284, 274, 307, 340, 373]
intermediate values:
y = (0, 0, 0, 0, 64, 72, 80, 88, 128, 144, 160, 176, 192, 216, 240, 264, 256, 288, 320, 352)

我可能应该更好地解释一下这是如何工作的,请询问是否有任何不清楚的地方。如前所述,此装饰器仅存储每个变量的 final 值(即变量在 after 函数代码已执行的值)。如果你有一个更复杂的函数,你可能会对值是什么感兴趣,例如,每个变量赋值——在这种情况下你将不得不做更多的工作,但它应该是可行的。

希望对您有所帮助