Funktion Ortsname Bindung aus einem äußeren Bereich

Ich brauche einen Weg, um Namen in eine Funktion aus einem äußeren Codeblock zu injizieren, also sind sie lokal zugänglich und müssen nicht speziell vom Code der Funktion behandelt werden (definiert als Funktionsparameter, geladen von *args etc.)

Das vereinfachte Szenario: Bereitstellung eines Frameworks, in dem die Benutzer in der Lage sind, benutzerdefinierte Funktionen zu definieren (mit so wenig Syntax wie möglich), um andere Objekte des Frameworks zu manipulieren (die nicht unbedingt global ).

Idealerweise definiert der Benutzer

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

Hier sind Cat , Mouse und Cheese Rahmenobjekte, die aus guten Gründen nicht an den globalen Namensraum beschränkt werden können.

Ich möchte einen Wrapper für diese Funktion schreiben, um sich so zu verhalten:

 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 

Dann könnte dieser Wrapper auf alle benutzerdefinierten Funktionen angewendet werden (als Dekorateur, vom Benutzer selbst oder automatisch, obwohl ich eine Metaklasse verwenden möchte).

 @framework_wrap def user_func(): 

Ich bin mir des nonlocal Keyword des Python 3 bewusst, aber ich nonlocal immer noch hässlich (aus der User-Perspektive des Frameworks), um eine zusätzliche Zeile hinzuzufügen:

 nonlocal Cat, Mouse, Cheese 

Und sich darum zu kümmern, jedes Objekt hinzuzufügen, das er dieser Linie braucht.

Jeder Vorschlag wird sehr geschätzt.

4 Solutions collect form web for “Funktion Ortsname Bindung aus einem äußeren Bereich”

Je mehr ich mit dem Stapel herumgehe, desto mehr wünschte ich es nicht. Hacken Sie nicht, um zu tun, was Sie wollen. Hack Bytecode stattdessen. Es gibt zwei Möglichkeiten, die ich mir vorstellen kann.

1) Fügen Sie Zellen hinzu, die die Referenzen verpacken, die Sie in f.func_closure . Sie müssen den Bytecode der Funktion wieder zusammenbauen, um LOAD_DEREF anstelle von LOAD_GLOBAL und eine Zelle für jeden Wert zu erzeugen. Sie übergeben dann ein Tupel der Zellen und das neue types.FunctionType zu types.FunctionType und erhalten eine Funktion mit den entsprechenden Bindungen. Verschiedene Kopien der Funktion können unterschiedliche lokale Bindungen haben, also sollte es so gewinnt sich wie Sie es machen wollen.

2) Fügen Sie Argumente für Ihre neuen Einheimischen am Ende der Funktionsargumentliste hinzu. Ersetzen Sie das entsprechende Vorkommen von LOAD_GLOBAL durch LOAD_FAST . Dann konstruiere eine neue Funktion unter Verwendung von types.FunctionType und übergeben das neue types.FunctionType und ein Tupel der Bindungen, die du als Standardoption wünschst. Dies ist in dem Sinne begrenzt, dass Pythonschlüssel Funktionsargumente auf 255 beschränken und es nicht auf Funktionen verwendet werden kann, die variable Argumente verwenden. Trotzdem schlug es mich als die anspruchsvollere der beiden, so dass ist die, die ich implementiert (plus gibt es andere Sachen, die mit diesem getan werden kann). Wieder können Sie entweder verschiedene Kopien der Funktion mit verschiedenen Bindungen oder rufen Sie die Funktion mit den Bindungen, die Sie von jedem Anruf Ort wollen. So kann es auch so gewinnt wie Sie es machen wollen.

 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) 

