2010-10-11 14 views
13

Necesito una forma de "insertar" nombres en una función desde un bloque de código externo, por lo que son accesibles localmente y no necesitan ser manejados específicamente por el código de la función (definido como parámetros de función, cargado desde *args etc.)Función de enlace de nombre local desde un ámbito externo

El escenario simplificado: proporciona un marco dentro del cual los usuarios pueden definir (con la menor sintaxis posible) funciones personalizadas para manipular otros objetos del marco (que son no necesariamente global).

Idealmente, el usuario define

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

Aquí Cat, Mouse y Cheese son objetos marco que, por buenas razones, no puede ser limitada al espacio de nombres global.

Quiero escribir un contenedor para esta función se comporte de esta manera:

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 

Entonces este envoltorio se podría aplicar a todas las funciones definidas por el usuario (como decorador, por el propio usuario o de forma automática, aunque Planeo usar una metaclase).

@framework_wrap 
def user_func(): 

Soy consciente de nonlocal la palabra clave del Python 3, pero todavía consideran fea (desde la perspectiva del usuario del marco) para agregar una línea adicional:

nonlocal Cat, Mouse, Cheese 

y que preocuparse por la adición de todos los objetos que él necesita a esta línea.

Cualquier sugerencia es muy apreciada.

+1

¿Cuáles son las buenas razones para no poder usar el espacio de nombres global? Si está enhebrando, puede usar el almacenamiento local de subprocesos. –

+0

@Ivo van der Wijk: También conozco el hilo local, pero el enhebrado no es el problema aquí. Una de las razones sería que deseo que algunos nombres genéricos (como this) se inyecten en funciones definidas en varias clases, y para que cada clase se resuelva en algo específico de clase. Entonces this no puede ser global. – onysseus

+0

@amadaeus Si la función pega algo en el dict global o usa 'global' para modificar un global, ¿quieres que ese cambio afecte al global real? – aaronasterling

Respuesta

11

Cuanto más perder el tiempo con la pila, más me gustaría no haberlo hecho. No hackes globales para hacer lo que quieras. Hackear bytecode en su lugar. Hay dos maneras en que puedo pensar para hacer esto.

1) Agregue celdas que envuelven las referencias que desea en f.func_closure. Debe volver a montar el bytecode de la función para usar LOAD_DEREF en lugar de LOAD_GLOBAL y generar una celda para cada valor. A continuación, pasa una tupla de las celdas y el nuevo objeto de código al types.FunctionType y obtiene una función con los enlaces apropiados. Las diferentes copias de la función pueden tener diferentes enlaces locales, por lo que debe ser lo más seguro posible para las hebras.

2) Agregue argumentos para sus nuevos locales al final de la lista de argumentos de funciones. Reemplace las ocurrencias apropiadas de LOAD_GLOBAL con LOAD_FAST. Luego construya una nueva función usando types.FunctionType y pasando el nuevo objeto de código y una tupla de los enlaces que desee como la opción predeterminada. Esto es limitado en el sentido de que los límites de python funcionan con argumentos a 255 y no se puede usar en funciones que usan argumentos variables. Sin embargo, me pareció el más desafiante de los dos, así que fue el que implementé (y hay otras cosas que se pueden hacer con este). De nuevo, puede hacer diferentes copias de la función con diferentes enlaces o llamar a la función con los enlaces que desee de cada ubicación de llamada. Por lo tanto, también puede ser tan seguro para subprocesos como desee.

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) 

Tenga en cuenta que toda una rama de este código (el relativo a EXTENDED_ARG) no se ha probado, pero que para los casos comunes, parece ser bastante sólido. Lo piratearé y actualmente estoy escribiendo un código para validar el resultado. Luego (cuando lo encuentre) lo ejecutaré contra toda la biblioteca estándar y corregiré cualquier error.

Probablemente también estaré implementando la primera opción.

+0

