Назначение локального имени функции из внешней области

Мне нужен способ "вводить" имена в функцию из внешнего блока кода, поэтому они доступны локально и, им не нужно специально обрабатывать код функции (определенный как параметры функции, загруженные из *args и т.д.)

Упрощенный сценарий: предоставление структуры, в которой пользователи могут определять (с минимальным синтаксисом) настраиваемые функции для управления другими объектами фреймворка (которые не обязательно global).

В идеале пользователь определяет

def user_func():
    Mouse.eat(Cheese)
    if Cat.find(Mouse):
        Cat.happy += 1

Здесь Cat, Mouse и Cheese являются объектами инфраструктуры, которые по уважительным причинам не могут быть ограничены глобальным пространством имен.

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

def framework_wrap(user_func):
    # this is a framework internal and has name bindings to Cat, Mouse and Cheese
    def f():
        inject(user_func, {'Cat': Cat, 'Mouse': Mouse, 'Cheese': Cheese})
        user_func()
    return f

Затем эту оболочку можно применить ко всем пользовательским функциям (как декоратор, самому пользователю или автоматически, хотя я планирую использовать метакласс).

@framework_wrap
def user_func():

Мне известно о ключевом слове Python 3 nonlocal, но я все же считаю уродливым (с точки зрения пользователя каркаса) добавить дополнительную строку:

nonlocal Cat, Mouse, Cheese

и беспокоиться о добавлении каждого объекта, который ему нужен, в эту строку.

Любое предложение очень ценится.

Ответ 1

Чем больше я общаюсь со стеклом, тем больше я бы этого не хотел. Не взламывайте глобалы, чтобы делать то, что вы хотите. Вместо этого возьмите байт-код. Есть два способа, которые я могу придумать для этого.

1) Добавьте ячейки, которые будут привязаны к ссылкам, которые вы хотите добавить в f.func_closure. Вы должны собрать байт-код функции, чтобы использовать LOAD_DEREF вместо LOAD_GLOBAL и сгенерировать ячейку для каждого значения. Затем вы передаете кортеж клеток и новый объект кода в types.FunctionType и получите функцию с соответствующими привязками. Различные копии функции могут иметь разные локальные привязки, поэтому они должны быть как потокобезопасные, как вы хотите это сделать.

2) Добавьте аргументы для ваших новых локалей в конец списка аргументов функций. Замените соответствующие вхождения LOAD_GLOBAL на LOAD_FAST. Затем создайте новую функцию, используя types.FunctionType и передав в новый объект кода и кортеж привязок, которые вы хотите в качестве опции по умолчанию. Это ограничено в том смысле, что python ограничивает функциональные аргументы до 255 и не может использоваться для функций, которые используют переменные аргументы. Тем не менее это меня поразило как более сложное из двух, так что тот, который я реализовал (плюс там другой материал, который можно сделать с этим). Опять же, вы можете либо сделать разные копии функции с различными связями, либо вызвать функцию со связями, которые вы хотите от каждого места вызова. Так что это тоже может быть как потокобезопасное, как вы хотите это сделать.

import types
import opcode

# Opcode constants used for comparison and replacecment
LOAD_FAST = opcode.opmap['LOAD_FAST']
LOAD_GLOBAL = opcode.opmap['LOAD_GLOBAL']
STORE_FAST = opcode.opmap['STORE_FAST']

DEBUGGING = True

def append_arguments(code_obj, new_locals):
    co_varnames = code_obj.co_varnames   # Old locals
    co_names = code_obj.co_names      # Old globals
    co_argcount = code_obj.co_argcount     # Argument count
    co_code = code_obj.co_code         # The actual bytecode as a string

    # Make one pass over the bytecode to identify names that should be
    # left in code_obj.co_names.
    not_removed = set(opcode.hasname) - set([LOAD_GLOBAL])
    saved_names = set()
    for inst in instructions(co_code):
        if inst[0] in not_removed:
            saved_names.add(co_names[inst[1]])

    # Build co_names for the new code object. This should consist of 
    # globals that were only accessed via LOAD_GLOBAL
    names = tuple(name for name in co_names
                  if name not in set(new_locals) - saved_names)

    # Build a dictionary that maps the indices of the entries in co_names
    # to their entry in the new co_names
    name_translations = dict((co_names.index(name), i)
                             for i, name in enumerate(names))

    # Build co_varnames for the new code object. This should consist of
    # the entirety of co_varnames with new_locals spliced in after the
    # arguments
    new_locals_len = len(new_locals)
    varnames = (co_varnames[:co_argcount] + new_locals +
                co_varnames[co_argcount:])

    # Build the dictionary that maps indices of entries in the old co_varnames
    # to their indices in the new co_varnames
    range1, range2 = xrange(co_argcount), xrange(co_argcount, len(co_varnames))
    varname_translations = dict((i, i) for i in range1)
    varname_translations.update((i, i + new_locals_len) for i in range2)

    # Build the dictionary that maps indices of deleted entries of co_names
    # to their indices in the new co_varnames
    names_to_varnames = dict((co_names.index(name), varnames.index(name))
                             for name in new_locals)

    if DEBUGGING:
        print "injecting: {0}".format(new_locals)
        print "names: {0} -> {1}".format(co_names, names)
        print "varnames: {0} -> {1}".format(co_varnames, varnames)
        print "names_to_varnames: {0}".format(names_to_varnames)
        print "varname_translations: {0}".format(varname_translations)
        print "name_translations: {0}".format(name_translations)


    # Now we modify the actual bytecode
    modified = []
    for inst in instructions(code_obj.co_code):
        # If the instruction is a LOAD_GLOBAL, we have to check to see if
        # it one of the globals that we are replacing. Either way,
        # update its arg using the appropriate dict.
        if inst[0] == LOAD_GLOBAL:
            print "LOAD_GLOBAL: {0}".format(inst[1])
            if inst[1] in names_to_varnames:
                print "replacing with {0}: ".format(names_to_varnames[inst[1]])
                inst[0] = LOAD_FAST
                inst[1] = names_to_varnames[inst[1]]
            elif inst[1] in name_translations:    
                inst[1] = name_translations[inst[1]]
            else:
                raise ValueError("a name was lost in translation")
        # If it accesses co_varnames or co_names then update its argument.
        elif inst[0] in opcode.haslocal:
            inst[1] = varname_translations[inst[1]]
        elif inst[0] in opcode.hasname:
            inst[1] = name_translations[inst[1]]
        modified.extend(write_instruction(inst))

    code = ''.join(modified)
    # Done modifying codestring - make the code object

    return types.CodeType(co_argcount + new_locals_len,
                          code_obj.co_nlocals + new_locals_len,
                          code_obj.co_stacksize,
                          code_obj.co_flags,
                          code,
                          code_obj.co_consts,
                          names,
                          varnames,
                          code_obj.co_filename,
                          code_obj.co_name,
                          code_obj.co_firstlineno,
                          code_obj.co_lnotab)


def instructions(code):
    code = map(ord, code)
    i, L = 0, len(code)
    extended_arg = 0
    while i < L:
        op = code[i]
        i+= 1
        if op < opcode.HAVE_ARGUMENT:
            yield [op, None]
            continue
        oparg = code[i] + (code[i+1] << 8) + extended_arg
        extended_arg = 0
        i += 2
        if op == opcode.EXTENDED_ARG:
            extended_arg = oparg << 16
            continue
        yield [op, oparg]

def write_instruction(inst):
    op, oparg = inst
    if oparg is None:
        return [chr(op)]
    elif oparg <= 65536L:
        return [chr(op), chr(oparg & 255), chr((oparg >> 8) & 255)]
    elif oparg <= 4294967296L:
        return [chr(opcode.EXTENDED_ARG),
                chr((oparg >> 16) & 255),
                chr((oparg >> 24) & 255),
                chr(op),
                chr(oparg & 255),
                chr((oparg >> 8) & 255)]
    else:
        raise ValueError("Invalid oparg: {0} is too large".format(oparg))



