Привязка локального имени функции из внешней области видимости - PullRequest
13 голосов
/ 11 октября 2010

Мне нужен способ «вставить» имена в функцию из внешнего блока кода, чтобы они были доступны локально и , их не нужно специально обрабатывать кодом функции (определяется как функцияпараметры, загруженные из *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

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

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

Ответы [ 4 ]

11 голосов
/ 12 октября 2010

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

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's 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) не проверена, но для обычных случаев она выглядит довольно солидной Я буду взламывать его и сейчас пишу код для проверки вывода. Затем (когда я доберусь до этого) я запустлю его для всей стандартной библиотеки и исправлю все ошибки.

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

4 голосов
/ 12 октября 2010

Отредактированный ответ - восстанавливает 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's _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's values
        f.globals_save = {name: func_globals[name] for name in preexistent}
        # temporarily inject framework's _namespace
        func_globals.update(_namespace)

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

        # clean up function's 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 голосов
/ 11 октября 2010

Звучит так, как будто вы, возможно, захотите использовать 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 уже настроены для модуля, в котором было определено .

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

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

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

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

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

1 голос
/ 11 октября 2010

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

...