Function local name binding from an outer scope

Potrzebuję sposobu na "wstrzyknięcie" nazw do funkcji z zewnętrznego bloku kodu, aby były dostępne lokalnie i nie muszą być specjalnie obsługiwane przez kod funkcji (zdefiniowany jako parametry funkcji, załadowany z *args itp.)

Uproszczony scenariusz: dostarczenie frameworka, w ramach którego użytkownicy mogą definiować (z jak najmniejszą składnią) funkcje niestandardowe do manipulowania innymi obiektami frameworka (które są , a nie koniecznie global).

Idealnie, użytkownik definiuje

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

Tutaj Cat, Mouse i {[8] } są obiektami framework, które z dobrych powodów nie mogą być ograniczone do globalnej przestrzeni nazw.

Chcę napisać wrapper, aby ta funkcja zachowywała się tak:

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

Wtedy ten wrapper może być zastosowany do wszystkich funkcji zdefiniowanych przez użytkownika (jako dekorator, przez samego Użytkownika lub automatycznie, chociaż planuję użyć metaklasy).

@framework_wrap
def user_func():

Znam Pythona 3 nonlocal słowo kluczowe, ale nadal uważam ugly (z punktu widzenia użytkownika frameworka) za dodanie dodatkowej linii:

nonlocal Cat, Mouse, Cheese

I martwić się o dodanie każdego obiektu, którego potrzebuje do tej linii.

Każda sugestia jest bardzo mile widziana.
Author: martineau, 2010-10-11

4 answers

Im bardziej zadzieram ze stosem, tym bardziej żałuję, że Nie. Nie hakuj globali, aby robić to, co chcesz. Zamiast tego Hack bytecode. Mogę to zrobić na dwa sposoby.

1) Dodaj komórki zawijające odwołania do f.func_closure. Musisz ponownie zmontować kod bajtowy funkcji, aby użyć LOAD_DEREF zamiast LOAD_GLOBAL i wygenerować komórkę dla każdej wartości. Następnie przekazujesz krotkę komórek i nowy obiekt kodu types.FunctionType i otrzymujesz funkcję z odpowiednimi powiązaniami. Różne kopie funkcji mogą mieć różne wiązania lokalne, więc powinny być tak bezpieczne, jak chcesz.

2) Dodaj argumenty dla nowych miejsc na końcu listy argumentów funkcji. Zastąpić odpowiednie wystąpienia {[3] } LOAD_FAST. Następnie utwórz nową funkcję, używając types.FunctionType i przekazując nowy obiekt kodu oraz krotkę powiązań, które chcesz jako opcję domyślną. Jest to ograniczone w tym sensie, że python ogranicza argumenty funkcji do 255 i nie może być używany w funkcjach, które używają zmiennych argumentów. Niemniej jednak uderzyło mnie to jako większe wyzwanie z dwóch, więc to jest ten, który wdrożyłem(plus są inne rzeczy, które można zrobić z tym jednym). Ponownie możesz tworzyć różne kopie funkcji z różnymi powiązaniami lub wywoływać funkcję z żądanymi powiązaniami z każdej lokalizacji połączenia. Więc to też może być tak bezpieczny wątek, jak chcesz.

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)

Zauważ, że cała gałąź tego kodu (że relating to EXTENDED_ARG) nie jest testowane, ale w powszechnych przypadkach wydaje się całkiem solidne. Będę hacking na nim i obecnie piszę jakiś kod, aby zweryfikować wyjście. Następnie (kiedy się do niego przejdę) uruchomię go z całą biblioteką standardową i naprawię wszelkie błędy.

Prawdopodobnie też będę wdrażał pierwszą opcję.

 11
Author: aaronasterling,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-15 11:58:20

Edytowana ODPOWIEDŹ -- przywraca przestrzeń nazw dict po wywołaniu user_func()

Testowane przy użyciu Pythona 2.7.5 i 3.3.2

Plik 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

Przykładowe użycie:

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

Wyjście:

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'>
 4
Author: martineau,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-08 19:19:24

Brzmi jakbyś chciał używać exec code in dict, Gdzie code jest funkcją użytkownika, a {[3] } jest słownikiem, który możesz podać

  • być wstępnie wypełnione odniesieniami do obiektów, których kod użytkownika powinien być w stanie użyć
  • Przechowuj wszystkie funkcje lub zmienne zadeklarowane przez kod użytkownika do późniejszego wykorzystania przez framework.

Docs for exec: http://docs.python.org/reference/simple_stmts.html#the-exec-statement

Jednakże, jestem całkiem pewien że będzie to działać tylko wtedy, gdy kod użytkownika jest wprowadzany jako ciąg znaków i trzeba go exec. Jeśli funkcja jest już skompilowana, będzie miała już ustawione globalne powiązania. Więc zrobienie czegoś w rodzaju exec "user_func(*args)" in framework_dict nie zadziała, ponieważ globale user_func są już ustawione na moduł, w którym zostało zdefiniowane .

Ponieważ func_globals jest tylko odczytywane, myślę, że będziesz musiał zrobić coś takiego , co sugeruje martineau, aby zmodyfikować globale funkcji.

Myślę prawdopodobnie (chyba, że robisz coś niesamowitego, lub brakuje mi jakiejś krytycznej subtelności), że prawdopodobnie lepiej byłoby umieścić swoje obiekty frameworku w module, a następnie zaimportować kod użytkownika Ten moduł. Zmienne modułu mogą być przypisane do lub zmutowane lub łatwo dostępne za pomocą kodu, który został zdefiniowany poza tym modułem, gdy moduł został imported.

Myślę, że byłoby to również lepsze dla czytelności kodu, ponieważ user_func skończy się mając jawne przestrzenie nazw dla Cat, Dog, itd. zamiast czytelników nieznających Twoich RAM zastanawiać się, skąd pochodzą. Np. animal_farm.Mouse.eat(animal_farm.Cheese), a może linie typu

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

Jeśli robisz coś niesamowitego, myślę, że będziesz musiał użyć C API, aby przekazać argumenty do obiektu kodu. Wygląda na to, że funkcja PyEval_EvalCodeEx jest tą, którą chcesz.

 3
Author: intuited,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:01

Jeśli Twoja aplikacja jest ściśle Pythonem 3, nie widzę, jak używanie nonlocal Pythona 3 jest brzydsze niż pisanie dekoratora do manipulowania lokalną przestrzenią nazw funkcji. Proponuję wypróbować rozwiązanie nonlocal lub przemyśleć tę strategię.

 1
Author: jathanism,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-11 18:00:45