utiliza resultado qué procedimientos por parametros para omision metodos metodo los llamar función funciones dentro definición consola comandos comando python closures monkeypatching

resultado - parametros por omision python



¿Puedes parchear*solo*una función anidada con cierre, o debe repetirse toda la función externa? (3)

Una biblioteca de terceros que utilizamos contiene una función bastante larga que utiliza una función anidada dentro de ella. Nuestro uso de esa biblioteca desencadena un error en esa función, y nos gustaría mucho resolverlo.

Desafortunadamente, los mantenedores de la biblioteca son algo lentos con las correcciones, pero no queremos tener que bifurcar la biblioteca. Tampoco podemos retener nuestra liberación hasta que hayan solucionado el problema.

Preferiríamos usar parches de mono para solucionar este problema aquí, ya que es más fácil de rastrear que parchear la fuente. Sin embargo, repetir una función muy grande en la que simplemente reemplazar la función interna sería suficiente, es excesivo y hace que a los demás les resulte más difícil ver qué cambiamos exactamente. ¿Estamos atrapados con un parche estático en el huevo de la biblioteca?

La función interna se basa en cerrar sobre una variable; Un ejemplo artificial sería:

def outerfunction(*args): def innerfunction(val): return someformat.format(val) someformat = ''Foo: {}'' for arg in args: yield innerfunction(arg)

donde quisiéramos reemplazar solo la implementación de innerfunction() . La función externa real es mucho, mucho más larga. Reutilizaríamos la variable cerrada y mantendríamos la firma de la función, por supuesto.


La respuesta de Martijn es buena, pero hay un inconveniente que sería bueno eliminar:

Desea asegurarse de que el objeto de código interno de reemplazo solo enumere esos mismos nombres como variables libres, y lo haga en el mismo orden.

Esta no es una restricción particularmente difícil para el caso normal, pero no es agradable depender de comportamientos indefinidos como el ordenamiento de nombres y cuando las cosas van mal, hay errores potencialmente realmente desagradables y posiblemente incluso accidentes graves.

Mi enfoque tiene sus propios inconvenientes, pero en la mayoría de los casos creo que el inconveniente anterior motivaría su uso. Por lo que puedo decir, también debería ser más portátil.

El enfoque básico es cargar la fuente con inspect.getsource , cambiarla y luego evaluarla. Esto se hace a nivel AST para mantener las cosas en orden.

Aquí está el código:

import ast import inspect import sys class AstReplaceInner(ast.NodeTransformer): def __init__(self, replacement): self.replacement = replacement def visit_FunctionDef(self, node): if node.name == self.replacement.name: # Prevent the replacement AST from messing # with the outer AST''s line numbers return ast.copy_location(self.replacement, node) self.generic_visit(node) return node def ast_replace_inner(outer, inner, name=None): if name is None: name = inner.__name__ outer_ast = ast.parse(inspect.getsource(outer)) inner_ast = ast.parse(inspect.getsource(inner)) # Fix the source lines for the outer AST outer_ast = ast.increment_lineno(outer_ast, inspect.getsourcelines(outer)[1] - 1) # outer_ast should be a module so it can be evaluated; # inner_ast should be a function so we strip the module node inner_ast = inner_ast.body[0] # Replace the function inner_ast.name = name modified_ast = AstReplaceInner(inner_ast).visit(outer_ast) # Evaluate the modified AST in the original module''s scope compiled = compile(modified_ast, inspect.getsourcefile(outer), "exec") outer_globals = outer.__globals__ if sys.version_info >= (3,) else outer.func_globals exec_scope = {} exec(compiled, outer_globals, exec_scope) return exec_scope.popitem()[1]

Un recorrido rápido. AstReplaceInner es un ast.NodeTransformer , que solo le permite modificar los AST asignando ciertos nodos a otros nodos. En este caso, se necesita un nodo de reemplazo para reemplazar un nodo ast.FunctionDef con cuando los nombres coincidan.

ast_replace_inner es la función que realmente nos importa, que tiene dos funciones y opcionalmente un nombre. El nombre se usa para permitir reemplazar la función interna con otra función de un nombre diferente.

Los AST se analizan:

outer_ast = ast.parse(inspect.getsource(outer)) inner_ast = ast.parse(inspect.getsource(inner))

La transformación se realiza:

modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

Se evalúa el código y se extrae la función:

exec(compiled, outer_globals, exec_scope) return exec_scope.popitem()[1]

Aquí hay un ejemplo de uso. Suponga que este código antiguo está en buggy.py :

def outerfunction(): numerator = 10.0 def innerfunction(denominator): return denominator / numerator return innerfunction

Desea reemplazar la función innerfunction con

def innerfunction(denominator): return numerator / denominator

Usted escribe:

import buggy def innerfunction(denominator): return numerator / denominator buggy.outerfunction = ast_replace_inner(buggy.outerfunction, innerfunction)

Alternativamente, podrías escribir:

def divide(denominator): return numerator / denominator buggy.outerfunction = ast_replace_inner(buggy.outerfunction, divide, "innerfunction")

La principal desventaja de esta técnica es que uno requiere inspect.getsource para trabajar tanto en el objetivo como en el reemplazo. Esto fallará si el destino está "incorporado" (escrito en C) o compilado en bytecode antes de distribuirlo. Tenga en cuenta que si está incorporado, la técnica de Martijn tampoco funcionará.

Otra desventaja importante es que los números de línea de la función interna están completamente atornillados. Este no es un gran problema si la función interna es pequeña, pero vale la pena pensar en una función interna grande.

Otras desventajas provienen si el objeto de función no se especifica de la misma manera. Por ejemplo, no podías parchear

def outerfunction(): numerator = 10.0 innerfunction = lambda denominator: denominator / numerator return innerfunction

de la misma manera se necesitaría una transformación AST diferente.

Debe decidir qué compensación tiene más sentido para su circunstancia particular.


Necesitaba esto, pero en una clase y python2 / 3. Entonces extendí la solución de @ MartijnPieters

import types, inspect, six def replace_inner_function(outer, new_inner, class_class=None): """Replace a nested function code object used by outer with new_inner The replacement new_inner must use the same name and must at most use the same closures as the original. """ if hasattr(new_inner, ''__code__''): # support both functions and code objects new_inner = new_inner.__code__ # find original code object so we can validate the closures match ocode = outer.__code__ iname = new_inner.co_name orig_inner = next( const for const in ocode.co_consts if isinstance(const, types.CodeType) and const.co_name == iname) # you can ignore later closures, but since they are matched by position # the new sequence must match the start of the old. assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] == new_inner.co_freevars), ''New closures must match originals'' # replace the code object for the inner function new_consts = tuple( new_inner if const is orig_inner else const for const in outer.__code__.co_consts) if six.PY3: new_code = types.CodeType(ocode.co_argcount, ocode.co_kwonlyargcount, ocode.co_nlocals, ocode.co_stacksize, ocode.co_flags, ocode.co_code, new_consts, ocode.co_names, ocode.co_varnames, ocode.co_filename, ocode.co_name, ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars, ocode.co_cellvars) else: # create a new function object with the new constants new_code = types.CodeType(ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize, ocode.co_flags, ocode.co_code, new_consts, ocode.co_names, ocode.co_varnames, ocode.co_filename, ocode.co_name, ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars, ocode.co_cellvars) new_function= types.FunctionType(new_code, outer.__globals__, outer.__name__, outer.__defaults__, outer.__closure__) if hasattr(outer, ''__self__''): if outer.__self__ is None: if six.PY3: return types.MethodType(new_function, outer.__self__, class_class) else: return types.MethodType(new_function, outer.__self__, outer.im_class) else: return types.MethodType(new_function, outer.__self__, outer.__self__.__class__) return new_function

Esto debería funcionar ahora para funciones, métodos de clase vinculados y métodos de clase no vinculados. (El argumento class_class solo es necesario para python3 para métodos independientes). ¡Gracias @MartijnPieters por hacer la mayor parte del trabajo! Nunca habría resuelto esto;)


Sí, puede reemplazar una función interna, incluso si está usando un cierre. Sin embargo, tendrás que saltar algunos aros. Por favor tenlo en cuenta:

  1. También debe crear la función de reemplazo como una función anidada, para garantizar que Python cree el mismo cierre. Si la función original tiene un cierre sobre los nombres foo y bar , debe definir su reemplazo como una función anidada con los mismos nombres cerrados. Más importante aún, debe usar esos nombres en el mismo orden ; los cierres están referenciados por índice.

  2. Los parches de mono siempre son frágiles y pueden romperse con el cambio de implementación. Esto no es una excepción. Vuelva a probar su parche de mono cada vez que cambie las versiones de la biblioteca parcheada.

