简体   繁体   English

有没有办法声明 function 应该使用调用者的 scope ?

[英]Is there a way to declare that a function should use the scope of the caller?

is there a feautre similar to C macros which lets you reuse code in an inline manner, without creating a seperate scope for that piece of code?是否有类似于 C 宏的功能,可让您以内联方式重用代码,而无需为那段代码创建单独的 scope?

for example:例如:

a=3
def foo():
    a=4
foo()
print a

will print 3, however i want it to print 4.将打印 3,但我希望它打印 4。

i am aware of solutions involving objects like classes or a global dict, however i'm looking for a more primitive solution (like a function decorator for example) that would simply let me make changes inside the scope of the caller instead.我知道涉及类或全局字典等对象的解决方案,但是我正在寻找一个更原始的解决方案(例如 function 装饰器),它只会让我在调用者的 scope 内部进行更改。

thank you very much非常感谢您

edit:any solution that requires declaring which variables i'm going to use OR declaring a "namespace" like mutabale objects beforehand is not a solution i'm looking for.编辑:任何需要声明我将使用哪些变量或事先声明像 mutabale 对象这样的“命名空间”的解决方案都不是我正在寻找的解决方案。

i had made an attempt on my own:我自己尝试过:

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"

but i have ran into a serious problem with how to inject code into strip_game without ruining the debugability of the program, because i had only thought of creating a new code object or using exec, both suffering from some severe problems.但是我遇到了一个严重的问题,即如何在不破坏程序可调试性的情况下将代码注入strip_game ,因为我只想创建一个新代码 object 或使用 exec,两者都遇到了一些严重的问题。

MAJOR EDIT:主要编辑:

ok, so i have something close to a working solution, however i encounter a very wierd problem:好的,所以我有一些接近工作解决方案的东西,但是我遇到了一个非常奇怪的问题:

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'")

now this seems to work however, i get the following:现在这似乎可行,但是,我得到以下信息:

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

now when i disassemble the functions, i get the following very wierd compilation difference between game and strip_game :现在,当我反汇编这些函数时,我得到以下gamestrip_game之间非常奇怪的编译差异:

in 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       

in strip game:在脱衣舞游戏中:

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       

why is does this difference occur?为什么会出现这种差异?

In this case, just use the global keyword:在这种情况下,只需使用global关键字:

a=3
def foo():
    global a
    a=4
foo()
print (a)

That modifies the outer scope, if it is global.这会修改外部 scope,如果它是全局的。

If the outer scope is a function, that is done with the nonlocal keyword instead - which was introduced with Python 3.0.如果外部 scope 是nonlocal ,则使用 nonlocal 关键字代替 - 这是 Python 3.0 引入的。

dynamic scoping动态范围

Changing the scope of the caller function however, is not a premise of Python, and is a language characteristic.然而,改变调用者 function 的 scope 并不是 Python 的前提,而是语言特性。

It can be done.这是可以做到的。 But just by calling private C api's (to bake 'locals' values back into the fast local variables) and is definettely not a good practice.但仅仅通过调用私有 C api(将“本地”值烘焙回快速局部变量),这绝对不是一个好习惯。

DOing it through a magic decorator would also be possible, but the decorator would have to rewrite the bytecode in the inner function - by replacing each access to a 'nonlocal' variable by retrieving and updating the value on the caler locals , and, at the end of the function - https://programtalk.com/python-examples/ctypes.pythonapi.PyFrame_LocalsToFast/也可以通过魔法装饰器来实现,但装饰器必须重写内部 function 中的字节码 - 通过检索和更新校准器locals上的值来替换对“非本地”变量的每次访问,并且,在function - https 的结尾://programtalk.com/python-examples/ctypes.pythonapi.PyFrame_LocalsToFast/

Example例子

So, that said, here is a proof of concept.所以,也就是说,这是一个概念证明。 It is, of course, thread, and async unsafe as hell - but if the attributes in the proxy class are promoted to threadlocals or context-local (pep 555), it should work.当然,它是线程和异步不安全的地狱- 但是如果代理 class 中的属性被提升为线程本地或上下文本地(pep 555),它应该可以工作。 it should be easy to adapt this to search for the local-variables to change up on the call stack (so that changes made in a sub-sub-call could change the grandparents locals, just as in dynamic scoped languages)它应该很容易适应它来搜索局部变量以在调用堆栈上进行更改(以便在子子调用中所做的更改可以更改祖父母的本地人,就像在动态范围的语言中一样)

As stated in the question, there is no need to declare the variables on the caller as anything - they just must be normal local variables.如问题中所述,无需将调用者上的变量声明为任何内容-它们必须是普通的局部变量。 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. 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. If you can't have even this, you will indeed have to resort to rewrite the bytecode on the decorated function, or use the hooks put in place for writing debuggers (setting "trace on" on the code).如果你连这个都做不到,你确实不得不求助于在装饰的 function 上重写字节码,或者使用为编写调试器而设置的钩子(在代码上设置“trace on”)。

