简体   繁体   English

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

[英]How can I get the values of the locals of a function after it has been executed?

Suppose I have a function like f(a, b, c=None) . 假设我有一个像f(a, b, c=None)这样的函数。 The aim is to call the function like f(*args, **kwargs) , and then construct a new set of args and kwargs such that: 目的是将函数调用为f(*args, **kwargs) ,然后构造一组新的args和kwargs,以便:

  1. If the function had default values, I should be able to acquire their values. 如果函数有默认值,我应该能够获取它们的值。 For example, if I call it like f(1, 2) , I should be able to get the tuple (1, 2, None) and/or the dictionary {'c': None} . 例如,如果我将其称为f(1, 2) ,我应该能够获得元组(1, 2, None)和/或字典{'c': None}
  2. If the value of any of the arguments was modified inside the function, get the new value. 如果在函数内部修改了任何参数的值,则获取新值。 For example, if I call it like f(1, 100000, 3) and the function does if b > 500: b = 5 modifying the local variable, I should be able to get the the tuple (1, 5, 3) . 例如,如果我将其称为f(1, 100000, 3)if b > 500: b = 5修改局部变量,则函数执行,我应该能够得到元组(1, 5, 3) 1,5,3 (1, 5, 3)

The aim here is to create aa decorator that finishes the job of a function. 这里的目的是创建一个完成函数工作的装饰器。 The original function acts as a preamble setting up the data for the actual execution, and the decorator finishes the job. 原始函数充当为实际执行设置数据的前导码,装饰器完成作业。

Edit: I'm adding an example of what I'm trying to do. 编辑:我正在添加一个我正在尝试做的例子。 It's a module for making proxies for other classes. 它是一个为其他类创建代理的模块。


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)

And then, we'll have that: 然后,我们将拥有:

ProxyForSpam().eggs(-10, 200) -> Spam().eggs(0, 100, 1)

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

There are two recipes available here , one which requires an external library and another that uses only the standard library. 有两种配方可在这里 ,一个需要外部库,另一个只使用标准库。 They don't quite do what you want, in that they actually modify the function being executed to obtain its locals() rather than obtain the locals() after function execution, which is impossible, since the local stack no longer exists after the function finishes execution. 它们并不能完全按照你想要的方式执行,因为它们实际上修改了正在执行的函数以获取其locals()而不是在函数执行后获取locals() ,这是不可能的,因为函数之后不再存在本地堆栈完成执行。

Another option is to see what debuggers, such as WinPDB or even the pdb module do. 另一种选择是查看调试器,例如WinPDB甚至pdb模块。 I suspect they use the inspect module (possibly along with others), to get the frame inside which a function is executing and retrieve locals() that way. 我怀疑他们使用inspect模块(可能还有其他模块)来获取正在执行函数的框架并以这种方式检索locals()

EDIT: After reading some code in the standard library, the file you want to look at is probably bdb.py , which should be wherever the rest of your Python standard library is. 编辑:在阅读标准库中的一些代码之后,您要查看的文件可能是bdb.py ,它应该是Python标准库的其余部分。 Specifically, look at set_trace() and related functions. 具体来说,请查看set_trace()和相关函数。 This will give you an idea of how the Python debugger breaks into the class. 这将让您了解Python调试器如何进入类中。 You might even be able to use it directly. 您甚至可以直接使用它。 To get the frame to pass to set_trace() look at the inspect module. 要将帧传递给set_trace() ,请inspect模块。

I don't see how you could do this non-intrusively -- after the function is done executing, it doesn't exist any more -- there's no way you can reach inside something that doesn't exist. 我不知道你怎么能非侵入地做这个 - 在函数执行完之后,它不再存在 - 你无法进入内部不存在的东西。

If you can control the functions that are being used, you can do an intrusive approach like 如果您可以控制正在使用的功能,您可以执行类似的侵入式方法

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': {...}}
>>> 

...or you can just require that those functions return locals() -- as @Thomas K asks above, what are you really trying to do here? ...或者你可以要求那些函数返回locals() - 正如@Thomas K上面提到的那样,你在这里真正想做什么?

I've stumbled upon this very need today and wanted to share my solution. 我今天偶然发现了这个需求,并想分享我的解决方案。

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

The idea is to use sys.trace() to catch the frame of the next 'call' . 我们的想法是使用sys.trace()来捕获下一个'call'的帧。 Tested on CPython 3.6. 在CPython 3.6上测试过。

Example usage 用法示例

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

Witchcraft below read on your OWN danger(!) 下面的巫术读你的OWN危险(!)

I have no clue what you want to do with this, it's possible but it's an awful hack... 我不知道你想用它做什么,这是可能的,但它是一个可怕的黑客......

Anyways, I HAVE WARNED YOU(!) , be lucky if such things don't work in your favorite language... 无论如何, 我已经警告过你(!) ,如果这些东西不适合你喜欢的语言,那就很幸运了...

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

Output: 输出:

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

There you go... I'm not responsible for any small children that get eaten by demons or something the like (or if it breaks on complicated functions). 在那里你去...我不负责任何被恶魔或类似东西吃掉的小孩子(或者如果它打破复杂的功能)。

PS: The inspect module is the pure EVIL . PS:检查模块是纯EVIL

Since you are trying to manipulate variables in one function, and do some job based on those variables on another function, the cleanest way to do it is having these variables to be an object's attributes. 由于您试图在一个函数中操作变量,并根据另一个函数上的这些变量执行某些工作,因此最简单的方法是将这些变量作为对象的属性。

It could be a dictionary - that could be defined inside the decorator - therefore access to it inside the decorated function would be as a "nonlocal" variable. 它可以是一个字典 - 可以在装饰器中定义 - 因此在装饰函数内部访问它将是一个“非局部”变量。 That cleans up the default parameter tuple of this dictionary, that @bgporter proposed.: 这清除了这个字典的默认参数元组,@ bgporter提出:

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

To be even more clean, you probably should have all these parameters as attributes of the instance (self) - so that no "magical" variable has to be used inside the decorated function. 为了更加干净,你可能应该将所有这些参数作为实例的属性(self) - 这样就不必在装饰函数中使用“魔法”变量。

As for doing it "magically" without having the parameters set as attributes of certain object explicitly, nor having the decorated function to return the parameters themselves (which is also an option) - that is, to have it to work transparently with any decorated function - I can't think of a way that does not involve manipulating the bytecode of the function itself. 至于在没有明确地将参数设置为某个对象的属性的情况下“神奇地”执行它,也没有使用装饰函数来返回参数本身(这也是一个选项) - 也就是说,让它与任何装饰函数透明地工作 - 我想不出一种不涉及操作函数本身的字节码的方法。 If you can think of a way to make the wrapped function raise an exception at return time, you could trap the exception and check the execution trace. 如果您能想到一种方法来使包装函数在返回时引发异常,则可以捕获异常并检查执行跟踪。

If it is so important to do it automatically that you consider altering the function bytecode an option, feel free to ask me further. 如果自动执行此操作非常重要,您可以考虑将函数字节码更改为选项,请随时向我询问。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 JS执行后如何获取网页的HTML代码? - How can I get the HTML code of a webpage after JS has been executed? 我怎样才能得到一个函数来存储一个新变量,并在它被传递后分配一个值 - how can I get a function to store a new variable, with a value assigned to it, after it has been passed 在Python unittest中,执行完TestCase中的所有测试后如何调用函数? - In Python unittest, how can I call a function after all tests in a TestCase have been executed? PYTHON-函数执行后如何删除? - PYTHON - How do I remove a function after it's been executed? 如何统计这个程序在Python中执行了多少次? - How can I count how many times this program has been executed in Python? 一旦我的代码已经执行,如何让我的代码循环回输入? (角色扮演游戏) - How do I get my code to loop back to an input once it has been executed already? (RPG Game) 在Django Celery中,我如何判断任务是否已异步执行 - In Django Celery how can I tell if a task has been executed asynchronously 返回后如何使用局部变量? - How can I use a local variable after it has been returned? 执行循环后列表未重置 - Lists not resetting after loop has been executed 如何计算函数用python执行所需的时间? - How can I calculate the time taken by a function to get executed with python?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM