【问题标题】:How can I get the values of the locals of a function after it has been executed?函数执行后如何获取函数的局部值?
【发布时间】:2010-11-18 12:52:53
【问题描述】:

假设我有一个像f(a, b, c=None) 这样的函数。目的是调用f(*args, **kwargs) 之类的函数,然后构造一组新的 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):
    """A fictional class that we'll make a proxy for"""
    def eggs(self, start, stop, step):
        """A fictional method"""
        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)

【问题讨论】:

  • 我不太清楚你想要达到什么目的。您是要在函数内部还是外部执行此操作?你能发布一个模糊完整的例子来说明你想做的事情吗(例如用伪代码)?
  • 您应该更正您的示例,因为它引用了v,而您的f() 将其定义为b。此外,如果参数不是通过引用传递的(strint 等...),您将不会获得更改的值。无论如何,我确实为调试目的创建了这样的装饰器,但现在手头没有。
  • 我正在尝试在函数之外执行此操作。我在我的问题中添加了一个示例。

标签: python


【解决方案1】:

here 有两种可用的配方,一种需要外部库,另一种只使用标准库。他们没有完全做你想做的事,因为他们实际上修改了正在执行的函数以获得它的locals(),而不是在函数执行后获得locals(),这是不可能的,因为本地函数执行完毕后栈不再存在。

另一种选择是查看调试器的功能,例如WinPDB 甚至pdb 模块。我怀疑他们使用inspect 模块(可能与其他模块一起)来获取函数正在执行的框架并以这种方式检索locals()

编辑:在阅读了标准库中的一些代码之后,您要查看的文件可能是bdb.py,它应该是您的 Python 标准库的其余部分所在的位置。具体看set_trace()及相关函数。这将使您了解 Python 调试器如何进入类。您甚至可以直接使用它。要让帧传递给set_trace(),请查看inspect 模块。

【讨论】:

  • 第二个食谱正是我感兴趣的。非常感谢。
【解决方案2】:

我今天偶然发现了这个需求,并想分享我的解决方案。

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 吗?我曾多次想有一种简单的方法在另一个模块中编写一个模块,但不知道如何进入并获取函数局部变量以制作通用装饰器。将是标准库 IMO 的一个很好的补充。
  • @RickTeachey 你能把我链接到线程吗? :-) 快速浏览一下从 8 月开始的 mail.python.org 线程和 Google 搜索并没有向我显示任何关于此的最新线程。
  • 只是想注意您可以在call_function_get_frame 外部定义snatch_locals 以提高效率并使用trace = _frame.f_back.f_locals['trace'] 进行设置和返回,似乎也不需要检查@ 987654329@ 作为第一个 'call' 应该使用原始 trace 函数/无(已测试)覆盖下一次检查。
【解决方案3】:

我不明白你如何能非侵入式地做到这一点——在函数执行完成后,它不再存在——你无法进入不存在的东西。

如果您可以控制正在使用的功能,则可以采用侵入式方法,例如

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 上面所问的,您真的想在这里做什么?

【讨论】:

    【解决方案4】:

    下面的巫术阅读你自己的危险(!)

    我不知道你想用这个做什么,这是可能的,但这是一个可怕的黑客......

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

    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'}
    

    你去吧...我不负责任何被恶魔吃掉的小孩或类似的东西(或者如果它破坏了复杂的功能)。

    PS:inspect 模块是纯粹的EVIL

    【讨论】:

      【解决方案5】:

      由于您试图在一个函数中操作变量,并根据另一个函数上的这些变量做一些工作,最简洁的方法是让这些变量成为对象的属性。

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

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

      为了更简洁,您可能应该将所有这些参数作为实例 (self) 的属性 - 这样就不必在装饰函数内部使用“神奇”变量。

      至于在没有将参数显式设置为某个对象的属性的情况下“神奇地”执行它,也没有装饰函数本身返回参数(这也是一个选项)——也就是说,让它透明地工作任何修饰函数 - 我想不出不涉及操纵函数本身字节码的方法。 如果你能想办法让被包装的函数在返回时引发异常,你可以捕获异常并检查执行跟踪。

      如果自动执行此操作非常重要,以至于您考虑更改函数字节码选项,请随时进一步询问我。

      【讨论】:

      • 无法使用非本地变量,因为不幸的是该函数没有在装饰器内部定义,self 可以工作,但我目前不愿意污染对象的命名空间。我在考虑你说的。是否可以像你的参数一样在函数的本地命名空间中注入一个对象?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多