Liberación de recursos en python

Pongámonos en contexto: estamos usando Python, un lenguaje no mucho más viejo que Java, y donde una de las primeras lecciones que se aprenden es que la memoria es gestionada automáticamente.

Hurrah! Ya no hay que hacer mallocs, ni deletes, ni tener destructores, ni hostias en vinagre,  porque python lo hace todo!

 

¿Entonces, por qué nos topamos a menudo con código como éste?:
f = open("foo.txt", "r")
text = f.readlines()
f.close()        # WAT.
O bien esto?:
with open("foo.txt", "r") as f: # WAT.
    text = f.readlines()

Y no hablo solo de ficheros, sino cualquier tipo de recurso: conexiones de red, puertos hardware, primitivas de sincronización, memoría RAM, etc.

(nota: el texto a continuación es básicamente un copiapega de un mail  enviado a una lista de correo privada. lo digo por si algo no encajara…)

Vamos a meternos en detalles (al menos hasta donde yo conozco; y no dudeis en corregirme si veis algún fallo, así aprendemos todos):

No es necesario hacer un close:

Es cierto, no es estrictamente necesario hacer un close, porque por defecto el lenguaje python se encarga de esas tareas mundanas por nosotros. El problema es que lo hace automáticamente, y a su manera, y puede que no sea la que nos interese.

De ahí que se suela hacer un close() explícito (ya sea con una llamada directa, o mediante el “with” que he comentado antes).

¿Por qué podría no interesarnos lo que hace python por defecto?
Puede ser por muchos motivos:

La vida de las variables en python es un detalle de implementación:

Por ejemplo, el intérprete que solemos usar (CPython) mantiene los objetos en memoria como mínimo hasta que salen de ámbito, y como máximo hasta cuando el GC de CPython lo determina en alguna de sus pasadas (cuando existen ciclos de referencias, y según el perfil de uso de memoria de nuestro programa).

En cristiano: que una variable que se sale de ámbito, podría eliminarse al momento, o podría tardar media hora en ser eliminada por el GC. Imaginad que las conexiones a una misma cámara IP solo se cierran cada diez minutos (porque hemos delegado todo al GC), y que esa cámara acepta como máximo 4 conexiones simultáneas…

(otras implementaciones como PyPy, Jython, IronPython, etc. pueden comportarse distinto)

En .NET ocurre lo mismo, como bien dice David (de ahí viene el jaleo de usar iDisposables incluso para recursos managed…), y en Java más de lo mismo.

Bugs de terceras partes:

Alguna vez hemos sufrido bugs de conteo de referencias, por alguna librería de python escrita en C que no actualizaba correctamente los conteos y provocaba leaks de nuestros propios objetos.

Si esos objetos tenian recursos abiertos, hay que cerrarlos a mano, o sino seguirán abiertos hasta que muera nuestro proceso de python.

Bugs propios:

Incluso asumiendo una gestión de memoria perfecta e instantánea por parte de CPython (que no es el caso necesariamente, como he explicado), existen casos en que nuestro código puede estar manteniendo variables en memoria (y con recursos abiertos) sin que nos demos cuenta.

Algunos casos típicos:

Variables de instancia:

def __init__(self):
    self.my_file = open(...)
    print self.my_file.readline()
    # el recurso permanecerá abierto al menos
    # hasta que el objeto self sea eliminado

Variables dentro de una función de larga duración:

my_file = open(...);
print my_file.readline()
while True:
    # bucle principal del programa
    # my_file sigue con el recurso abierto al
    # menos hasta el fin de la función

Variables que por su naturaleza son compartidas:

El ejemplo más claro, un mutex. También semáforos, bufferes de memoria compartidos por varios hilos, etc.

Variables referenciadas por closures:

Aunque salgamos de una función, los closures alargan la vida de las variables que cogen prestadas de su función padre:
def my_function():
    my_file = open("foobar")
    def internal_function():
        return my_file.readline()
    return internal_function
my_closure = my_function()
# my_file sigue existiendo y con el archivo
# abierto, hasta que se destruya my_closure

Ámbito de función, no de bloque:

Es algo básico de python, pero no está mal repasarlo: en python no vale definir una variable dentro de un bloque (por ej. dentro de un for), porque su ámbito es siempre de función:
for path in ["/a.txt", "b.txt"]:
    my_file = fopen(path...)
    print my_file.readline()
print path    # va a imprimir "b.txt", aunque
              # estemos fuera del bucle.
print my_file # lo mismo pasa con my_file, aunque
              # pueda ser anti-intuitivo
del my_file   # si queremos que se elimine esta
              # referencia a la variable, y el GC
              # pueda hacer su trabajo en algún
              # momento indeterminado
while True:
    #bucle principal de duración infinita
Bueno, creo que así queda una explicación más completa (y espero que también sea técnicamente correcta!) 🙂

Tags: [es] | October 24th, 2012 |

Leave a Reply