[英]Is there a way to use python logging to print from the scope of the caller
[英]Is there a way to declare that a function should use the scope of the caller?
是否有類似於 C 宏的功能,可讓您以內聯方式重用代碼,而無需為那段代碼創建單獨的 scope?
例如:
a=3
def foo():
a=4
foo()
print a
將打印 3,但我希望它打印 4。
我知道涉及類或全局字典等對象的解決方案,但是我正在尋找一個更原始的解決方案(例如 function 裝飾器),它只會讓我在調用者的 scope 內部進行更改。
非常感謝您
編輯:任何需要聲明我將使用哪些變量或事先聲明像 mutabale 對象這樣的“命名空間”的解決方案都不是我正在尋找的解決方案。
我自己嘗試過:
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
,因為我只想創建一個新代碼 object 或使用 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 += "d\x00\x00\x04U" # LOAD_CONST None, DUP_TOP, EXEC_STMT
init += "z" + struct.pack("H", len(self.f.func_code.co_code) + 4) #SETUP_FINALLY
fini = "Wd\x00\x00" # 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 += "d\x00\x00\x04UXd\x00\x00S" # 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 = "\x00\x00\x0b" + 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:\Python27\somefile.py", line 14, in cgame
strip_game(a)
File "C:\Python27\somefile.py", line 78, in __call__
return self.inline_f(*args, **kwargs)
File "C:\Python27\somefile.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)
這會修改外部 scope,如果它是全局的。
如果外部 scope 是nonlocal
,則使用 nonlocal 關鍵字代替 - 這是 Python 3.0 引入的。
然而,改變調用者 function 的 scope 並不是 Python 的前提,而是語言特性。
這是可以做到的。 但僅僅通過調用私有 C api(將“本地”值烘焙回快速局部變量),這絕對不是一個好習慣。
也可以通過魔法裝飾器來實現,但裝飾器必須重寫內部 function 中的字節碼 - 通過檢索和更新校准器locals
上的值來替換對“非本地”變量的每次訪問,並且,在function - https 的結尾://programtalk.com/python-examples/ctypes.pythonapi.PyFrame_LocalsToFast/
所以,也就是說,這是一個概念證明。 當然,它是線程和異步不安全的地獄- 但是如果代理 class 中的屬性被提升為線程本地或上下文本地(pep 555),它應該可以工作。 它應該很容易適應它來搜索局部變量以在調用堆棧上進行更改(以便在子子調用中所做的更改可以更改祖父母的本地人,就像在動態范圍的語言中一樣)
如問題中所述,無需將調用者上的變量聲明為任何內容-它們必須是普通的局部變量。 However, this requires the declaration, on the decorated function, the variables I want to change on the caller scope as 'global', so that changing then will go through an object I can customize. 如果你連這個都做不到,你確實不得不求助於在裝飾的 function 上重寫字節碼,或者使用為編寫調試器而設置的鈎子(在代碼上設置“trace on”)。
注意,最近更改 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 shell 上:
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 += "d\x00\x00\x04U" # LOAD_CONST None, DUP_TOP, EXEC_STMT
init += "z" + struct.pack("H", len(self.f.func_code.co_code) + 4) #SETUP_FINALLY
fini = "Wd\x00\x00" # 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 += "d\x00\x00\x04UXd\x00\x00S" # 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 = "\x00\x00\x0b" + 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
所需的實際代碼在class inline_func
和一些導入中(也許您可以將它們設置為 class 的內部?我真的不確定)
那么這整件事是做什么的呢? 好吧,它使strip_game
和game
的代碼(幾乎)相同,即:
有一些主要的陷阱使這樣的事情發生,我將列出我遇到的一些問題:
compiler_nameop
function 基於給定 function 的簡單性優化命名空間查找,這意味着如果可以的話,它將優化名稱查找到全局查找co_lnotab
變量中解決了這個問題感謝@jsbueno 投入時間並將我指向 PyFrame_LocalsToFast。
PS 此解決方案適用於 python 2.7.6,python 在 API 的穩定性方面存在一些問題,因此對於較新的版本,這可能需要修復。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.