sacar - medir tiempo de ejecucion en python
¿Por qué las declaraciones de rendimiento de Python forman un cierre? (3)
Añadiendo a la respuesta de @sepp2k está viendo estos dos comportamientos diferentes porque las funciones lambda
que se están creando no saben de dónde tienen que obtener el valor de i
. En el momento en que se crea esta función, todo lo que sabe es que tiene que obtener el valor del alcance local, del alcance cerrado, del alcance global o de las construcciones internas.
En este caso particular, es una variable de cierre (alcance cerrado). Y su valor está cambiando con cada iteración.
Echa un vistazo a LEGB en Python .
Ahora, ¿por qué el segundo funciona como se esperaba, pero no el primero?
Es porque cada vez que cedes una función lambda
, la ejecución de la función del generador se detiene en ese momento y cuando la invocas y usará el valor de i
en ese momento. Pero en el primer caso ya hemos avanzado su valor a 9 antes de invocar cualquiera de las funciones.
Para probarlo, puede obtener el valor actual de i
del contenido de la celda de __closure__
:
>>> for func in test_with_yield():
print "Current value of i is {}".format(func.__closure__[0].cell_contents)
print func(9)
...
Current value of i is 0
Current value of i is 1
Current value of i is 2
Current value of i is 3
Current value of i is 4
Current value of i is 5
Current value of i is 6
...
Pero, en cambio, si almacena las funciones en algún lugar y las llama más tarde, verá el mismo comportamiento que la primera vez:
from itertools import islice
funcs = []
for func in islice(test_with_yield(), 4):
print "Current value of i is {}".format(func.__closure__[0].cell_contents)
funcs.append(func)
print ''-'' * 20
for func in funcs:
print "Now value of i is {}".format(func.__closure__[0].cell_contents)
Salida:
Current value of i is 0
Current value of i is 1
Current value of i is 2
Current value of i is 3
--------------------
Now value of i is 3
Now value of i is 3
Now value of i is 3
Now value of i is 3
El ejemplo utilizado por Patrick Haugh en los comentarios también muestra lo mismo: sum(t(1) for t in list(test_with_yield()))
Forma correcta:
Asigne i
como valor predeterminado a lambda
, los valores predeterminados se calculan cuando se crea la función y no cambian (a menos que sea un objeto mutable ). Ahora soy una variable local para las funciones lambda
.
>>> def test_without_closure():
return [lambda x, i=i: x+i for i in range(10)]
...
>>> sum(t(1) for t in test_without_closure())
55
Tengo dos funciones que devuelven una lista de funciones. Las funciones toman un número x
y lo agregan a él. i
es un número entero que aumenta de 0 a 9.
def test_without_closure():
return [lambda x: x+i for i in range(10)]
def test_with_yield():
for i in range(10):
yield lambda x: x+i
Esperaría que test_without_closure
devolviera una lista de 10 funciones, cada una de las cuales agrega 9
a x
ya que su valor es 9
.
print sum(t(1) for t in test_without_closure()) # prints 100
test_with_yield
que test_with_yield
también tuviera el mismo comportamiento, pero crea correctamente las 10 funciones.
print sum(t(1) for t in test_with_yield()) # print 55
Mi pregunta es, ¿el rendimiento forma un cierre en Python?
El rendimiento no crea un cierre en Python, las lambdas crean un cierre. La razón por la que obtienes todos los 9 en "test_without_closure" no es porque no haya cierre. Si no hubiera, no podrías acceder a i
en absoluto. El problema es que todos los cierres contienen una referencia¹ a la misma variable i, que será 9 al final de la función.
Esta situación no es muy diferente en test_with_yield
. ¿Por qué, entonces, obtienes resultados diferentes? Debido a que el yield
suspende el funcionamiento de la función, entonces es posible usar las lambdas producidas antes de que se llegue al final de la función, es decir, antes de que i
sea 9. Para ver lo que esto significa, considere los siguientes dos ejemplos de usar test_with_yield
:
[f(0) for f in test_with_yield()]
# Result: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[f(0) for f in list(test_with_yield())]
# Result: [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
Lo que sucede aquí es que el primer ejemplo produce un lambda (mientras que yo es 0), lo llama (i sigue siendo 0), luego avanza la función hasta que se produce otra lambda (i ahora es 1), llama a la lambda, y así sucesivamente . Lo importante es que cada lambda se test_with_yield
antes de que el flujo de control vuelva a test_with_yield
(es decir, antes de que cambie el valor de i).
En el segundo ejemplo, primero creamos una lista. Entonces la primera lambda es cedida (i es 0) y puesta en la lista, la segunda lambda es creada (i es ahora 1) y puesta en la lista ... hasta que la última lambda es cedida (ahora es 9) y puesta en la lista. Y luego comenzamos a llamar a las lambdas. Entonces, como ahora tengo 9, todas las lambdas devuelven 9.
¹ Lo importante aquí es que los cierres contienen referencias a variables, no copias del valor que tenían cuando se creó el cierre. De esta manera, si asignas a la variable dentro de una lambda (o función interna, que crea cierres de la misma manera que las lambdas), esto también cambiará la variable fuera de la lambda y si cambias el valor afuera, ese cambio será visible dentro de la lambda.
No, ceder no tiene nada que ver con los cierres.
Aquí es cómo reconocer cierres en Python: un cierre es
Una función
en el que se realiza una búsqueda de nombre no calificado
no existe un enlace del nombre en la función misma
pero existe una vinculación del nombre en el ámbito local de una función cuya definición rodea la definición de la función en la que se busca el nombre.
La razón de la diferencia de comportamiento que observa es la pereza, en lugar de tener que ver con cierres. Compara y contrasta lo siguiente
def lazy():
return ( lambda x: x+i for i in range(10) )
def immediate():
return [ lambda x: x+i for i in range(10) ]
def also_lazy():
for i in range(10):
yield lambda x:x+i
not_lazy_any_more = list(also_lazy())
print( [ f(10) for f in lazy() ] ) # 10 -> 19
print( [ f(10) for f in immediate() ] ) # all 19
print( [ f(10) for f in also_lazy() ] ) # 10 -> 19
print( [ f(10) for f in not_lazy_any_more ] ) # all 19
Observe que los ejemplos primero y tercero dan resultados idénticos, al igual que el segundo y el cuarto. El primero y el tercero son flojos, el segundo y el cuarto no.
Tenga en cuenta que los cuatro ejemplos proporcionan un grupo de cierres sobre el enlace más reciente de i
, es solo que en el primer y tercer caso usted evalúa los cierres antes de volver a enlazar i
(incluso antes de haber creado el siguiente cierre en la secuencia), mientras en el segundo y cuarto caso, primero espere hasta que haya rebotado a 9 (después de haber creado y recopilado todos los cierres que va a realizar), y solo luego evalúe los cierres.