alertando sobre muitas figuras abertas

166

Em um script no qual crio muitas figuras fix, ax = plt.subplots(...), recebo o aviso RuntimeWarning: Mais de 20 figuras foram abertas. As figuras criadas através da interface pyplot ( matplotlib.pyplot.figure) são mantidas até serem fechadas explicitamente e podem consumir muita memória.

No entanto, não entendo por que recebo esse aviso, porque depois de salvar a figura com fig.savefig(...), a apago com fig.clear(); del fig. Em nenhum momento do meu código, tenho mais de uma figura aberta por vez. Ainda assim, recebo o aviso sobre muitas figuras em aberto. O que isso significa / como posso evitar o aviso?

andreas-h
fonte
9
Se você está fazendo muito disso e não está exibindo nada de forma interativa, é melhor ignorar pltcompletamente. Por exemplo stackoverflow.com/a/16337909/325565 (Não para ligar uma das minhas próprias respostas, mas é o que eu poderia encontrar mais rápido ...)
Joe Kington
1
@JoeKington obrigado esta é uma solução melhor
hihell
A resposta de Joe Kington deve estar na lista principal de respostas. Ele funciona e também soluciona o problema de plt.close () tornar o programa mais lento que Don Kirby mencionou.
NatalieL 10/04

Respostas:

198

Use .clfou .clano seu objeto de figura em vez de criar uma nova figura. De @DavidZwicker

Supondo que você importou pyplotcomo

import matplotlib.pyplot as plt

plt.cla()limpa um eixo , ou seja, o eixo atualmente ativo na figura atual. Deixa os outros eixos intocados.

plt.clf()limpa a figura atual inteira com todos os seus eixos, mas deixa a janela aberta, para que possa ser reutilizada em outras parcelas.

plt.close()fecha uma janela , que será a janela atual, se não for especificado de outra forma. plt.close('all')fechará todas as figuras abertas.

A razão que del fignão funciona é que a pyplotmáquina de estado mantém uma referência à figura (como deve saber se é a 'figura atual'). Isso significa que, mesmo que você exclua sua referência para a figura, há pelo menos uma referência ativa e, portanto, ela nunca será coletada como lixo.

Como estou pesquisando a sabedoria coletiva aqui para esta resposta, @JoeKington menciona nos comentários que plt.close(fig)removerão uma instância de figura específica da máquina de estado do pylab ( plt._pylab_helpers.Gcf ) e permitirão que seja coletada como lixo.

Hooked
fonte
1
Mhh. Existe clfpara a figureclasse, mas não close. Por que, del figna verdade, não fecha e exclui a figura?
mail andreas-h
2
@ andreas-h Meu palpite: para algo complexo como um gerenciador de janelas com seus próprios manipuladores, pode haver mais limpeza do que colocar algo fora do escopo. Você está certo que closenão funcionará no objeto figura, chame-o como plt.close(), em vez de fig.clf().
21714
5
@ andreas-h - Basicamente, a razão pela qual del fig não funciona é que, ao fornecer um __del__método (que seria basicamente chamado plt.close(fig)), acabaria causando referências circulares nesse caso específico, e figter um __del__método fará com que outras coisas não sejam coletadas de lixo . (Ou essa é a minha vaga lembrança, pelo menos.) De qualquer forma, é certamente um pouco chato, mas você deve ligar em plt.close(fig)vez de del fig. Em uma nota lateral, matplotlib poderia realmente usar um gerenciador de contexto para isso ...
Joe Kington
6
@Hooked - Para deixar um pouco mais claro, edite sua pergunta para mencionar que plt.close(fig)removerá uma instância de figura específica da máquina de estado do pylab ( plt._pylab_helpers.Gcf) e permitirá que ela seja coletada no lixo.
21814 Joe Kington
2
@JoeKington plté um pouco confuso e há pensamentos de como refazer um monte disso. O gerenciador de contexto é intrigante ... Consulte github.com/matplotlib/matplotlib/pull/2736 , github.com/matplotlib/matplotlib/pull/2624
tacaswell:
32

Aqui está um pouco mais detalhadamente para expandir a resposta de Hooked . Quando li a resposta pela primeira vez, perdi a instrução para ligar em clf() vez de criar uma nova figura . clf()por si só não ajuda se você for criar outra figura.

Aqui está um exemplo trivial que causa o aviso:

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 o aviso, tenho que puxar a chamada para subplots()fora do loop. Para continuar vendo os retângulos, preciso mudar clf()para cla(). Isso limpa o eixo sem remover o próprio eixo.

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()

Se você estiver gerando plotagens em lotes, poderá usar ambos cla()e close(). Encontrei um problema em que um lote podia ter mais de 20 parcelas sem reclamar, mas ele reclamava após 20 lotes. Corrigi isso usando cla()depois de cada plot e close()depois 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()

Eu medi o desempenho para ver se valia a pena reutilizar a figura dentro de um lote, e esse pequeno programa de amostra diminuiu de 41s para 49s (20% mais lento) quando acabei de ligar para close()cada plotagem.

Don Kirkby
fonte
Esta é uma ótima resposta. A resposta aceita realmente não resolve o problema real em questão, que é o consumo de memória.
Kyle
24

Se você pretende manter conscientemente muitas plotagens na memória, mas não deseja ser avisado sobre isso, pode atualizar suas opções antes de gerar números.

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

Isso impedirá que o aviso seja emitido sem alterar nada sobre o modo como a memória é gerenciada.

mightypile
fonte
em um ambiente Jupyter, a alocação de memória persiste enquanto existir a célula que mostra o gráfico?
matanster
2
@matanster, eu publicaria isso como sua própria pergunta. Comecei a responder e depois percebi que realmente não sabia o suficiente sobre o gerenciamento de kernels do jupyter para responder honestamente.
Mightypile 26/08/18
@matanster Todas as variáveis ​​e memória alocadas a elas existem até que o kernel seja encerrado explicitamente pelo usuário. Não está vinculado às células. No Jupyter Hub mais recente, o sistema pode desligar os kernels (ele pode ser configurado).
greatvovan
0

O seguinte snippet resolveu o problema para mim:


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(...
# .....

Quando _wrapped_figuresai do escopo, o tempo de execução chama nosso __del__()método com plt.close()inside. Isso acontece mesmo que a exceção seja acionada após o _wrapped_figureconstrutor.

Dmitry
fonte
0

Isso também é útil se você deseja suprimir temporariamente o aviso:

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