advertencia sobre demasiadas figuras abiertas

166

En un script donde creo muchas figuras fix, ax = plt.subplots(...), recibo la advertencia RuntimeWarning: se han abierto más de 20 figuras. Las figuras creadas a través de la interfaz pyplot ( matplotlib.pyplot.figure) se conservan hasta que se cierren explícitamente y pueden consumir demasiada memoria.

Sin embargo, no entiendo por qué recibo esta advertencia, porque después de guardar la figura con fig.savefig(...), la elimino con fig.clear(); del fig. En ningún punto de mi código, tengo más de una figura abierta a la vez. Aún así, recibo la advertencia sobre demasiadas figuras abiertas. ¿Qué significa eso / cómo puedo evitar recibir la advertencia?

andreas-h
fuente
9
Si está haciendo mucho de esto y no muestra nada de manera interactiva, es mejor que evite por pltcompleto. Por ejemplo stackoverflow.com/a/16337909/325565 (No para conectar una de mis propias respuestas, pero es la que pude encontrar más rápido ...)
Joe Kington
1
@ JoeKington gracias, esta es una mejor solución
hihell
La respuesta de Joe Kington debería estar en la lista de respuestas principal. Funciona y también soluciona el problema con plt.close () ralentizando el programa que Don Kirby mencionó.
NatalieL

Respuestas:

199

Use .clfo .claen su objeto de figura en lugar de crear una nueva figura. De @DavidZwicker

Suponiendo que haya importado pyplotcomo

import matplotlib.pyplot as plt

plt.cla()borra un eje , es decir, el eje actualmente activo en la figura actual. Deja los otros ejes intactos.

plt.clf()borra toda la figura actual con todos sus ejes, pero deja la ventana abierta, de modo que puede reutilizarse para otros trazados.

plt.close()cierra una ventana , que será la ventana actual, si no se especifica lo contrario. plt.close('all')cerrará todas las figuras abiertas.

La razón por la que del figno funciona es que la pyplotmáquina de estado mantiene una referencia a la figura (como debe ser si va a saber cuál es la 'figura actual'). Esto significa que incluso si elimina su referencia a la figura, hay al menos una referencia en vivo, por lo tanto, nunca se recolectará basura.

Dado que estoy investigando la sabiduría colectiva aquí para esta respuesta, @JoeKington menciona en los comentarios que plt.close(fig)eliminarán una instancia de figura específica de la máquina de estado pylab ( plt._pylab_helpers.Gcf ) y permitirá que se recolecte basura.

Enganchado
fuente
1
Mhh Hay clfpara la figureclase, pero no close. ¿Por qué en del figrealidad no cierra y elimina la figura?
andreas-h
2
@ andreas-h Mi conjetura: para algo complejo como un administrador de ventanas con sus propios controladores, es posible que se necesite más limpieza que poner algo fuera de alcance. Su derecho que closeno funcionará en el objeto de la figura, llámelo como plt.close(), en lugar de fig.clf().
Enganchado
55
@ andreas-h - Básicamente, la razón por la que del figno funciona es que darle un __del__método (que básicamente llamaría plt.close(fig)) terminaría causando referencias circulares en este caso particular, y figtener un __del__método hará que otras cosas no se recojan basura . (O ese es mi vago recuerdo, de todos modos.) En cualquier caso, sin duda es un poco molesto, pero debería llamar en plt.close(fig)lugar de del fig. En una nota al margen, matplotlib realmente podría usar un administrador de contexto para esto ...
Joe Kington
66
@Hooked: para que quede un poco más claro, puede editar su pregunta para mencionar que plt.close(fig)eliminará una instancia de figura específica de la máquina de estado de pylab ( plt._pylab_helpers.Gcf) y permitirá que se recolecte basura.
Joe Kington
2
@JoeKington pltes un poco desordenado y hay pensamientos sobre cómo volver a hacer un montón de cosas. El administrador de contexto es intrigante ... Ver github.com/matplotlib/matplotlib/pull/2736 , github.com/matplotlib/matplotlib/pull/2624
tacaswell
33

Aquí hay un poco más de detalle para ampliar la respuesta de Hooked . Cuando leí esa respuesta por primera vez, me perdí las instrucciones para llamar en clf() lugar de crear una nueva figura . clf()por sí solo no ayuda si luego vas y creas otra figura.

Aquí hay un ejemplo trivial que causa la advertencia:

from matplotlib import pyplot as plt, patches
import os