Para entender cómo funcionará esto, primero explicaré cómo Python maneja las funciones anidadas. Python usa objetos de código para producir objetos de función según sea necesario. Cada objeto de código tiene una secuencia de constantes asociadas, y los objetos de código para funciones anidadas se almacenan en esa secuencia:

>>> def outerfunction(*args): ... def innerfunction(val): ... return someformat.format(val) ... someformat = ''Foo: {}'' ... for arg in args: ... yield innerfunction(arg) ... >>> outerfunction.__code__ <code object outerfunction at 0x105b27ab0, file "<stdin>", line 1> >>> outerfunction.__code__.co_consts (None, <code object innerfunction at 0x10f136ed0, file "<stdin>", line 2>, ''outerfunction.<locals>.innerfunction'', ''Foo: {}'')

La secuencia co_consts es un objeto inmutable, una tupla, por lo que no podemos simplemente intercambiar el objeto de código interno. Más adelante mostraré cómo produciremos un nuevo objeto de función con solo ese objeto de código reemplazado.

A continuación, debemos cubrir los cierres. En tiempo de compilación, Python determina que a) someformat no es un nombre local en función innerfunction y que b) se está cerrando sobre el mismo nombre en función outerfunction . Python no solo genera el código de someformat para producir las búsquedas correctas de nombres, sino que los objetos de código para las funciones anidadas y externas se anotan para registrar que someformat va a cerrar:

>>> outerfunction.__code__.co_cellvars (''someformat'',) >>> outerfunction.__code__.co_consts[1].co_freevars (''someformat'',)

Desea asegurarse de que el objeto de código interno de reemplazo solo enumere esos mismos nombres como variables libres, y lo haga en el mismo orden.

Los cierres se crean en tiempo de ejecución; El código de bytes para producirlos es parte de la función externa:

>>> import dis >>> dis.dis(outerfunction) 2 0 LOAD_CLOSURE 0 (someformat) 2 BUILD_TUPLE 1 4 LOAD_CONST 1 (<code object innerfunction at 0x10f136ed0, file "<stdin>", line 2>) 6 LOAD_CONST 2 (''outerfunction.<locals>.innerfunction'') 8 MAKE_FUNCTION 8 (closure) 10 STORE_FAST 1 (innerfunction) # ... rest of disassembly omitted ...

El LOAD_CLOSURE crea un cierre para la variable someformat ; Python crea tantos cierres como los que usa la función en el orden en que se usan por primera vez en la función interna . Este es un hecho importante para recordar para más adelante. La función en sí busca estos cierres por posición:

>>> dis.dis(outerfunction.__code__.co_consts[1]) 3 0 LOAD_DEREF 0 (someformat) 2 LOAD_METHOD 0 (format) 4 LOAD_FAST 0 (val) 6 CALL_METHOD 1 8 RETURN_VALUE

El LOAD_DEREF operación LOAD_DEREF seleccionó el cierre en la posición 0 aquí para obtener acceso al cierre de someformat .

En teoría, esto también significa que puede usar nombres completamente diferentes para los cierres en su función interna, pero para fines de depuración tiene mucho más sentido atenerse a los mismos nombres. También facilita la verificación de que la función de reemplazo se co_freevars correctamente, ya que puede comparar las tuplas co_freevars si usa los mismos nombres.

Ahora para el truco de intercambio. Las funciones son objetos como cualquier otro en Python, instancias de un tipo específico. El tipo no se expone normalmente, pero la llamada type() aún lo devuelve. Lo mismo se aplica a los objetos de código, y ambos tipos incluso tienen documentación:

>>> type(outerfunction) <type ''function''> >>> print(type(outerfunction).__doc__) Create a function object. code a code object globals the globals dictionary name a string that overrides the name from the code object argdefs a tuple that specifies the default argument values closure a tuple that supplies the bindings for free variables >>> type(outerfunction.__code__) <type ''code''> >>> print(type(outerfunction.__code__).__doc__) code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, firstlineno, lnotab[, freevars[, cellvars]]) Create a code object. Not for the faint of heart.

