Estoy luchando con un problema de memoria en Heroku cuando ejecuto una aplicación Django (con gunicorn).
Tengo el siguiente código que toma una imagen cargada por el usuario, elimina todos los datos EXIF y devuelve la imagen lista para cargarla en S3. Esto se usa como un limpiador de datos de formulario y al leer datos base64 en la memoria.
def sanitise_image(img): # img is InMemoryUploadedFile try: image = Image.open(img) except IOError: return None # Move all pixel data into a new PIL image data = list(image.getdata()) image_without_exif = Image.new(image.mode, image.size) image_without_exif.putdata(data) # Create new file with image_without_exif instead of input image. thumb_io = StringIO.StringIO() image_without_exif.save(thumb_io, format=image.format) io_len = thumb_io.len thumb_file = InMemoryUploadedFile(thumb_io, None, strip_tags(img.name), img.content_type, io_len, None) # DEL AND CLOSE EVERYTHING thumb_file.seek(0) img.close() del img thumb_io.close() image_without_exif.close() del image_without_exif image.close() del image return thumb_fileBásicamente tomo un InMemoryUploadedFile y devuelvo uno nuevo con solo los datos de píxeles.
del y close s pueden ser redundantes, pero representan mi intento de solucionar la situación en la que el uso de la memoria de Heroku sigue creciendo y no se libera cada vez que esta función finaliza, incluso permaneciendo durante la noche: 
Ejecutando esto en localhost con Guppy y siguiendo el tutorial, no quedan InMemoryUploadedFile s ni StringIO s ni PIL Image en el montón, lo que me deja desconcertado.
Mi sospecha es que Python no libera la memoria al sistema operativo, como he leído en varios hilos en SO. ¿Alguien ha jugado con InMemoryUploadedFile y puede darme una explicación de por qué esta memoria no se libera?
Cuando no realizo esta desinfección, el problema no ocurre.
¡Muchas gracias!
Creo que el problema es crear el objeto de lista temporal:
data = list(image.getdata())Probar:
image_without_exif.putdata(image.getdata())Por eso creo que ese es el problema:
>>> images = [Image.new('RGBA', (100, 100)) for _ in range(100)]El uso de la memoria de Python aumentó ~4Mb.
>>> get_datas = [image.getdata() for image in images]Sin aumento de memoria.
>>> pixel_lists = [list(image.getdata()) for image in images]El uso de la memoria de Python aumentó en ~ 85 Mb.
Probablemente no quiera convertir getdata() en una lista a menos que necesite los números explícitamente. De los documentos de Pillow:
Tenga en cuenta que el objeto de secuencia devuelto por este método es un tipo de datos PIL interno, que solo admite ciertas operaciones de secuencia. Para convertirlo en una secuencia ordinaria (por ejemplo, para imprimir), use list(im.getdata()).
Eventualmente encontré mi propia respuesta. Muchas gracias a Ryan Tran por indicarme la dirección correcta. list() de hecho causa la fuga.
Usando el método equivalente split() y merge() ( docs ), este es el código actualizado:
with Image.open(img) as image: comp = image.split() image_without_exif = Image.merge(image.mode, comp) thumb_io = StringIO.StringIO() image_without_exif.save(thumb_io, format=image.format) io_len = thumb_io.len clean_img = InMemoryUploadedFile(thumb_io, None, strip_tags(img.name), img.content_type, io_len, None) clean_img.seek(0) return clean_img