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

时间:2010-11-18 12:52:54

标签: python

假设我有一个像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)
  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)

5 个答案:

答案 0 :(得分:4)

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

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

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

答案 1 :(得分: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上面提到的那样,您真的试图在这做什么?

答案 2 :(得分:1)

下面的巫术读取你的OWN危险(!)

我不知道你想用它做什么,这是可能的,但这是一个非常糟糕的黑客......

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

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:检查模块是纯 EVIL

答案 3 :(得分:1)

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

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()

答案 4 :(得分:0)

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

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

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

为了更加干净,你可能应该将所有这些参数作为实例的属性(self) - 这样就不必在装饰函数中使用“魔法”变量。

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

如果自动执行此操作非常重要,您可以考虑将函数字节码更改为选项,请随时向我询问。