def main():
    path = 'figures'
    for i in range(21):
        _fig, ax = plt.subplots()
        x = range(3*i)
        y = [n*n for n in x]
        ax.add_patch(patches.Rectangle(xy=(i, 1), width=i, height=10))
        plt.step(x, y, linewidth=2, where='mid')
        figname = 'fig_{}.png'.format(i)
        dest = os.path.join(path, figname)
        plt.savefig(dest)  # write image to file
        plt.clf()
    print('Done.')

main()

Para evitar la advertencia, tengo que tirar de la llamada subplots()fuera del bucle. Para seguir viendo los rectángulos, necesito cambiar clf()a cla(). Eso despeja el eje sin quitar el eje mismo.

from matplotlib import pyplot as plt, patches
import os


def main():
    path = 'figures'
    _fig, ax = plt.subplots()
    for i in range(21):
        x = range(3*i)
        y = [n*n for n in x]
        ax.add_patch(patches.Rectangle(xy=(i, 1), width=i, height=10))
        plt.step(x, y, linewidth=2, where='mid')
        figname = 'fig_{}.png'.format(i)
        dest = os.path.join(path, figname)
        plt.savefig(dest)  # write image to file
        plt.cla()
    print('Done.')

main()

Si está generando gráficos en lotes, es posible que tenga que usar ambos cla()y close(). Me encontré con un problema en el que un lote podía tener más de 20 parcelas sin quejarse, pero se quejaría después de 20 lotes. Lo arreglé usando cla()después de cada parcela y close()después de cada lote.

from matplotlib import pyplot as plt, patches
import os


def main():
    for i in range(21):
        print('Batch {}'.format(i))
        make_plots('figures')
    print('Done.')


def make_plots(path):
    fig, ax = plt.subplots()
    for i in range(21):
        x = range(3 * i)
        y = [n * n for n in x]
        ax.add_patch(patches.Rectangle(xy=(i, 1), width=i, height=10))
        plt.step(x, y, linewidth=2, where='mid')
        figname = 'fig_{}.png'.format(i)
        dest = os.path.join(path, figname)
        plt.savefig(dest)  # write image to file
        plt.cla()
    plt.close(fig)


main()

Medí el rendimiento para ver si valía la pena reutilizar la cifra dentro de un lote, y este pequeño programa de muestra disminuyó de 41 a 49 (20% más lento) cuando llamé close()después de cada trama.

Don Kirkby
fuente
Esta es una respuesta genial. La respuesta aceptada no aborda realmente el problema actual, que es el consumo de memoria.
Kyle
24

Si tiene la intención de mantener a sabiendas muchas parcelas en la memoria, pero no desea que se le advierta al respecto, puede actualizar sus opciones antes de generar cifras.

import matplotlib.pyplot as plt
plt.rcParams.update({'figure.max_open_warning': 0})

Esto evitará que se emita la advertencia sin cambiar nada sobre la forma en que se gestiona la memoria.

mightypile
fuente
en un entorno de Jupyter, ¿persiste la asignación de memoria mientras exista la celda que muestra el gráfico?
matanster
2
@matanster, publicaría eso como una pregunta propia. Comencé a responder, luego me di cuenta de que realmente no sé lo suficiente sobre la administración de núcleos de Jupyter para responder honestamente.
mightypile
@matanster Todas las variables y la memoria asignada para ellas existen hasta que el usuario cierre explícitamente el núcleo. No está vinculado a las células. En el nuevo Jupyter Hub, el sistema puede apagar los núcleos (se puede configurar).
greatvovan
0

El siguiente fragmento resolvió el problema para mí:


class FigureWrapper(object):
    '''Frees underlying figure when it goes out of scope. 
    '''

    def __init__(self, figure):
        self._figure = figure

    def __del__(self):
        plt.close(self._figure)
        print("Figure removed")


# .....
    f, ax = plt.subplots(1, figsize=(20, 20))
    _wrapped_figure = FigureWrapper(f)

    ax.plot(...
    plt.savefig(...
# .....

Cuando _wrapped_figuresale del alcance, el tiempo de ejecución llama a nuestro __del__()método con plt.close()inside. Sucede incluso si la excepción se dispara después del _wrapped_figureconstructor.

Dmitry
fuente
0

Esto también es útil si solo desea suprimir temporalmente la advertencia:

    import matplotlib.pyplot as plt
       
    with plt.rc_context(rc={'figure.max_open_warning': 0}):
        lots_of_plots()
rwb
fuente