是否有类似于 C 宏的功能,它允许您以内联方式重用代码,而无需为该代码段创建单独的范围?
例如:
a=3
def foo():
a=4
foo()
print a
将打印 3,但我希望它打印 4。
我知道涉及类或全局字典等对象的解决方案,但是我正在寻找一种更原始的解决方案(例如函数装饰器),它可以简单地让我在调用者的作用域内进行更改。
谢谢
编辑:任何需要声明我将要使用的变量或事先声明像 muttabale 对象这样的"命名空间"的解决方案都不是我正在寻找的解决方案。
我自己做了一个尝试:
def pgame():
a=3
c=5
print locals()
game(a)
print locals()
class inline_func(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
return self.f(*args, **kwargs)
#to be @inline_func
def game(b, a=4):
exec("inspect.stack()[3][0].f_locals.update(inspect.stack()[1] [0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))ninspect.stack()[1][0].f_locals.update(inspect.stack()[3][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[1][0]),ctypes.c_int(0))")
try:
print "your code here"
finally:
exec("inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))")
@inline_func
def strip_game(b, a=4):
print "your code here"
但是我在如何在不破坏程序可调试性的情况下将代码注入strip_game
遇到了严重的问题,因为我只想到创建一个新的代码对象或使用 exec,两者都遇到了一些严重的问题。
主要编辑:
好的,所以我有一些接近工作的解决方案,但是我遇到了一个非常奇怪的问题:
import inspect
import ctypes
import struct
import dis
import types
def cgame():
a=3
c=5
print locals()
strip_game(a)
print locals()
def pgame():
a=3
c=5
print locals()
game(a)
print locals()
class empty_deco(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
return self.f(*args, **kwargs)
debug_func = None
class inline_func(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
init_exec_string = "inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)n" +
"ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))n" +
"inspect.stack()[1][0].f_locals.update(inspect.stack()[3][0].f_locals)n" +
"ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[1][0]),ctypes.c_int(0))"
fini_exec_string = "inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)n" +
"ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))"
co_stacksize = max(6, self.f.func_code.co_stacksize) # make sure we have enough space on the stack for everything
co_consts = self.f.func_code.co_consts +(init_exec_string, fini_exec_string)
init = "d" + struct.pack("H", len(strip_game.f.func_code.co_consts)) #LOAD_CONST init_exec_string
init += "dx00x00x04U" # LOAD_CONST None, DUP_TOP, EXEC_STMT
init += "z" + struct.pack("H", len(self.f.func_code.co_code) + 4) #SETUP_FINALLY
fini = "Wdx00x00" # POP_BLOCK, LOAD_CONST None
fini += "d" + struct.pack("H", len(strip_game.f.func_code.co_consts) + 1) #LOAD_CONST fini_exec_string
fini += "dx00x00x04UXdx00x00S" # LOAD_CONST None, DUP_TOP, EXEC_STMT, END_FINALLY, LOAD_CONST None, RETURN
co_code = init + self.f.func_code.co_code + fini
co_lnotab = "x00x00x0b" + self.f.func_code.co_lnotab[1:] # every error in init will be attributed to @inline_func, errors in the function will be treated as expected, errors in fini will be attributed to the last line probably.
new_code = types.CodeType(
self.f.func_code.co_argcount,
self.f.func_code.co_nlocals,
co_stacksize,
self.f.func_code.co_flags & ~(1), # optimized functions are problematic for us
co_code,
co_consts,
self.f.func_code.co_names,
self.f.func_code.co_varnames,
self.f.func_code.co_filename,
self.f.func_code.co_name,
self.f.func_code.co_firstlineno,
co_lnotab,
self.f.func_code.co_freevars,
self.f.func_code.co_cellvars,)
self.inline_f = types.FunctionType(new_code, self.f.func_globals, self.f.func_name, self.f.func_defaults, self.f.func_closure)
#dis.dis(self.inline_f)
global debug_func
debug_func = self.inline_f
return self.inline_f(*args, **kwargs)
@empty_deco
def game(b, a=4):
exec("inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))ninspect.stack()[1][0].f_locals.update(inspect.stack()[3][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[1][0]),ctypes.c_int(0))")
try:
print "inner locals:"
print locals()
print c
return None
finally:
exec("inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))")
@inline_func
def strip_game(b, a=4):
print "inner locals:"
print locals()
print c
return None
def stupid():
exec("print 'hello'")
try:
a=1
b=2
c=3
d=4
finally:
exec("print 'goodbye'")
现在这似乎有效,但是,我得到以下内容:
>>>cgame()
{'a': 3, 'c': 5}
{'a': 4, 'c': 5, 'b': 3}
your code here
Traceback (most recent call last):
File "<pyshell#43>", line 1, in <module>
cgame()
File "C:Python27somefile.py", line 14, in cgame
strip_game(a)
File "C:Python27somefile.py", line 78, in __call__
return self.inline_f(*args, **kwargs)
File "C:Python27somefile.py", line 94, in strip_game
z = c
NameError: global name 'c' is not defined
现在当我反汇编函数时,我在game
和strip_game
之间得到了以下非常奇怪的编译差异:
在游戏中:
86 16 LOAD_NAME 0 (locals)
19 CALL_FUNCTION 0
22 PRINT_ITEM
23 PRINT_NEWLINE
87 24 **LOAD_NAME** 1 (c)
27 PRINT_ITEM
28 PRINT_NEWLINE
在脱衣舞游戏中:
95 16 LOAD_GLOBAL 0 (locals)
19 CALL_FUNCTION 0
22 PRINT_ITEM
23 PRINT_NEWLINE
96 24 LOAD_GLOBAL 1 (c)
27 PRINT_ITEM
28 PRINT_NEWLINE
为什么会出现这种差异?
在这种情况下,只需使用global
关键字:
a=3
def foo():
global a
a=4
foo()
print (a)
这会修改外部作用域(如果它是全局的)。
如果外部作用域是一个函数,则使用nonlocal
关键字来完成 - 这是在 Python 3.0 中引入的。
动态范围界定
然而,改变调用方函数的作用域并不是Python的前提,而是一种语言特征。
这是可以做到的。但是仅仅通过调用私有 C api(将"局部变量"值烘焙回快速局部变量)并且绝对不是一个好的做法。
通过魔术装饰器来执行它也是可能的,但是装饰器必须在内部函数中重写字节码 - 通过检索和更新调用器locals
上的值来替换对"非局部"变量的每次访问,并且在函数结束时 - https://programtalk.com/python-examples/ctypes.pythonapi.PyFrame_LocalsToFast/
例
所以,也就是说,这是一个概念证明。当然,它是线程和异步不安全的 - 但如果代理类中的属性 被提升为线程本地或上下文本地(PEP 555),它应该可以工作。 调整它以搜索要在调用堆栈上更改的局部变量应该很容易(因此在子子调用中所做的更改可以更改祖父母本地变量,就像在动态作用域语言中一样)
如问题中所述,没有必要将调用者上的变量声明为任何东西 - 它们必须是普通的局部变量。但是,这需要在装饰函数上声明我想在调用方作用域上更改的变量为"全局",以便更改将通过我可以自定义的对象。如果你连这个都做不到,你确实将不得不在装饰的函数上重写字节码,或者使用用于编写调试器的钩子(在代码上设置"跟踪")。
注意更改的确切行为 locals() 最近被指定到语言中 - 在 3.8, IIRC 之前, - "locals_to_fast"似乎是 一个足够稳定的 API - 但它将来可能会改变。
# Tested in Python 3.8.0
import ctypes
from functools import wraps
from sys import _getframe as getframe
from types import FunctionType
class GlobalProxy(dict):
__slots__ = ("parent", "frame", "mode")
def __init__(self, parent):
self.parent = parent
self.frame = None
self.mode = None
def __getitem__(self, name):
if self.mode == "target":
if name in self.frame.f_locals:
return self.frame.f_locals[name]
if name in self.parent:
return self.parent[name]
return getattr(self.parent["__builtins__"], name)
return super().__getitem__(name)
"""
# This is not run - Python's VM STORE_GLOBAL bypasses the custom __setitem__ (although __getitem__ above runs)
def __setitem__(self, name, value):
if name in self.frame.f_locals:
self.frame.f_locals[name] = value
bake_locals(self.frame)
self.parent[name] = value
"""
def bake_locals(self):
ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(self.frame), ctypes.c_int(1))
def save_changes(self):
self.mode = "inner"
target = self.frame.f_locals
target_names = set(target.keys())
for key in self:
if key in target_names:
target[key] = self[key]
else:
self.parent[key] = self[key]
self.bake_locals()
def caller_changer(func):
"""Makes all global variable changes on the decorated function affect _local_ variables on the callee function instead.
"""
code = func.__code__
# NB: for Python 2, these dunder-attributes for functions have other names.
# this is for Python 3
proxy = GlobalProxy(func.__globals__)
new_function = FunctionType(code, proxy, func.__name__, func.__defaults__, func.__closure__)
@wraps(func)
def wrapper(*args, **kw):
proxy.frame = getframe().f_back
proxy.mode = "target"
result = new_function(*args, **kw)
proxy.save_changes()
return result
wrapper.proxy = proxy
return wrapper
### Example and testing code:
@caller_changer
def blah():
global iwillchange
iwillchange = "new value"
def bleh():
iwillchange = "original value"
print(iwillchange)
blah()
print(iwillchange)
并且,将所有这些粘贴到IPython外壳上:
In [121]: bleh()
original value
new value
(我可以补充一点,测试感觉很奇怪,因为功能是 更改局部变量不需要任何装饰器, 或对变量的任何特殊声明)
好的,所以在坐了几个小时之后,我设法写了一个解决方案,在接近这个时有一些主要的陷阱,我会在下面注明
import inspect
import ctypes
import struct
import dis
import types
def dump(obj):
for attr in dir(obj):
print("obj.%s = %r" % (attr, getattr(obj, attr)))
def cgame():
a=3
c=5
print locals()
strip_game(a)
print locals()
def pgame():
a=3
c=5
print locals()
game(a)
print locals()
class empty_deco(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
return self.f(*args, **kwargs)
debug_func = None
class inline_func(object):
def __init__(self, f):
self.f = f
# this is the price we pay for using 2.7
# also, there is a huge glraing issue here, which is what happens if the user TRIES to access a global variable?
@staticmethod
def replace_globals_with_name_lookups(co):
res = ""
code = list(co)
n = len(code)
i = 0
while i < n:
c = code[i]
op = ord(c)
if dis.opname[op] == "STORE_GLOBAL":
code[i] = chr(dis.opmap['STORE_NAME'])
elif dis.opname[op] == "DELETE_GLOBAL":
code[i] = chr(dis.opmap['DELETE_NAME'])
elif dis.opname[op] == "LOAD_GLOBAL":
code[i] = chr(dis.opmap['LOAD_NAME'])
i = i+1
if op >= dis.HAVE_ARGUMENT:
i = i+2
return "".join(code)
def __call__(self, *args, **kwargs):
init_exec_string = "inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)n" +
"ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))n" +
"inspect.stack()[1][0].f_locals.update(inspect.stack()[3][0].f_locals)n" +
"ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[1][0]),ctypes.c_int(0))"
fini_exec_string = "inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)n" +
"ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))"
co_stacksize = max(6, self.f.func_code.co_stacksize) # make sure we have enough space on the stack for everything
co_consts = self.f.func_code.co_consts +(init_exec_string, fini_exec_string)
init = "d" + struct.pack("H", len(strip_game.f.func_code.co_consts)) #LOAD_CONST init_exec_string
init += "dx00x00x04U" # LOAD_CONST None, DUP_TOP, EXEC_STMT
init += "z" + struct.pack("H", len(self.f.func_code.co_code) + 4) #SETUP_FINALLY
fini = "Wdx00x00" # POP_BLOCK, LOAD_CONST None
fini += "d" + struct.pack("H", len(strip_game.f.func_code.co_consts) + 1) #LOAD_CONST fini_exec_string
fini += "dx00x00x04UXdx00x00S" # LOAD_CONST None, DUP_TOP, EXEC_STMT, END_FINALLY, LOAD_CONST None, RETURN
co_code = init + self.replace_globals_with_name_lookups(self.f.func_code.co_code) + fini
co_lnotab = "x00x00x0b" + self.f.func_code.co_lnotab[1:] # every error in init will be attributed to @inline_func, errors in the function will be treated as expected, errors in fini will be attributed to the last line probably.
new_code = types.CodeType(
self.f.func_code.co_argcount,
self.f.func_code.co_nlocals,
co_stacksize,
self.f.func_code.co_flags & ~(1), # optimized functions are problematic for us
co_code,
co_consts,
self.f.func_code.co_names,
self.f.func_code.co_varnames,
self.f.func_code.co_filename,
self.f.func_code.co_name,
self.f.func_code.co_firstlineno,
co_lnotab,
self.f.func_code.co_freevars,
self.f.func_code.co_cellvars,)
self.inline_f = types.FunctionType(new_code, self.f.func_globals, self.f.func_name, self.f.func_defaults, self.f.func_closure)
#dis.dis(self.inline_f)
global debug_func
debug_func = self.inline_f
return self.inline_f(*args, **kwargs)
@empty_deco
def game(b, a=4):
exec("inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))ninspect.stack()[1][0].f_locals.update(inspect.stack()[3][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[1][0]),ctypes.c_int(0))")
try:
print "inner locals:"
print locals()
print c
return None
finally:
exec("inspect.stack()[3][0].f_locals.update(inspect.stack()[1][0].f_locals)nctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(inspect.stack()[3][0]),ctypes.c_int(0))")
@inline_func
def strip_game(b, a=4):
print "inner locals:"
print locals()
print c
return None
所需的 acutal 代码在于class inline_func
和一些导入(也许你可以将它们放在类内部?我真的不确定)
那么这整个事情是做什么的呢?好吧,它使strip_game
和game
的代码(几乎)相同,即:
- 它插入一个函数序幕,该序幕更新调用方的局部变量,然后将调用方的局部变量添加到被调用方。 在
- 函数周围插入 Try Final 块
- 将每个符号查找从全局查找更改为普通(名称)查找,经过一番思考,我意识到这实际上没有任何影响
- 进入 finally 块后,更新调用方本地变量。
做这样的事情有一些主要的陷阱,我将列出我遇到的几个问题:
- cpython
compiler_nameop
函数根据给定函数的简单性优化命名空间查找,这意味着如果可以,它将名称查找优化为全局查找 - 更改字节码意味着影响程序的可调试性,我已经在
co_lnotab
变量中解决了这个问题 - 对于大型函数,此解决方案不起作用,因为某些操作码必须使用extended_args:即变量的负载和 try-finally 块(无论如何,这一点都可以通过使用extended_args来解决......
感谢@jsbueno花时间并指出我PyFrame_LocalsToFast。
附言这个解决方案适用于python 2.7.6,python在API的稳定性方面存在一些问题,因此对于较新的版本,这可能需要修复。