if __name__=='__main__':
    import dis

    class Foo(object):
        y = 1

    z = 1
    def test(x):
        foo = Foo()
        foo.y = 1
        foo = x + y + z + foo.y
        print foo

    code_obj = append_arguments(test.func_code, ('y',))
    f = types.FunctionType(code_obj, test.func_globals, argdefs=(1,))
    if DEBUGGING:
        dis.dis(test)
        print '-'*20
        dis.dis(f)
    f(1)

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

Я также, вероятно, также буду использовать первый вариант.

Ответ 2

Отредактированный ответ - восстанавливает пространство имен dict после вызова user_func()

Протестировано с использованием Python 2.7.5 и 3.3.2

Файл framework.py:

# framework objects
class Cat: pass
class Mouse: pass
class Cheese: pass

_namespace = {'Cat':Cat, 'Mouse':Mouse, 'Cheese':Cheese } # names to be injected

# framework decorator
from functools import wraps
def wrap(f):
    func_globals = f.func_globals if hasattr(f,'func_globals') else f.__globals__
    @wraps(f)
    def wrapped(*args, **kwargs):
        # determine which names in framework _namespace collide and don't
        preexistent = set(name for name in _namespace if name in func_globals)
        nonexistent = set(name for name in _namespace if name not in preexistent)
        # save any preexistent name values
        f.globals_save = {name: func_globals[name] for name in preexistent}
        # temporarily inject framework _namespace
        func_globals.update(_namespace)

        retval = f(*args, **kwargs) # call function and save return value

        # clean up function namespace
        for name in nonexistent:
             del func_globals[name] # remove those that didn't exist
        # restore the values of any names that collided
        func_globals.update(f.globals_save)
        return retval

    return wrapped

Пример использования:

from __future__ import print_function
import framework

class Cat: pass  # name that collides with framework object

@framework.wrap
def user_func():
    print('in user_func():')
    print('  Cat:', Cat)
    print('  Mouse:', Mouse)
    print('  Cheese:', Cheese)

user_func()

print()
print('after user_func():')
for name in framework._namespace:
    if name in globals():
        print('  {} restored to {}'.format(name, globals()[name]))
    else:
        print('  {} not restored, does not exist'.format(name))

Вывод:

in user_func():
  Cat: <class 'framework.Cat'>
  Mouse: <class 'framework.Mouse'>
  Cheese: <class 'framework.Cheese'>

after user_func():
  Cheese not restored, does not exist
  Mouse not restored, does not exist
  Cat restored to <class '__main__.Cat'>

Ответ 3

Похоже, вы, возможно, захотите использовать exec code in dict, где code - это пользовательская функция, а dict - словарь, который вы предоставляете, который может

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

Документы для exec: http://docs.python.org/reference/simple_stmts.html#the-exec-statement

Однако я уверен, что это будет работать только в том случае, если код пользователя вводится в виде строки, и вам нужно выполнить его. Если функция уже скомпилирована, она уже установит глобальные привязки. Так что что-то вроде exec "user_func(*args)" in framework_dict не будет работать, потому что user_func globals уже установлены в модуль, в котором он был определен.

Так как func_globals только для чтения, я думаю, вам нужно сделать что-то вроде что предлагает martineau, чтобы изменить глобальные функции.

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

Я думаю, что это было бы лучше для чтения кода, потому что user_func в конечном итоге будет иметь явное пространство имен для Cat, Dog и т.д., а не читателей, незнакомых с вашей каркасной структурой, которые должны задаться вопросом, откуда они пришли. НАПРИМЕР. animal_farm.Mouse.eat(animal_farm.Cheese), или, может быть, строки типа

from animal_farm import Goat
cheese = make_cheese(Goat().milk())

Если вы делаете что-то беспрецедентное, я думаю, вам нужно будет использовать API C для передачи аргументов объекту кода. Похоже, функция PyEval_EvalCodeEx - это тот, который вы хотите.

Ответ 4

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