¡Increíble trabajo de verdad! Personalmente, creo que el primer enfoque (que involucra las celdas de cierre de la función) es más limpio (si se puede etiquetar un bytecode como "limpio"). Creo que lo probaré usando Byteplay (http://wiki.python.org/moin/ByteplayDoc) – onysseus

+0

@amadaeus Estoy de acuerdo contigo acerca de que el primer enfoque sea más limpio. Estoy escribiendo código de prueba que debería funcionar para ambos enfoques. Lo publicaré cuando termine. Aunque no tenía idea acerca de los módulos de bytecode existentes. Tendré que mirarlos. Gracias por publicar. – aaronasterling

1

Si su aplicación es estrictamente Python 3, no veo cómo usar Python 3's nonlocal es más feo que escribir un decorador para manipular el espacio de nombres local de la función. Digo probar la solución nonlocal o replantear esta estrategia.

+0

Presenté el enfoque de decorador principalmente para su simplicidad Quiero que el contenedor para las funciones se llame a través de una metaclase, por lo que no será necesario que los usuarios apliquen manualmente el decorador. También me gustaría mantener el proyecto compatible con Python 2.x (> = 2.6) – onysseus

3

suena como usted tal vez desee estar usando exec code in dict, donde code es la función del usuario y dict es un diccionario que facilite la cual puede

  • ser precargada con referencias a objetos que el código de usuario debería poder para usar
  • almacene las funciones o variables declaradas por el código del usuario para su uso posterior por su marco.

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

Sin embargo, estoy bastante seguro de que esto sólo funcionaría si el código de usuario está siendo llevado en forma de cadena y es necesario que exec. Si la función ya está compilada, ya tendrá sus enlaces globales establecidos. Entonces, hacer algo como exec "user_func(*args)" in framework_dict no funcionará, porque los valores globales de user_func ya están configurados en el módulo en el que estaba definido como.

Dado que func_globals es de solo lectura, creo que tendrá que hacer algo como what martineau suggests para modificar la función global.

Creo que es probable (a menos que esté haciendo algo sin precedentes impresionante, o me falta algo de sutileza crítica) que probablemente sería mejor colocar sus objetos de marco en un módulo, y luego tener el código de usuario importado módulo. Las variables del módulo pueden reasignarse o mutarse o accederse con bastante facilidad mediante un código que se haya definido fuera de ese módulo, una vez que el módulo haya sido import ed.

creo que esto sería mejor para el código de legibilidad también, porque user_func resultado final será tener los espacios de nombre explícito para Cat, Dog, etc en lugar de los lectores no familiarizados con su marco que tiene que preguntarse de dónde venían. P.EJ. animal_farm.Mouse.eat(animal_farm.Cheese), o tal vez como líneas

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

Si son hacer algo unprecedently impresionante, creo que tendrá que utilizar la API de C para pasar argumentos a un objeto de código. Parece que la función PyEval_EvalCodeEx es la que desea.

+0

Me gusta lo limpio que es su enfoque. Algunos problemas, sin embargo: para evitar la compilación de código adicional, me gustaría ejecutar user_func.func_code (el objeto de código), pero no puedo encontrar ninguna forma de pasar argumentos adicionales a la llamada de user_func (si es necesario por la definición de la función)) Otro problema potencial es el manejo de globales en ciertos escenarios, pero eso no es realmente un problema por el momento. – onysseus

+0

Si agrega 'code' a' dict', puede 'exec 'code (parameters)" in dict'. – Zooba

+0

Pero, por supuesto, no está evitando la compilación adicional en ese caso, mi mal. Aunque si tiene inquietudes sobre el rendimiento (?) Acerca de la compilación de una llamada a función simple, un lenguaje interpretado (en su mayoría) no es la mejor opción de todos modos. – Zooba

4

respuesta Editado - restaura dict espacio de nombres después de llamar user_func()

probada usando Python 2.7.5 y 3.3.2

framework.py archivo: Uso

# 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 

Ejemplo:

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

Salida:

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

Me encontré con este enfoque y lo descarté rápidamente debido a http://docs.python.org/reference/datamodel.html#index-843 (func_globals se menciona como de solo lectura). Sé que eso significa que no puedes reasignar func_globals a otro dict, pero ¿es seguro modificarlo? – onysseus

+0

@amadaeus: Sí, vi la indicación del atributo RO, pero lo tomé como una reasignación a otra dicción, no dice dejar solos los valores mutables. @ AaronMcSmooth: Gracias por arreglar las cadenas docs de cotizaciones triples. Realmente odio el resaltador de sintaxis de StackOverflow que no se da cuenta de que está haciendo Python ... – martineau

+0

@martineau Me acabo de dar cuenta de que f.func_globals es en realidad una referencia al diccionario globals(), por lo que su código realmente vincula los nombres al espacio de nombres global . – onysseus

Cuestiones relacionadas