nb the exact behavior of changes locals() was specified to the language recently - prior to 3.8, IIRC, - and "locals_to_fast" seems to be an stable enough API - but it might change in the future.注意,最近更改 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)

And, pasting all that on an IPython shell:并且,将所有内容粘贴到 IPython shell 上:

In [121]: bleh()                                                                                                                     
original value
new value

(I might add that it felt weird testing that, since the functions that have the local variables changed do not need any decorator, or any special declaration to the variables at all) (我可能会补充一点,测试感觉很奇怪,因为改变了局部变量的函数不需要任何装饰器,或者根本不需要对变量进行任何特殊声明)

ok, so after several hours of sitting on this thing i've managed to write a solution, there are some major pitfalls when approaching this and i'll note them below好的,所以在坐了几个小时之后,我设法编写了一个解决方案,在处理这个问题时有一些主要的陷阱,我会在下面指出它们

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

where the acutal code needed lies in the class inline_func and some of the imports (maybe you can make them internal to the class? i'm really not sure)所需的实际代码在class inline_func和一些导入中(也许您可以将它们设置为 class 的内部?我真的不确定)

so what does this whole thing do?那么这整件事是做什么的呢? well, it makes it so the code for strip_game and game are (nearly) identical, namely:好吧,它使strip_gamegame的代码(几乎)相同,即:

  1. it inserts a function prologue which updates the locals of the caller, then adds to locals of the caller to the callee.它插入一个 function 序言,更新调用者的本地变量,然后将调用者的本地变量添加到被调用者。
  2. insert a try finally block around the function在 function 周围插入一个 try finally 块
  3. changes every symbol lookup from a global lookup to a normal (name) lookup, after some thought i had realized that this doens't really have any effects将每个符号查找从全局查找更改为普通(名称)查找,经过一番思考,我意识到这并没有任何影响
  4. upon entering the finally block, updates the caller locals.进入 finally 块后,更新调用者本地人。

there are some major pitfalls making things like these, i'll list a few problems i've encountered:有一些主要的陷阱使这样的事情发生,我将列出我遇到的一些问题:

  1. cpython compiler_nameop function optimizes namespace lookup based on the simplicity of the given function, that means that it will optimize name lookups to global lookups if it can cpython compiler_nameop function 基于给定 function 的简单性优化命名空间查找,这意味着如果可以的话,它将优化名称查找到全局查找
  2. changing the bytecode means affecting the debug-ability of the program, i had addressed this in the co_lnotab variable更改字节码意味着影响程序的调试能力,我已经在co_lnotab变量中解决了这个问题
  3. for large functions this solution won't work as some of the opcodes would have to use extended_args: namely, the loads of the variables and the try-finally block (this point is solvable by using extended_args anyways...)对于大型函数,此解决方案将不起作用,因为某些操作码必须使用扩展参数:即变量的负载和 try-finally 块(这点无论如何都可以通过使用扩展参数来解决......)

thank @jsbueno for putting in the time and pointing me to PyFrame_LocalsToFast.感谢@jsbueno 投入时间并将我指向 PyFrame_LocalsToFast。

PS this solution works for python 2.7.6, python has some issues when it comes to stability of the API, so for newer versions this might need to be fixed. PS 此解决方案适用于 python 2.7.6,python 在 API 的稳定性方面存在一些问题,因此对于较新的版本,这可能需要修复。

暂无
暂无

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

相关问题 有没有一种方法可以使用python日志记录从调用方的范围进行打印 - Is there a way to use python logging to print from the scope of the caller 有没有办法像调用者所说的那样在被调用者中获取 function 参数? - Is there a way to get the function parameters in the calle as the caller put it? 使用泛型函数根据调用者执行操作? - Use a generic function to do stuff based on the caller? 如何使用“ var []。function()”调用方调度? - How to use the “var[].function()” caller dispatch? 在函数/方法中改变字典并返回给调用者的首选方式? - Preferred way of mutating dictionary inside function/method and returning to the caller? 在Python function中,有没有办法在调用方告诉参数的变量名? - In a Python function, Is there a way to tell the argument's variable name on the caller side? 将变量注入调用者的范围? - Injecting variables into the caller's scope? Python:引用将在以后声明的函数的优美方法 - Python: Elegant way to reference a function that will be declare later 有没有办法让多个函数的调用者以纯函数的方式将函数引用转发给选定的函数? - Is there a way for a caller of multiple functions to forward a function ref to selected functions in a purely functional way? 有没有办法使用Twilio python查找api打印输出并使用作为变量找到的调用者名称? - Is there a way to use Twilio python lookup api print output and use the caller name found as variable?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM