Jak usunąć linie z wykresu Matplotlib

85

Jak mogę usunąć linię (lub linie) osi matplotlib w taki sposób, aby faktycznie zbierał śmieci i zwalniał pamięć? Poniższy kod wydaje się usuwać wiersz, ale nigdy nie zwalnia pamięci (nawet w przypadku jawnych wywołań gc.collect())

from matplotlib import pyplot
import numpy
a = numpy.arange(int(1e7))
# large so you can easily see the memory footprint on the system monitor.
fig = pyplot.Figure()
ax  = pyplot.add_subplot(1, 1, 1)
lines = ax.plot(a) # this uses up an additional 230 Mb of memory.
# can I get the memory back?
l = lines[0]
l.remove()
del l
del lines
# not releasing memory
ax.cla() # this does release the memory, but also wipes out all other lines.

Czy jest więc sposób, aby po prostu usunąć jedną linię z osi i odzyskać pamięć? To potencjalne rozwiązanie również nie działa.

David Morton
źródło

Odpowiedzi:

71

Pokazuję, że połączenie lines.pop(0) l.remove()i del lzałatwia sprawę.

from matplotlib import pyplot
import numpy, weakref
a = numpy.arange(int(1e3))
fig = pyplot.Figure()
ax  = fig.add_subplot(1, 1, 1)
lines = ax.plot(a)

l = lines.pop(0)
wl = weakref.ref(l)  # create a weak reference to see if references still exist
#                      to this object
print wl  # not dead
l.remove()
print wl  # not dead
del l
print wl  # dead  (remove either of the steps above and this is still live)

Sprawdziłem twój duży zbiór danych i zwolnienie pamięci jest również potwierdzone na monitorze systemu.

Oczywiście prostszym sposobem (bez rozwiązywania problemów) byłoby wyjęcie go z listy i wywołanie removeobiektu liniowego bez tworzenia do niego trwałego odniesienia:

lines.pop(0).remove()
Paweł
źródło
Uruchomiłem twój kod i otrzymałem: [20:37] @flattop: ~ / Desktop / sandbox> python delete_lines.py <slabyref at 0x8dd348c; do „Line2D” pod adresem 0x8dd43ec> <slabyref pod adresem 0x8dd348c; do „Line2D” pod adresem 0x8dd43ec> <slabyref pod adresem 0x8dd348c; do „Line2D” pod adresem 0x8dd43ec> Używam wersji Matplotlib 0.99.1.1 w systemie ubuntu 10.04
David Morton
1
@David Morton Właśnie obniżyłem wersję do 0.99.1 i teraz odtwarzam twój problem. Myślę, że mogę tylko polecić aktualizację do 1.0.1. Było wiele poprawek błędów od 0.99.x
Paul
1
Problem w tym miejscu jest prawdopodobnie problemem kręcenia się odniesień, gdy nie powinny. Założę się, że OP używa IPythona do testowania rzeczy. Zobacz moją odpowiedź.
Wirowość
67

To bardzo długie wyjaśnienie, które napisałem dla mojego współpracownika. Myślę, że również tutaj byłoby pomocne. Bądź jednak cierpliwy. Dochodzę do prawdziwego problemu, który masz pod koniec. Podobnie jak zwiastun, jest to kwestia dodatkowych odniesień do Line2Dkręcących się wokół obiektów.

OSTRZEŻENIE: Jeszcze jedna uwaga, zanim zaczniemy. Jeśli używasz IPythona do testowania tego, IPython zachowuje własne odwołania i nie wszystkie z nich są slabymi ref. Tak więc testowanie czyszczenia pamięci w IPythonie nie działa. Po prostu wprowadza zamieszanie.

Dobra, zaczynamy. Każdy matplotlibobiekt ( Figure, Axesitp) zapewnia dostęp do swoich dziecięcych artystów poprzez różne atrybuty. Poniższy przykład robi się dość długi, ale powinien być pouczający.

Zaczynamy od stworzenia Figureobiektu, a następnie dodajemy Axesobiekt do tej figury. Zauważ, że axi fig.axes[0]to ten sam obiekt (taki sam id()).

>>> #Create a figure
>>> fig = plt.figure()
>>> fig.axes
[]

>>> #Add an axes object
>>> ax = fig.add_subplot(1,1,1)

>>> #The object in ax is the same as the object in fig.axes[0], which is 
>>> #   a list of axes objects attached to fig 
>>> print ax
Axes(0.125,0.1;0.775x0.8)
>>> print fig.axes[0]
Axes(0.125,0.1;0.775x0.8)  #Same as "print ax"
>>> id(ax), id(fig.axes[0])
(212603664, 212603664) #Same ids => same objects

Dotyczy to również linii w obiekcie osi:

>>> #Add a line to ax
>>> lines = ax.plot(np.arange(1000))

>>> #Lines and ax.lines contain the same line2D instances 
>>> print lines
[<matplotlib.lines.Line2D object at 0xce84bd0>]
>>> print ax.lines
[<matplotlib.lines.Line2D object at 0xce84bd0>]

>>> print lines[0]
Line2D(_line0)
>>> print ax.lines[0]
Line2D(_line0)

>>> #Same ID => same object
>>> id(lines[0]), id(ax.lines[0])
(216550352, 216550352)

Gdybyś miał wywołać plt.show()to, co zostało zrobione powyżej, zobaczyłbyś figurę zawierającą zestaw osi i jedną linię:

Figura zawierająca zestaw osi i jedną linię

Teraz, chociaż widzieliśmy, że zawartość linesi ax.linesjest taka sama, bardzo ważne jest, aby zauważyć, że obiekt, do którego odwołuje się lineszmienna, nie jest tym samym, co obiekt, do którego odwołuje się obiekt, do którego odwołuje się obiekt, ax.linesco można zobaczyć w następujący sposób:

>>> id(lines), id(ax.lines)
(212754584, 211335288)

W konsekwencji usunięcie elementu z linesnic nie robi dla bieżącego wykresu, ale usunięcie elementu z ax.linespowoduje usunięcie tej linii z bieżącego wykresu. Więc:

>>> #THIS DOES NOTHING:
>>> lines.pop(0)

>>> #THIS REMOVES THE FIRST LINE:
>>> ax.lines.pop(0)

Tak więc, jeśli miałbyś uruchomić drugą linię kodu, usunąłbyś Line2Dobiekt zawarty w ax.lines[0]bieżącym wykresie i zniknąłby. Zauważ, że można to również zrobić poprzez ax.lines.remove()to, że możesz zapisać Line2Dinstancję w zmiennej, a następnie przekazać ją, ax.lines.remove()aby usunąć tę linię, na przykład:

>>> #Create a new line
>>> lines.append(ax.plot(np.arange(1000)/2.0))
>>> ax.lines
[<matplotlib.lines.Line2D object at 0xce84bd0>,  <matplotlib.lines.Line2D object at 0xce84dx3>]

Figura zawierająca zestaw osi i dwie linie

>>> #Remove that new line
>>> ax.lines.remove(lines[0])
>>> ax.lines
[<matplotlib.lines.Line2D object at 0xce84dx3>]

Figura zawierająca zestaw osi i tylko drugą linię

Wszystko to działa fig.axestak samo dobrze, jak działaax.lines

Teraz prawdziwy problem. Jeśli będziemy przechowywać odniesienie zawarte w ax.lines[0]do weakref.refobiektu, a następnie próbuje go usunąć, to zauważymy, że nie zostanie śmieci zbierane:

>>> #Create weak reference to Line2D object
>>> from weakref import ref
>>> wr = ref(ax.lines[0])
>>> print wr
<weakref at 0xb758af8; to 'Line2D' at 0xb757fd0>
>>> print wr()
<matplotlib.lines.Line2D at 0xb757fd0>

>>> #Delete the line from the axes
>>> ax.lines.remove(wr())
>>> ax.lines
[]

>>> #Test weakref again
>>> print wr
<weakref at 0xb758af8; to 'Line2D' at 0xb757fd0>
>>> print wr()
<matplotlib.lines.Line2D at 0xb757fd0>

Odniesienie jest nadal aktywne! Czemu? Dzieje się tak, ponieważ istnieje jeszcze inne odniesienie do Line2Dobiektu, na które wskazuje odniesienie wr. Pamiętasz, jak linesnie miał tego samego identyfikatora co, ax.linesale zawierał te same elementy? Cóż, w tym problem.

>>> #Print out lines
>>> print lines
[<matplotlib.lines.Line2D object at 0xce84bd0>,  <matplotlib.lines.Line2D object at 0xce84dx3>]

To fix this problem, we simply need to delete `lines`, empty it, or let it go out of scope.

>>> #Reinitialize lines to empty list
>>> lines = []
>>> print lines
[]
>>> print wr
<weakref at 0xb758af8; dead>

Więc morał tej historii jest taki, posprzątaj po sobie. Jeśli spodziewasz się, że coś zostanie zebrane jako śmieci, ale tak nie jest, prawdopodobnie zostawiasz gdzieś jakieś odniesienie.

Wirowość
źródło
2
Dokładnie to, czego potrzebowałem. Planuję tysiące map, z których każda zawiera wykres punktowy nad rzutem mapy świata. Każdy z nich zajmował 3 sekundy! Używając ponownie figury z już narysowaną mapą i usuwając wynikową kolekcję z ax.collections, zmniejszyłem ją do 1/3 sekundy. Dzięki!
GaryBishop
4
Myślę, że nie jest to już konieczne w obecnych wersjach mpl. Artysta ma remove()funkcję, która oczyści je z mpl strony rzeczy, a wtedy musisz tylko śledzić swoje odniesienia.
tacaswell
2
Hm, masz pojęcie, w której wersji matplotlib ta zmiana jest taka sama?
Wirowość
Okazało się, że jest to przydatne podczas korzystania z wielu działek w animacji matplotlib. W przeciwnym razie będziesz miał bardzo dużą ilość używanej pamięci. Teraz, żeby to przyspieszyć.
Danny Staple
14

Wypróbowałem wiele różnych odpowiedzi na różnych forach. Myślę, że to zależy od maszyny, na której pracujesz. Ale użyłem tego stwierdzenia

ax.lines = []

i działa idealnie. Nie używam, cla()ponieważ usuwa wszystkie definicje, które wprowadziłem do wykresu

Dawny.

pylab.setp(_self.ax.get_yticklabels(), fontsize=8)

ale wielokrotnie próbowałem usunąć te wiersze. Używam również biblioteki slabref, aby sprawdzić odwołanie do tej linii podczas usuwania, ale nic nie działało.

Mam nadzieję, że to zadziała dla kogoś innego = D

Jeronimo Schreyer
źródło
Problem w tym miejscu jest prawdopodobnie problemem kręcenia się odniesień, gdy nie powinny. Założę się, że OP używa IPythona do testowania rzeczy. Zobacz moją odpowiedź.
Wirowość
5

(używając tego samego przykładu co facet powyżej)

from matplotlib import pyplot
import numpy
a = numpy.arange(int(1e3))
fig = pyplot.Figure()
ax  = fig.add_subplot(1, 1, 1)
lines = ax.plot(a)

for i, line in enumerate(ax.lines):
    ax.lines.pop(i)
    line.remove()
Jeronimo
źródło
1

Mam nadzieję, że może to pomóc innym: powyższe przykłady używają ax.lines. W nowszym mpl (3.3.1) jest ax.get_lines(). To omija potrzebę dzwonieniaax.lines=[]

for line in ax.get_lines(): # ax.lines:
    line.remove()
# ax.lines=[] # needed to complete removal when using ax.lines
brobr
źródło