如何在函数执行后获取局部变量的值?

14
假设我有一个函数 f(a, b, c=None),目标是调用像 f(*args, **kwargs) 这样的函数,然后构造一个新的参数集和关键字参数集,使得:
  1. 如果函数有默认值,我应该能够获取它们的值。例如,如果我像这样调用它 f(1, 2),我应该能够获得元组 (1, 2, None) 或字典 {'c': None}
  2. 如果任何一个参数的值在函数内被修改,获取新的值。例如,如果我像这样调用它 f(1, 100000, 3),并且函数执行了 if b > 500: b = 5 修改本地变量,我应该能够得到元组 (1, 5, 3)
目标是创建一个完成函数任务的装饰器。原始函数作为前奏设置实际执行的数据,而装饰器则完成任务。
编辑:我正在添加一个示例,说明我想做什么。这是一个用于制作其他类代理的模块。

class Spam(object):
    """我们将为其制作代理的虚构类"""
    def eggs(self, start, stop, step):
        """虚构方法"""
        return range(start, stop, step)

class ProxyForSpam(clsproxy.Proxy): proxy_for = Spam @clsproxy.signature_preamble def eggs(self, start, stop, step=1): start = max(0, start) stop = min(100, stop)


然后我们将有: ProxyForSpam().eggs(-10, 200) -> Spam().eggs(0, 100, 1)

ProxyForSpam().eggs(3, 4) -> Spam().eggs(3, 4, 1)

将 ProxyForSpam 类的 eggs 方法转换为 Spam 类的 eggs 方法,并在参数列表末尾添加值1。

我不是很清楚你想要实现什么。你是想在函数内部还是外部做这个?你能否发布一个大致完整的示例来说明你想要做什么(例如伪代码)? - Thomas K
你应该更正你的例子,因为它引用了 v,而你的 f() 将其定义为 b。此外,如果参数没有通过引用传递(如 strint 等),你将无法获得更改后的值。不过,我确实为调试目的创建了这样的装饰器,但现在手头上没有它。 - Danosaure
我正在尝试在函数外部完成这个操作。我在我的问题中添加了一个示例。 - Rosh Oxymoron
参见:https://dev59.com/oGQo5IYBdhLWcg3wdPEa。没有好的理由设计成这样。相反,只需让代理的方法“返回”一个字典,其中包含将用于调用基础方法的值。 “简单比复杂更好”。“显式比隐式更好”。 - Karl Knechtel
我有一个指向函数的引用/指针,想要查看变量/本地变量,而不修改函数本身。例如,理想的解决方案是locals(f),但不起作用。有什么想法如何做到这一点? - Charlie Parker
如何在Python中找到一个lambda函数作为局部变量的名字? - Charlie Parker
5个回答

11

这里有两种可用的方法,其中一种需要使用外部库,另一种只使用标准库。它们并不能完全满足您的需求,因为它们实际上修改了执行函数以获得其 locals() 而不是在函数执行后获取 locals(),这是不可能的,因为当函数执行完成后,本地堆栈将不再存在。

另一个选择是查看调试器,例如 WinPDB 或者 pdb 模块。我猜它们使用 inspect 模块(可能还使用其他模块)来获取函数执行所在的帧并以此方式检索 locals()

编辑: 经过阅读标准库中的一些代码,您要查看的文件可能是 bdb.py,它应该与您的 Python 标准库位于同一处。具体来说,请查看 set_trace() 和相关函数。这将使您了解 Python 调试器如何进入该类。您甚至可以直接使用它。要获取传递给 set_trace() 的帧,请查看 inspect 模块。


第二个配方正好做了我感兴趣的事情。非常感谢。 - Rosh Oxymoron
对于我来说,我有一个函数的引用/指针,并且想查看变量/本地变量而不修改函数本身。例如,理想的解决方案是locals(f),但无法使用。有什么想法如何做到这一点吗? - Charlie Parker

6
我今天偶然遇到了这个需求,并想分享我的解决方案。
import sys

def call_function_get_frame(func, *args, **kwargs):
  """
  Calls the function *func* with the specified arguments and keyword
  arguments and snatches its local frame before it actually executes.
  """

  frame = None
  trace = sys.gettrace()
  def snatch_locals(_frame, name, arg):
    nonlocal frame
    if frame is None and name == 'call':
      frame = _frame
      sys.settrace(trace)
    return trace
  sys.settrace(snatch_locals)
  try:
    result = func(*args, **kwargs)
  finally:
    sys.settrace(trace)
  return frame, result

使用 sys.trace() 捕捉下一个 'call' 的帧。在 CPython 3.6 上已测试。 示例用法
import types

