working with what the generators following expressions python generator python-internals

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.