python - with - generadores de pitón recolección de basura
what is a python generator (2)
Apoyo la respuesta de @abarnert, pero como ya escribí esto ...
Sí, el comportamiento en su primer ejemplo es un artefacto del conteo de referencia de CPython
. Cuando se sale del bucle, la countdown(10)
anónima del objeto generador-iterador countdown(10)
devuelto pierde su última referencia y, por lo tanto, se recolecta la basura de una vez. Eso a su vez activa el generador de finally:
suite.
En su segundo ejemplo, el generador-iterador permanece vinculado a c
hasta que sale su main()
, por lo que CPython
sabe que puede reanudar c
en cualquier momento. No es "basura" hasta que main()
sale. Un compilador más sofisticado podría notar que c
nunca se hace referencia después de que finalice el bucle, y decidir efectivamente del c
antes de eso, pero CPython
no intenta predecir el futuro. Todos los nombres locales permanecen vinculados hasta que usted se desvincula explícitamente, o el ámbito en el que terminan son locales.
Creo que mi pregunta está relacionada con this , pero no exactamente similar. Considere este código:
def countdown(n):
try:
while n > 0:
yield n
n -= 1
finally:
print(''In the finally block'')
def main():
for n in countdown(10):
if n == 5:
break
print(''Counting... '', n)
print(''Finished counting'')
main()
La salida de este código es:
Counting... 10
Counting... 9
Counting... 8
Counting... 7
Counting... 6
In the finally block
Finished counting
¿Se garantiza que la línea "En el bloque final" se imprimirá antes de "Recuento finalizado"? ¿O es esto debido a los detalles de implementación de cPython que un objeto se recolectará como basura cuando el recuento de referencia llegue a 0?
¿También tengo curiosidad sobre cómo finally
se ejecuta el bloque del generador de countdown
? Por ejemplo, si cambio el código de main
a
def main():
c = countdown(10)
for n in c:
if n == 5:
break
print(''Counting... '', n)
print(''Finished counting'')
Luego veo el Finished counting
impreso antes In the finally block
. ¿Cómo va el recolector de basura directamente al bloque finally
? Creo que siempre he try/except/finally
su valor nominal, pero pensar en el contexto de los generadores me hace pensar dos veces al respecto.
Usted está, como esperaba, confiando en el comportamiento específico de la implementación del recuento de referencias CPython. 1
De hecho, si ejecuta este código en, digamos, PyPy, la salida generalmente será:
Counting... 10
Counting... 9
Counting... 8
Counting... 7
Counting... 6
Finished counting
In the finally block
Y si lo ejecuta en una sesión de PyPy interactiva, esa última línea puede aparecer varias líneas más tarde, o incluso solo cuando finalmente salga.
Si nos fijamos en cómo se implementan los generadores, tienen métodos más o menos así:
def __del__(self):
self.close()
def close(self):
try:
self.raise(GeneratorExit)
except GeneratorExit:
pass
CPython elimina los objetos inmediatamente cuando el recuento de referencias se convierte en cero (también tiene un recolector de basura para dividir las referencias cíclicas, pero eso no es relevante aquí). Tan pronto como el generador queda fuera del alcance, se elimina, por lo que se cierra, por lo que genera un GeneratorExit
en el marco del generador y lo reanuda. Y, por supuesto, no hay un controlador para el GeneratorExit
, por lo que la cláusula finally
se ejecuta y el control pasa a la pila, donde se ingiere la excepción.
En PyPy, que utiliza un recolector de basura híbrido, el generador no se elimina hasta la próxima vez que el GC decida escanear. Y en una sesión interactiva, con poca presión de memoria, eso podría ser tan tarde como el tiempo de salida. Pero una vez que lo hace, sucede lo mismo.
Puedes ver esto manejando el GeneratorExit
explícitamente:
def countdown(n):
try:
while n > 0:
yield n
n -= 1
except GeneratorExit:
print(''Exit!'')
raise
finally:
print(''In the finally block'')
(Si deja el raise
, obtendrá los mismos resultados por razones ligeramente diferentes).
Puede close
explícitamente un generador y, a diferencia de lo que se muestra arriba, esto es parte de la interfaz pública del tipo de generador:
def main():
c = countdown(10)
for n in c:
if n == 5:
break
print(''Counting... '', n)
c.close()
print(''Finished counting'')
O, por supuesto, puede usar una sentencia with
:
def main():
with contextlib.closing(countdown(10)) as c:
for n in c:
if n == 5:
break
print(''Counting... '', n)
print(''Finished counting'')
1. Como señala la respuesta de Tim Peters , también está confiando en el comportamiento específico de la implementación del compilador CPython en la segunda prueba.