from gc import commonsense - Finish Him!

../hacking_python_s1/claudiofreire.jpg Autor: Claudio Freire

No sé si todos, pero muchos de los que usamos python (y cualquier lenguaje de alto nivel, en realidad) nos sentimos atraídos por sus elegantes abstracciones. Y no es la menor la que abstrae el manejo de memoria, el garbage colector como se lo conoce.

Este bicho tan ignorado pero reverenciado, dicen las historias, nos permite programar sin preocuparnos por la memoria. No hace falta reservar memoria, no hace falta liberarla... el garbage collector se encarga de eso.

Y, como toda historia, tiene algo de verdad.

En esta tira analizaremos los mitos y verdades del manejo de memoria automático. Mucho de lo que veremos se aplica a muchos lenguajes a la vez - a todos los que utilicen algún tipo de manejo de memoria automático - pero, por supuesto, el foco es sobre el manejo que hace Python. Y no cualquier python, que hay varios.. CPython.

Finalización

Escapando un rato de lo que es reservar y liberar bytes, porque antes de meternos en esos detalles profundos y viscerales de CPython debemos conocer la superficie que los cubren, veamos un tema que tiene repercusiones serias en el manejo de memoria y de recursos en general.

Si el lector ha programado en varios lenguajes orientados a objetos (no sólo python), conocerá de sobremanera los constructores. Pequeñas funcioncitas que, bueno, construyen instancias de objetos de alguna clase particular.

Por ejemplo:

>>> class ClaseInutil:
...     def __init__(self, valor):
...         self.valor = valor
...
>>> objetoInutil = ClaseInutil(3)
>>> objetoInutil.valor
3

El mismo lector también recordará algo menos común en Python: los destructores. Los destructores (así llamados en muchos lenguajes, pero toman otros nombres también) son funciones que se invocan para liberar recursos asociados a la instancia. Por ejemplo, si nuestra ClaseInutil tuviera como valor un archivo, un socket, o algo que necesite ser "cerrado" o "liberado", vamos a querer implementar un destructor que lo haga cuando la instancia deje de existir.

Esto se llama finalización, y en python se escribe:

>>> class ClaseInutil:
...     def __init__(self, archivito):
...         print "abriendo"
...         self.valor = open(archivito, "r")
...     def __del__(self):
...         print "cerrando"
...         self.valor.close()
...
>>> objetoInutil = ClaseInutil("archivito.txt")
abriendo
>>> objetoInutil = None
cerrando

Otro lector dirá Hm... interesante. No lo voy a contradecir.

Y sí, la clase se llama inútil porque los archivos en python ya tienen su destructor, que cierra el archivo. Pero es un ejemplo.

Vida y obra de una clase

Ahora viene la pregunta que hay que hacerse. ¿Cuándo pues deja de existir una instancia? ¿Cuándo se llama a __del__?

En la mayoría de los lenguajes de alto nivel que administran la memoria por nosotros, la definición es muy laxa: en algún momento cuando no haya referencias alcanzables a la instancia.

En esa pequeña frasecita se esconde un mundo de subespecificación. ¿Qué es una referencia alcanzable? ¿Qué momento exactamente? ¿Inmediatamente al volverse inalcanzables las referencias remanentes? ¿Un minuto después? ¿una hora después? ¿Un día después?.

Como la primera pregunta es complicada, la vamos a ver la próxima. Y para la segunda, tercera, cuarta, quinta y sexta pregunta... bueno... no hay respuesta precisa a partir de la especificación del lenguaje. La especificación, aquí, es vaga y a propósito.

La utilidad de subespecificar (no dejar en claro cuándo se finaliza una instancia) es muy grande, créase o no. Si no fuera por esto, Jython no existiría. Para los que no conozcan Jython, es una implementación del lenguaje Python, pero hecha en Java - porque nada dice que hay que implementar Python con C, y porque nada lo impide.

Si la especificación hubiera dicho que los objetos se finalizan inmediatamente después de volverse inalcanzables, una implementación en Java hubiera sido muchísimo más ineficiente, puesto que semejante requerimiento es muy diferente a los requerimientos del garbage collector de Java. Siendo inespecífica, la especificación de Python permite reutilizar el garbage collector de java, cosa que hace a Jython viable.

Y si algún lector programó finalizadores en Java, ya estará notando el problema: Python, como lenguaje, no nos da garantía alguna de cuándo va a ejecutarse nuestro destructor __del__. Sólo que se va a ejecutar. Hoy, mañana, pasado... o cuando se apague la computadora. Da lo mismo a la especificación del lenguaje, cualquiera de esas opciones sería una implementación válida de Python.

En realidad es peor... puesto que Python, en la especificación, también dice que no garantiza que se llame al destructor de los objetos que estén vivos al finalizar el intérprete. O sea, si hago sys.exit(0), los objetos que estén vivos pueden no llamar a su destructor. O sea que tampoco garantiza que vaya a llamarse eventualmente en todos los casos.

Pero CPython, a diferencia de Jython, implementa un tipo de garbage collector que es mucho más inmediato en detectar referencias inalcanzables - al menos en la mayoría de los casos. Esto hace parecer a los destructores de python mágicos, inmediatos, como los destructores de C++. Y es la razón por la que los destructores, en CPython, son diez veces más útiles que, digamos, en Java. O Jython.

Muchos programadores Python, equivocadamente, se apoyan en esta inmediatez inherente a CPython (una implementación entre tantas del lenguaje Python), como si lo fuera a Python (el lenguaje en sí). Lamentablemente me cuento entre ellos. Es muy cómodo, hay que admitir, así que si vamos a apoyarnos en esta comodidad, hagámoslo a conciencia, sabiendo lo que hacemos, y las limitaciones que tiene:

