Есть ли способ объявить, что функция должна использовать область вызова? - PullRequest
0 голосов
/ 03 апреля 2020

существует ли feautre, похожий на макросы C, который позволяет повторно использовать код встроенным способом, не создавая отдельную область для этого фрагмента кода?

, например:

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

напечатает 3, однако я хочу, чтобы он напечатал 4.

Я знаю о решениях, включающих объекты, такие как классы или глобальный диктат, однако я ищу более примитивное решение (например, декоратор функций для пример), который просто позволил бы мне внести изменения в область действия вызывающей стороны.

большое спасибо

edit: любое решение, которое требует объявления, какие переменные я собираюсь использовать ИЛИ, объявляя заранее «пространство имен», подобное объектам 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, не нарушая отлаживаемость программы, потому что я думал только о создании нового объекта кода или использовании exe c, оба страдали от некоторых серьезных проблем.

ОСНОВНОЕ РЕДАКТИРОВАНИЕ:

хорошо, так что у меня есть что-то близкое к рабочему решению, однако я столкнулся с очень странной проблемой:

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       

Почему возникает такая разница?

Ответы [ 2 ]

1 голос
/ 04 апреля 2020

хорошо, так что после нескольких часов сидения на этой вещи мне удалось написать решение, при приближении к этому есть некоторые серьезные подводные камни, и я отмечу их ниже

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 и некоторых импортах (может быть, вы можете сделать их внутренними по отношению к классу? я действительно не уверен)

так, что делает все это? ну, это делает так, что код для strip_game и game (почти) идентичен, а именно:

  1. он вставляет пролог функции, который обновляет локальные адреса вызывающего, а затем добавляет локальные от вызывающего к вызываемому.
  2. вставьте блок try finally вокруг функции
  3. изменяет каждый поиск символа с глобального поиска на обычный поиск (имя), после некоторой мысли, что я понял, что на самом деле это не имеет никаких эффектов
  4. при входе в блок finally, обновляет локальные номера вызывающих абонентов.

есть некоторые серьезные подводные камни, создающие подобные вещи, я перечислю несколько проблемы, с которыми я столкнулся:

  1. cpython compiler_nameop функция оптимизирует поиск в пространстве имен, основываясь на простоте данной функции, это означает, что она оптимизирует поиск по имени для глобального поиска, если она может
  2. изменить байт-код, что влияет на возможность отладки программы, я рассмотрел это в переменной co_lnotab
  3. для больших функций, которые это соль Эта опция не будет работать, так как некоторые коды операций должны будут использовать extended_args: загрузку переменных и блок try-finally (эта точка в любом случае разрешима с помощью extended_args ...)

спасибо @jsbueno за потраченное время и указание на PyFrame_LocalsToFast.

PS это решение работает для python 2.7.6, python имеет некоторые проблемы, когда речь заходит о стабильности API, поэтому для в новых версиях это может потребоваться исправить.

0 голосов
/ 03 апреля 2020

В этом случае просто используйте ключевое слово global:

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

Это изменяет внешнюю область, если она глобальная.

Если внешняя область является функцией, то вместо этого используется ключевое слово nonlocal, которое было введено с Python 3.0.

dynamici c scoping

Однако изменение области действия вызывающей функции не является предпосылкой Python и является характеристикой языка c.

Это можно сделать. Но просто вызывать private C api (чтобы запечь значения 'localals' обратно в быстрые локальные переменные) и это определенно не очень хорошая практика.

Делать это через магический c декоратор также возможно, но декоратору придется переписывать байт-код во внутренней функции - заменяя каждый доступ к «нелокальной» переменной путем извлечения и обновления значения на вызывающем устройстве locals, а в конце функции - https://programtalk.com/python-examples/ctypes.pythonapi.PyFrame_LocalsToFast/

Пример

Итак, как говорится, вот подтверждение концепции. Это, конечно, thread и asyn c небезопасно как ад - но если атрибуты в прокси-классе повышены до threadlocals или context-local (pep 555), это должно работать. должно быть легко адаптировать это для поиска локальных переменных, чтобы изменить их в стеке вызовов (чтобы изменения, сделанные в суб-суб-вызове, могли изменить локальных бабушек и дедушек, как в динамических c языках с областями видимости)

Как указано в вопросе, нет необходимости объявлять переменные в вызывающей стороне как что-либо - они просто должны быть обычными локальными переменными. Однако для этого требуется, чтобы в декорированной функции было объявлено, что переменные, которые я хочу изменить в области вызова, как «глобальные», поэтому изменение будет go через объект, который я могу настроить. Если у вас нет даже этого, вам действительно придется прибегнуть к переписыванию байт-кода для декорированной функции или использовать зацепки, установленные на месте для написания отладчиков (установив «трассировку» в коде).

nb точное поведение изменений locals () было недавно задано для языка - до 3.8, IIR C, - и "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)

И, вставив все это в оболочку I Python:

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

(я мог бы добавить, что это было странно проверять это, поскольку функции с локальными переменными для изменения не требуется никакого декоратора или какого-либо специального объявления переменных)

...