¿Se comstackn las funciones internas de Python?

De modo que AFAIK en CPython, las definiciones de funciones se comstackn en objetos de funciones cuando se ejecutan en tiempo de análisis. Pero ¿qué pasa con las funciones internas? ¿Se comstackn en objetos de función en tiempo de análisis o se comstackn (o interpretan) cada vez que se llama a la función? ¿Las funciones internas incurren en alguna penalización de rendimiento?

Para dar una explicación general, asumiendo que tiene el siguiente código en un módulo:

 def outer(x=1): def inner(y=2): return x+y 

Cuando Python analiza el archivo a través de compile() , el texto anterior se convierte en un código de bytes para saber cómo ejecutar el módulo . En el bytecode del módulo, hay dos “objetos de código”, uno para el bytecode of outer() y otro para el bytecode inner() . Tenga en cuenta que dije objetos de código, no funciones; los objetos de código contienen poco más que el código de bytes utilizado por la función, y cualquier información que pueda conocerse en el momento de la comstackción, como el código de bytes para la etiqueta outer() contiene una referencia al código de bytes de inner() .

Cuando el módulo realmente se carga, al evaluar el objeto de código asociado con el módulo, una cosa que sucede es que se crea un “objeto de función” real para outer() y se almacena en el atributo outer del módulo. El objeto de función actúa como una colección del código de byte y todas las cosas relacionadas con el contexto que se necesitan para llamar a la función (por ejemplo, de qué globales lo deben extraer, etc.) que no se pueden conocer en el momento de la comstackción. En cierto modo, un objeto de código es una plantilla para una función, que es una plantilla para la ejecución del código de bytes real con todas las variables completadas.

Nada de esto involucró a la función inner() como una función aún: cada vez que realmente llama a la función outer() , es cuando se crea un nuevo objeto de función inner() para esa invocación de externa , que enlaza con el ya creado. objeto de bytecode interno a una lista de elementos globales, incluido el valor de x que se transfiere a esa llamada a external. Como puede imaginar, esto es bastante rápido, ya que no se necesita un análisis, simplemente rellenando una estructura rápida con algunos punteros a otros objetos ya existentes.

Prueba fácil: los argumentos predeterminados de una función se llaman una vez, en el momento definido.

 >>> def foo(): ... def bar(arg=count()): ... pass ... pass ... >>> def count(): ... print "defined" ... >>> foo() defined >>> foo() defined 

Así que sí: este es un hit de rendimiento menor (muy, muy pequeño).

 >>> import dis >>> def foo(): ... def bar(): ... print "stuff" ... return bar ... >>> b = foo() >>> dis.dis(foo) 2 0 LOAD_CONST 1 (", line 2>) 3 MAKE_FUNCTION 0 6 STORE_FAST 0 (bar) 4 9 LOAD_FAST 0 (bar) 12 RETURN_VALUE >>> dis.dis(b) 3 0 LOAD_CONST 1 ('stuff') 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE 

Sospecho que esto depende en gran medida de la implementación, pero eso fue CPython 2.6.6, y la función interna parece que fue comstackda. Aquí hay otro ejemplo:

 >>> def foo(): ... def bar(): ... return 1 ... return dis.dis(bar) ... >>> foo() 3 0 LOAD_CONST 1 (1) 3 RETURN_VALUE 

Entonces podemos concluir que están comstackdos. En cuanto a sus características de rendimiento, utilízalos. Si empiezas a tener problemas de rendimiento, perfil. Sé que no es realmente una respuesta, pero casi nunca importa y cuando lo hace, las respuestas generales no lo cortan. Las llamadas a funciones incurren en cierta sobrecarga y parece que las funciones internas son simplemente funciones.

Para extender la función interna de respuesta de nmichaels, se comstackn en tiempo de comstackción como él adivinó y allí el código de byte se guarda en foo.func_code.co_consts y se accede a ellos usando el código de operación LOAD_CONST como se puede ver en el desassembly de la función.

Ejemplo:

 >>> def foo(): ... def inner(): ... pass >>> print foo.func_code.co_consts (None, ", line 2>) 

Llego tarde a esto, pero como un pequeño complemento experimental de estas respuestas exhaustivas: puede usar la función incorporada id para verificar si se crea un nuevo objeto o no:

 In []: # inner version def foo(): def bar(): return id(bar) return bar() foo(), foo() Out[]: (4352951432, 4352952752) 

Los números reales pueden diferir, pero su diferencia indica que se han creado dos instancias distintas de bar .

 In []: # outer version def bar(): return id(bar) def foo(): return bar() foo(), foo() Out[]: (4352950952, 4352950952) 

Esta vez, como se esperaba, los dos id son los mismos.

Ahora por algunas mediciones de tiempo. Primero interno, segundo externo

 100000 loops, best of 3: 1.93 µs per loop 1000000 loops, best of 3: 1.25 µs per loop 

Entonces, en mi máquina, parece que la versión interna es 50% más lenta (Python 2.7, IPython Notebook).