假设我有一个像f(a, b, c=None)
这样的函数。目的是调用函数f(*args, **kwargs)
,然后构造一组新的args和kwargs:
f(1, 2)
,我应该能够获取元组(1, 2, None)
和/或字典{'c': None}
。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)
答案 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)
我不知道你想用它做什么,这是可能的,但这是一个非常糟糕的黑客......
无论如何,我已经警告过你(!),如果这些东西不能用你最喜欢的语言工作,那就太幸运了......
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) - 这样就不必在装饰函数中使用“魔法”变量。
至于“神奇地”做它而没有明确地将参数设置为某个对象的属性,也没有使用装饰函数来返回参数本身(这也是一个选项) - 也就是说,让它透明地工作任何修饰的函数 - 我想不出一种不涉及操作函数本身的字节码的方法。 如果你能想到一种方法来使包装函数在返回时引发异常,你可以捕获异常并检查执行跟踪。
如果自动执行此操作非常重要,您可以考虑将函数字节码更改为选项,请随时向我询问。