Beachten Sie, dass ein ganzer Zweig dieses Codes (der sich auf EXTENDED_ARG ) ungetestet ist, aber für gewöhnliche Fälle scheint es ziemlich fest zu sein. Ich werde auf sie hacken und schreibe derzeit einen Code, um die Ausgabe zu validieren. Dann (wenn ich da herumkomme) werde ich es gegen die ganze Standardbibliothek laufen und irgendwelche Bugs beheben.

Ich werde wahrscheinlich auch die erste Option implementieren.

Bearbeitete Antwort – Wiederherstellung des Namensraums nach dem Aufruf von user_func()

Geprüft mit Python 2.7.5 und 3.3.2

Datei 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 

Beispiel:

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

Ausgabe:

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

Klingt wie Sie vielleicht wollen, um exec code in dict , wo code ist die Benutzer-Funktion und dict ist ein Wörterbuch, das Sie bieten, die können

  • Mit Verweisen auf Objekte, die der Benutzercode verwenden darf, vorgefüllt sein
  • Speichern Sie alle Funktionen oder Variablen, die durch den Code des Benutzers für die spätere Verwendung durch Ihr Framework deklariert sind.

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

Allerdings bin ich mir ziemlich sicher, dass dies nur funktionieren würde, wenn der Code des Benutzers als String eingefügt wird und du es ausführen musst. Wenn die Funktion bereits kompiliert ist, wird sie bereits ihre globalen Bindungen gesetzt. Also tut so etwas wie exec "user_func(*args)" in framework_dict wird nicht funktionieren, da user_func 's globals bereits auf das Modul gesetzt ist, in dem es definiert wurde .

Da func_globals ist, denke ich, dass du etwas tun musst, was martineau vorschlägt , um die Funktion globals zu modifizieren.

Ich denke es wahrscheinlich (es sei denn, du machst etwas beispielloses Ehrfürchtiges, oder ich vermisse eine kritische Subtilität), dass du wahrscheinlich besser dran bist, deine Framework-Objekte in ein Modul zu legen und dann den User-Code das Modul zu importieren. Modulvariablen können neu zugewiesen oder verwandelt werden oder ganz einfach durch Code zugewiesen werden, der außerhalb dieses Moduls definiert wurde, sobald das Modul import .

Ich denke, das wäre besser für Code-Lesbarkeit auch, weil user_func wird am Ende mit expliziten Namensraum für Cat , Dog , etc. anstatt Leser nicht vertraut mit Ihrem Framework zu fragen, woher sie kamen. EG animal_farm.Mouse.eat(animal_farm.Cheese) , oder vielleicht Zeilen wie

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

Wenn du etwas Unerwartetes toll machst, denke ich, dass du die C-API verwenden musst, um Argumente an ein Codeobjekt zu übergeben. Es sieht so aus wie die Funktion PyEval_EvalCodeEx ist die, die du willst.

Wenn Ihre Anwendung ist streng Python 3, ich sehe nicht, wie mit Python 3's nonlocal ist irgendwie hässlicher als Schreiben eines Dekorateurs zu manipulieren Funktion lokalen Namespace. Ich sage, geben die nonlocal Lösung einen Versuch oder überdenken diese Strategie.

  • Wie kann man neue Scopes in Python schaffen?
  • Warum ist eine Klassenvariable nicht im Listenverständnis definiert, sondern eine andere ist?
  • Variabler Bereich (Python Newbie)
  • Python: Wie erfasse ich eine Variable, die in einem nicht globalen Ahnenumfang angegeben ist?
  • UnboundLocalError bei der Manipulation von Variablen ergibt inkonsistentes Verhalten
  • String mit 'f' Präfix in python-3.6
  • Blockieren Sie den Bereich in Python
  • Python überschreibt Variablen in verschachtelten Funktionen
  • Python Variable Scope (Vorbehalt durch Verweis oder Kopie?)
  • Kurze Beschreibung der Scoping Regeln?
  • Wie kann ich auf eine Klassenvariable in einem Initialisierer in Python zugreifen?
  • Python ist die beste Programmiersprache der Welt.