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:
-
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
ybar
, 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. -
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:
- Cree su función interna de parche de mono como una función anidada con los mismos cierres
-
Use
replace_inner_function()
para producir una nueva función externa - Monkey parchea la función externa original para usar la nueva función externa producida en el paso 2.