def namespace_decorator(func):
  frame, result = call_function_get_frame(func)
  try:
    module = types.ModuleType(func.__name__)
    module.__dict__.update(frame.f_locals)
    return module
  finally:
    del frame

@namespace_decorator
def mynamespace():
  eggs = 'spam'
  class Bar:
    def hello(self):
      print("Hello, World!")

assert mynamespace.eggs == 'spam'
mynamespace.Bar().hello()

最近在Python的邮件列表中讨论了创建命名空间装饰器的问题。您是否愿意贡献一个工作示例?您认为它是否足够强大,可以成为PEP吗?我想要有一种简单的方法来编写另一个模块内的模块,但不知道如何获取函数本地变量以制作通用的装饰器。在我看来,这将是标准库的一个很好的补充。 - Rick
@RickTeachey,你能给我提供一下那个帖子的链接吗?:-) 我快速浏览了一下8月份的mail.python.org帖子和进行了谷歌搜索,但是没有发现最近有关此事的任何帖子。 - Niklas R
1
https://mail.python.org/archives/list/python-ideas@python.org/thread/TAVHEKDZVYKJUGZKWSVZVAOGBPLZVKQG/ - Rick
只是想提醒您,可以将 snatch_locals 定义为 call_function_get_frame 外部的变量,以获得更高的效率,并使用 trace = _frame.f_back.f_locals['trace'] 进行设置和返回。似乎也没有必要检查 if frame is None,因为第一个 'call' 应该会用原始的 trace 函数/None 覆盖下一次检查(已测试)。 - Tcll
我有一个指向函数的引用/指针,想要查看变量/本地变量,而不修改函数本身。例如,理想的解决方案是locals(f),但不起作用。有什么想法如何做到这一点? - Charlie Parker

2

以下内容涉及巫术,请自行承担风险(!)

我不知道你想用这个做什么,这可能是可行的,但是这是一个糟糕的hack...

无论如何,我已经警告过你了(!),如果这些东西在你喜欢的语言中不起作用,那就算你走运了...

from inspect import getargspec, ismethod
import inspect


def main():

    @get_modified_values
    def foo(a, f, b):
        print a, f, b

        a = 10
        if a == 2:
            return a

        f = 'Hello World'
        b = 1223

    e = 1
    c = 2
    foo(e, 1000, b = c)


# intercept a function and retrieve the modifed values
def get_modified_values(target):
    def wrapper(*args, **kwargs):

        # get the applied args
        kargs = getcallargs(target, *args, **kwargs)

        # get the source code
        src = inspect.getsource(target)
        lines = src.split('\n')


        # oh noes string patching of the function
        unindent = len(lines[0]) - len(lines[0].lstrip())
        indent = lines[0][:len(lines[0]) - len(lines[0].lstrip())]

        lines[0] = ''
        lines[1] = indent + 'def _temp(_args, ' + lines[1].split('(')[1]
        setter = []
        for k in kargs.keys():
            setter.append('_args["%s"] = %s' % (k, k))

        i = 0
        while i < len(lines):
            indent = lines[i][:len(lines[i]) - len(lines[i].lstrip())]
            if lines[i].find('return ') != -1 or lines[i].find('return\n') != -1:
                for e in setter:
                    lines.insert(i, indent + e)

                i += len(setter)

            elif i == len(lines) - 2:
                for e in setter:
                    lines.insert(i + 1, indent + e)

                break

            i += 1

        for i in range(0, len(lines)):
            lines[i] = lines[i][unindent:]

        data = '\n'.join(lines) + "\n"

        # setup variables
        frame = inspect.currentframe()
        loc = inspect.getouterframes(frame)[1][0].f_locals
        glob = inspect.getouterframes(frame)[1][0].f_globals
        loc['_temp'] = None


        # compile patched function and call it
        func = compile(data, '<witchstuff>', 'exec')
        eval(func, glob, loc)
        loc['_temp'](kargs, *args, **kwargs)

        # there you go....
        print kargs
        # >> {'a': 10, 'b': 1223, 'f': 'Hello World'}

    return wrapper