Referencias circulares

Nuestra clase inútil utiliza un destructor para cerrar el archivo... cosa que se considera incorrecta en Python. ¿Por qué?, tanta gente se pregunta.

Veamos:

>>> objetoInutil = ClaseInutil("archivito.txt")
abriendo
>>> objetoInutil2 = ClaseInutil("archivito.txt")
abriendo
>>> objetoInutil.circulito = objetoInutil2
>>> objetoInutil2.circulito = objetoInutil
>>> objetoInutil = objetoInutil2 = None

Ahora, ejercicio interesante para el lector pensar qué saldrá por la consola luego de esa última sentencia. No es poco común equivocarse aquí, y decir: imprime "cerrando" dos veces. No, no lo hace. Adelante, hagan la prueba.

Para entender qué sucede, tipear en la consola import gc ; gc.garbage. Ahí están nuestras dos instancias de ClaseInutil.

¿Qué sucedió? Lo veremos en detalle en otra entrega. Lo importante para recordar es que los destructores se llevan muy mal con las referencias circulares. Y hay muchas, muchas formas de caer en referencias circulares, y no siempre son sencillas de detectar, y siempre son más difíciles aún de evitar. gc.garbage va a ser nuestro amigo siempre que sospechemos de este tipo de problemas.

Resucitando objetos

Las personas no son las únicas que pueden recibir CPR (resucitación cardiopulmonar). Los objetos en python también. Realmente, a mí personalmente, nunca me fue útil para nada. Nada de nada de nada. Pero alguien debe haber pensado que estaba bueno, porque es parte del lenguaje:

Si un destructor de una instancia, en el proceso, crea una referencia alcanzable a sí mismo, la destrucción se cancela, y el objeto vive.

Capaz que puede servir para depuración, o para hacer cosas locas. Imaginemos que un recurso lo tengo que destruir sí o sí en el thread principal (no es descabellado, sucede y no pocas veces). El destructor, pues, podría pedir thread.get_ident() y comparar con el thread principal, si no se está corriendo en el thread correcto, encolar la instancia para ser destruida en el thread principal. Al encolarla, se crea una referencia alcanzable, y CPython va a detectar esto. Es perfectamente legal.

También puede suceder esto por accidente, y es lo más importante para recordar, puesto que dudo que muchos lectores quieran hacerlo a propósito. Es importante, pues, no dejar escapar una referencia a self desde el destructor, o terminaremos con situaciones feas. Goteras de memoria, recursos sin cerrar, excepciones. Cosas feas.

Justamente, veamos un caso del que podremos zafar puesto que Python mismo lo evita:

>>> class ClaseInutil:
...     def __init__(self, archivito):
...         print "abriendo"
...         self.valor = open(archivito, "r")
...     def __del__(self):
...         raise RuntimeError, "quiero romper todo"
...
>>> try:
...    x = ClaseInutil("archivito.txt")
...    # hacer cosas
...    x = None
... except:
...    pass
...
abriendo
Exception RuntimeError: RuntimeError('quiero romper todo',)
      in <bound method ClaseInutil.__del__
      of <__main__.ClaseInutil instance at 0x7f2b2873e4d0>> ignored

Lo divertido del código de arriba, no es que explota. Es obvio, después de todo tiré un RuntimeError muy a propósito. Lo divertido es que no explota.

Uno esperaría que tire RuntimeError, el except debería atrapar la excepción e ignorarla (pero no imprimir que la ignoró). Pero si hiciera eso, la referencia no desaparece, porque al tirar la excepción, se guardó una referencia a self en el objeto Traceback de la excepción. Y al salir del bloque de except intentaría volver a destruirla, lo que tira otra excepción, lo que vuelve a resucitar el objeto... y así. Muy divertido.

Nota: Sucede que todas las excepciones tienen una referencia a las variables locales de donde se produjo la excepción, pues es útil para los depuradores, y eso puede mantener vivas instancias, o resucitarlas.

Así que CPython, muy al tanto de este problema, ignora las excepciones que intentan escapar de un destructor. Si el destructor no atrapa una excepción, no se elevará al código que "llamó" al destructor. Lo cual tiene sentido, porque el código que llamó al destructor lo hizo implícitamente, de casualidad, rara vez sabría atrapar la excepción.

Otra forma de dejar escapar referencias a self que suele pasar desapercibida es al usar closures. Expresiones lambda como lambda x : self.atributo + x, tienen implícita una referencia a self, y si escapa esa expresión lambda también escapa self.

Administradores de contexto

Concluyendo, los destructores son útiles, cómodos, y difíciles de predecir. Hay que usarlos con cuidado, y siempre que asumamos que los destructores se llaman inmediatamente al dereferenciar una instancia, estaremos creando código que sólo funcionará correctamente en CPython.

Para cerrar archivos, Python nos provee de una herramienta más predecible, y con soporte uniforme en todas las implementaciones del lenguaje: la sentencia with:

>>> with open("archivito.txt", "r") as f:
...     # hacer algo
...     # no hace falta llamar a f.close(),
...     # se llama al salir del bloque 'with'

No veremos los detalles de la sentencia with, pero cabe mencionar que no reemplaza a los destructores. Sólo reemplaza al uso que le dimos a los destructores en este artículo, o sea, para cerrar archivos. La sentencia tiene muchos otros usos, los invito a investigarla.

Versión en PDF. | Versión en reSt.

Help PET: Donate

blog comments powered by Disqus

Último cambio: Tue Aug 17 12:25:56 2010.  -  Esta revista está bajo una licencia Creative Commons.