(El recuento exacto de argumentos y la cadena de documentación varía entre las versiones de Python; Python 3.0 agregó el argumento kwonlyargcount y, a partir de Python 3.8, se agregó el recuento posonlyargcount).

Utilizaremos estos objetos de tipo para producir un nuevo objeto de code con constantes actualizadas, y luego un nuevo objeto de función con un objeto de código actualizado; La siguiente función es compatible con las versiones de Python 2.7 a 3.8.

def replace_inner_function(outer, new_inner): """Replace a nested function code object used by outer with new_inner The replacement new_inner must use the same name and must at most use the same closures as the original. """ if hasattr(new_inner, ''__code__''): # support both functions and code objects new_inner = new_inner.__code__ # find original code object so we can validate the closures match ocode = outer.__code__ function, code = type(outer), type(ocode) iname = new_inner.co_name orig_inner = next( const for const in ocode.co_consts if isinstance(const, code) and const.co_name == iname) # you can ignore later closures, but since they are matched by position # the new sequence must match the start of the old. assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] == new_inner.co_freevars), ''New closures must match originals'' # replace the code object for the inner function new_consts = tuple( new_inner if const is orig_inner else const for const in outer.__code__.co_consts) # create a new code object with the new constants try: # Python 3.8 added code.replace(), so much more convenient! ncode = ocode.replace(co_consts=new_consts) except AttributeError: # older Python versions, argument counts vary so we need to check # for specifics. args = [ ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize, ocode.co_flags, ocode.co_code, new_consts, # replacing the constants ocode.co_names, ocode.co_varnames, ocode.co_filename, ocode.co_name, ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars, ocode.co_cellvars, ] if hasattr(ocode, ''co_kwonlyargcount''): # Python 3+, insert after co_argcount args.insert(1, ocode.co_kwonlyargcount) # Python 3.8 adds co_posonlyargcount, but also has code.replace(), used above ncode = code(*args) # and a new function object using the updated code object return function( ncode, outer.__globals__, outer.__name__, outer.__defaults__, outer.__closure__ )

La función anterior valida que la nueva función interna (que se puede pasar como un objeto de código o como una función) de hecho usará los mismos cierres que el original. Luego crea nuevos códigos y objetos de función para que coincidan con el antiguo objeto de función outer , pero con la función anidada (ubicada por nombre) reemplazada por su parche de mono.

Para demostrar que todo lo anterior funciona, reemplacemos la función innerfunction con una que incremente cada valor formateado en 2:

>>> def create_inner(): ... someformat = None # the actual value doesn''t matter ... def innerfunction(val): ... return someformat.format(val + 2) ... return innerfunction ... >>> new_inner = create_inner()

La nueva función interna también se crea como una función anidada; Esto es importante ya que garantiza que Python utilizará el someformat correcto para buscar el cierre de someformat . Utilicé una declaración return para extraer el objeto de función, pero también podría mirar create_inner.__code__.co_consts para tomar el objeto de código.

Ahora podemos parchear la función externa original, intercambiando solo la función interna:

>>> new_outer = replace_inner_function(outerfunction, new_inner) >>> list(outerfunction(6, 7, 8)) [''Foo: 6'', ''Foo: 7'', ''Foo: 8''] >>> list(new_outer(6, 7, 8)) [''Foo: 8'', ''Foo: 9'', ''Foo: 10'']

La función original hizo eco de los valores originales, pero los nuevos valores devueltos se incrementaron en 2.

Incluso puede crear nuevas funciones internas de reemplazo que usen menos cierres:

>>> def demo_outer(): ... closure1 = ''foo'' ... closure2 = ''bar'' ... def demo_inner(): ... print(closure1, closure2) ... demo_inner() ... >>> def create_demo_inner(): ... closure1 = None ... def demo_inner(): ... print(closure1) ... >>> replace_inner_function(demo_outer, create_demo_inner.__code__.co_consts[1])() foo

Entonces, para completar la imagen:

  1. Cree su función interna de parche de mono como una función anidada con los mismos cierres
  2. Use replace_inner_function() para producir una nueva función externa
  3. Monkey parchea la función externa original para usar la nueva función externa producida en el paso 2.