# from python 2.7 inspect module
def getcallargs(func, *positional, **named):
    """Get the mapping of arguments to values.

    A dict is returned, with keys the function argument names (including the
    names of the * and ** arguments, if any), and values the respective bound
    values from 'positional' and 'named'."""
    args, varargs, varkw, defaults = getargspec(func)
    f_name = func.__name__
    arg2value = {}

    # The following closures are basically because of tuple parameter unpacking.
    assigned_tuple_params = []
    def assign(arg, value):
        if isinstance(arg, str):
            arg2value[arg] = value
        else:
            assigned_tuple_params.append(arg)
            value = iter(value)
            for i, subarg in enumerate(arg):
                try:
                    subvalue = next(value)
                except StopIteration:
                    raise ValueError('need more than %d %s to unpack' %
                                     (i, 'values' if i > 1 else 'value'))
                assign(subarg,subvalue)
            try:
                next(value)
            except StopIteration:
                pass
            else:
                raise ValueError('too many values to unpack')
    def is_assigned(arg):
        if isinstance(arg,str):
            return arg in arg2value
        return arg in assigned_tuple_params
    if ismethod(func) and func.im_self is not None:
        # implicit 'self' (or 'cls' for classmethods) argument
        positional = (func.im_self,) + positional
    num_pos = len(positional)
    num_total = num_pos + len(named)
    num_args = len(args)
    num_defaults = len(defaults) if defaults else 0
    for arg, value in zip(args, positional):
        assign(arg, value)
    if varargs:
        if num_pos > num_args:
            assign(varargs, positional[-(num_pos-num_args):])
        else:
            assign(varargs, ())
    elif 0 < num_args < num_pos:
        raise TypeError('%s() takes %s %d %s (%d given)' % (
            f_name, 'at most' if defaults else 'exactly', num_args,
            'arguments' if num_args > 1 else 'argument', num_total))
    elif num_args == 0 and num_total:
        raise TypeError('%s() takes no arguments (%d given)' %
                        (f_name, num_total))
    for arg in args:
        if isinstance(arg, str) and arg in named:
            if is_assigned(arg):
                raise TypeError("%s() got multiple values for keyword "
                                "argument '%s'" % (f_name, arg))
            else:
                assign(arg, named.pop(arg))
    if defaults:    # fill in any missing values with the defaults
        for arg, value in zip(args[-num_defaults:], defaults):
            if not is_assigned(arg):
                assign(arg, value)
    if varkw:
        assign(varkw, named)
    elif named:
        unexpected = next(iter(named))
        if isinstance(unexpected, unicode):
            unexpected = unexpected.encode(sys.getdefaultencoding(), 'replace')
        raise TypeError("%s() got an unexpected keyword argument '%s'" %
                        (f_name, unexpected))
    unassigned = num_args - len([arg for arg in args if is_assigned(arg)])
    if unassigned:
        num_required = num_args - num_defaults
        raise TypeError('%s() takes %s %d %s (%d given)' % (
            f_name, 'at least' if defaults else 'exactly', num_required,
            'arguments' if num_required > 1 else 'argument', num_total))
    return arg2value

main()

输出:

1 1000 2
{'a': 10, 'b': 1223, 'f': 'Hello World'}

给你,我不对任何被恶魔吃掉的小孩或者复杂功能出现问题负责。

顺便说一下,inspect模块真是纯粹的恶魔


2
我不认为你能够以非侵入式的方式完成这个操作——函数执行完成后,它就不存在了——你无法接触到不存在的东西。
如果你可以控制正在使用的函数,你可以采用侵入式方法,例如:
def fn(x, y, z, vars):
   ''' 
      vars is an empty dict that we use to pass things back to the caller
   '''
   x += 1
   y -= 1
   z *= 2
   vars.update(locals())

>>> updated = {}
>>> fn(1, 2, 3, updated)
>>> print updated
{'y': 1, 'x': 2, 'z': 6, 'vars': {...}}
>>> 

你也可以要求这些函数返回locals() -- 正如@Thomas K在上面提问的那样,你到底想做什么?


0

由于您正在尝试在一个函数中操作变量,并根据这些变量执行一些任务在另一个函数中,最干净的方法是将这些变量作为对象属性。

它可以是一个字典 - 可以在装饰器内定义 - 因此在装饰的函数内访问它将作为“非本地”变量。这清理了@bgporter提出的默认参数元组的字典:

def eggs(self, a, b, c=None):
   # nonlocal parms ## uncomment in Python 3
   parms["a"] = a
   ...

为了更加规范,你可能应该将所有这些参数作为实例(self)的属性 - 以便装饰函数内部不必使用任何“神奇”的变量。
至于如何“神奇地”完成操作而又不必将参数显式设置为某个对象的属性,也不必让装饰函数返回参数本身(这也是一种选择) - 即使透明地与任何装饰函数一起工作 - 我无法想到一种不涉及操作函数本身字节码的方法。如果您能想到一种方法让包装函数在返回时引发异常,那么您可以捕获异常并检查执行跟踪。
如果您认为修改函数字节码是一个选项,因为自动执行非常重要,请随时向我提问。

使用non-locals是不可能的,因为该函数不幸地没有在装饰器内定义,self将会起作用,但我目前不想污染对象的命名空间。我正在考虑你所说的。是否可能像你的params一样在函数的本地命名空间中注入一个对象? - Rosh Oxymoron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接