打印函数的局部变量名称和值

Posted

技术标签:

【中文标题】打印函数的局部变量名称和值【英文标题】:Printing a function's local variable names and values 【发布时间】:2014-08-01 15:43:35 【问题描述】:

为了帮助我调试我编写的一些代码,我想制作一个函数装饰器,在创建或修改每个变量时打印出一个变量的名称及其值,本质上给我一个“逐个播放” " 查看我调用函数时发生的情况。

到目前为止,我一直在使用的方法只是在我想查看发生了什么的任何地方添加类似 print(foo) 的行,但这非常耗时并且使我的代码看起来很乱(可能是缩影非 Python 性)。

实际上我想要做的是:

@show_guts
def foo(a,b):
    biz = str(a)
    baz = str(b)
    return biz + baz

foo("banana","phone")

在 IDE 中打印类似的内容:

biz = "banana"
baz = "phone"
bananaphone

我的问题是@show_guts 会是什么样子。我知道可以使用类似的装饰器仅打印 ab 的值

def print_args(function):
    def wrapper(*args,**kwargs):
        print("Arguments:",args)
        print("Keyword Arguments:",kwargs)
        return function(*args,**kwargs)
    return wrapper

这给了我

Arguments: ('banana', 'phone')
Keyword Arguments: 
'bananaphone'

但我完全不知道如何打印局部变量名称及其值。更不用说以“整洁”的方式进行操作了。

【问题讨论】:

与其编写复杂的装饰器,不如考虑使用pdb 模块。您可以使用 pdb.set_trace 暂停程序的执行,查看变量的内容,并一次执行一行代码,为您提供全面的“逐个播放”视图。 【参考方案1】:

如果不启用跟踪,您将无法执行此操作;这会损害性能。函数局部变量在函数被调用时构造,并在函数返回时被清除,因此没有其他方法可以从装饰器访问这些局部变量。

您可以使用sys.settrace() 插入跟踪函数,然后响应 Python 解释器发送该函数的事件。我们要做的是跟踪 just 修饰函数,并在函数返回时记录局部变量:

import sys
import threading

def show_guts(f):
    sentinel = object()
    gutsdata = threading.local()
    gutsdata.captured_locals = None
    gutsdata.tracing = False

    def trace_locals(frame, event, arg):
        if event.startswith('c_'):  # C code traces, no new hook
            return 
        if event == 'call':  # start tracing only the first call
            if gutsdata.tracing:
                return None
            gutsdata.tracing = True
            return trace_locals
        if event == 'line':  # continue tracing
            return trace_locals

        # event is either exception or return, capture locals, end tracing
        gutsdata.captured_locals = frame.f_locals.copy()
        return None

    def wrapper(*args, **kw):
        # preserve existing tracer, start our trace
        old_trace = sys.gettrace()
        sys.settrace(trace_locals)

        retval = sentinel
        try:
            retval = f(*args, **kw)
        finally:
            # reinstate existing tracer, report, clean up
            sys.settrace(old_trace)
            for key, val in gutsdata.captured_locals.items():
                print ': !r'.format(key, val)
            if retval is not sentinel:
                print 'Returned: !r'.format(retval)
            gutsdata.captured_locals = None
            gutsdata.tracing = False

        return retval

    return wrapper

演示:

>>> @show_guts
... def foo(a,b):
...     biz = str(a)
...     baz = str(b)
...     return biz + baz
... 
>>> result = foo("banana","phone")
a: 'banana'
biz: 'banana'
b: 'phone'
baz: 'phone'
Returned: 'bananaphone'

【讨论】:

哇,这对我来说太先进了!假设我不担心性能,这种方法是否可以在构造每个局部变量时对其进行跟踪?还是我必须采取不同的方式? @PolskiPhysics:你当然可以追踪当地人的变化,是的。你必须看看frame.f_locals 是如何随着每个line 事件调用而改变的;查看添加和删除了哪些键,以及更改了哪些值。小心,检测哪些值发生了变化会很快变得棘手。【参考方案2】:

本着调试的精神,我提出我的hax:

import re
import inspect

assignment_regex = re.compile(r'(\s*)([\w\d_]+)\s*=\s*[\w\d+]')

def show_guts(fn):
    source = inspect.getsource(fn)
    lines = []
    for line in source.split('\n'):
        if 'show_guts' in line:
            continue
        lines.append(line)
        if 'def' in line:
            # kwargs will match the regex
            continue
        search = assignment_regex.search(line)
        try:
            groups = search.groups()
            leading_whitespace = groups[0]
            variable_name = groups[1]
            lines.append(leading_whitespace + 'print "Assigning 0 =", 0'.format(variable_name))
        except AttributeError:  # no match
            pass
    new_source = '\n'.join(lines)
    namespace = 
    exec new_source in namespace
    fn = namespace[fn.__name__]

    def wrapped(*args, **kwargs):
        arg_string = ', '.join(map(str, args))
        kwarg_string = ', '.join(key + '=' + str(value) for key, value in kwargs.iteritems())
        print "Calling", fn.__name__ + '(' + ', '.join((arg_string, kwarg_string)) + ')'
        return fn(*args, **kwargs)
    return wrapped

基本上,这会自动执行您正在执行的操作。它获取传入函数的源代码,遍历源代码中的每一行,并为每个赋值语句创建一个新的打印语句并将其附加到源代码主体。编译新的源代码并将函数替换为新编译的函数。然后为了获得*args**kwargs,我创建了普通的装饰器包装函数并输入了一些不错的打印语句。在inspect 模块的帮助下,这部分可能会好一些,但是whatevs。

foo() -> 带有打印语句的 foo()

# This...
@show_guts
def complicated(a, b, keyword=6):
    bar = str(a)
    baz = str(b)
    if a == b:
        if keyword != 6:
            keyword = a
    else:
        keyword = b
    return bar + baz

# becomes this
def complicated(a, b, keyword=6):
    bar = str(a)
    print "Assigning bar =", bar
    baz = str(b)
    print "Assigning baz =", baz
    if a == b:
        if keyword != 6:
            keyword = a
            print "Assigning keyword =", keyword
    else:
        keyword = b
        print "Assigning keyword =", keyword
    return bar + baz

用法

@show_guts
def foo(a, b):
    bar = str(a)
    baz = str(b)
    return bar + baz


@show_guts
def complicated(a, b, keyword=6):
    bar = str(a)
    baz = str(b)
    if a == b:
        if keyword != 6:
            keyword = a
    else:
        keyword = b
    return bar + baz


foo(1, 2)
complicated(3, 4)
complicated(3, 3)
complicated(3, 3, keyword=123)

输出

Calling foo(1, 2, )
Assigning bar = 1
Assigning baz = 2
Calling complicated(3, 4, )
Assigning bar = 3
Assigning baz = 4
Assigning keyword = 4
Calling complicated(3, 3, )
Assigning bar = 3
Assigning baz = 3
Calling complicated(3, 3, keyword=123)
Assigning bar = 3
Assigning baz = 3
Assigning keyword = 3

我可能在正则表达式中遗漏了一些极端情况,但这会让你接近。

【讨论】:

以上是关于打印函数的局部变量名称和值的主要内容,如果未能解决你的问题,请参考以下文章

Go语言之全局变量定义函数内部局部变量定义和defer关键字和值类型与引用类型

python 函数进阶

名称空间

19.局部变量和全局变量

函数之局部变量和使用global语句

python 局部变量和全局变量 global