如何在Matplotlib绘图中删除线条

102

如何删除 matplotlib 图中的一条或多条线,并使其实际上被垃圾回收并释放内存?下面的代码似乎可以删除该线,但是即使显式调用 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.

那么,有没有一种方法仅从一个axes中删除一行并释放内存?这个潜在的解决方案也不起作用。

6个回答

93

这是我为我的一位同事准备的非常详细的解释。我认为它在这里也会很有帮助。不过请耐心等待,我会在最后讲到你遇到的真正问题。只作为预告,它涉及额外引用了你的Line2D对象。

注意:在我们深入讨论之前还有一个要点。如果您正在使用IPython进行测试,则IPython会保留自己的引用,其中并非所有引用都是弱引用。因此,在IPython中测试垃圾回收无效。它只会让事情更加混乱。

好的,让我们开始吧。每个matplotlib对象(FigureAxes等)都通过各种属性提供对其子艺术家的访问。以下示例变得相当长,但应该很有启示性。

我们首先创建一个Figure对象,然后将一个Axes对象添加到该图形中。请注意,axfig.axes [0]是同一个对象(相同的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

这也适用于坐标轴对象中的线条:

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

如果您按照上面的方法调用plt.show(),您将会看到一个包含一组轴和一条线的图形:

包含一组轴和一条线的图形

现在,虽然我们已经看到了linesax.lines的内容是相同的,但非常重要的是要注意,由lines变量引用的对象与由ax.lines引用的对象不同,如下所示:

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

因此,从 lines 中删除元素不会对当前绘图产生影响,但是从 ax.lines 中删除元素将从当前绘图中删除该线。 因此:

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

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

因此,如果您运行第二行代码,您将从当前图中删除包含在ax.lines[0]中的Line2D对象,并且它将不存在。请注意,这也可以通过ax.lines.remove()完成,这意味着您可以将Line2D实例保存在变量中,然后将其传递给ax.lines.remove()以删除该线条,就像这样:

>>> #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>]

一张包含坐标轴和两条线的图

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

一张包含坐标轴和仅第二条线的图

对于fig.axes,所有上述内容同样适用于ax.lines

现在,真正的问题是:如果我们将ax.lines[0]中包含的引用存储到一个weakref.ref对象中,然后尝试删除它,我们会发现它不会被垃圾回收:

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

引用仍然有效!为什么呢?这是因为还有另一个引用指向wr中的Line2D对象。还记得linesax.lines没有相同的ID但包含相同的元素吗?这就是问题所在。

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

因此,这个故事想要告诉我们的道理是,要自己清洗干净。如果你期望某个东西被垃圾回收,但它没有被回收,那么很可能是你留下了一些未处理的引用。


2
正是我所需要的。我正在绘制成千上万张地图,每张地图都有一个散点图覆盖在世界地图投影上。它们每个需要3秒钟!通过重复使用已经绘制好地图的图形,并从ax.collections中弹出结果集合,我将其缩短到了1/3秒。谢谢! - GaryBishop
4
我认为在当前版本的mpl中,这已不再必要。艺术家有一个remove()函数,可以将它们从mpl清除,然后您只需要跟踪您的引用即可。 - tacaswell
2
哎呀,你知道这个变化是在哪个版本的matplotlib中出现的吗? - Vorticity
在使用matplotlib动画时,我发现这个很有用,特别是当你需要使用大量的图表时。否则,你会发现内存占用非常大。现在要做的是让这个东西更快。 - Danny Staple

78

我展示了一种结合使用 lines.pop(0) l.remove()del l 的方法来解决问题。

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)

我检查了你的大型数据集,并在系统监视器上确认了内存释放。

当不进行故障排除时,更简单的方法是从列表中弹出它并在不创建硬引用的情况下调用remove函数来移除这个对象:

lines.pop(0).remove()

我运行了你的代码,结果如下: [8:37pm]@flattop:~/Desktop/sandbox>python delete_lines.py <weakref at 0x8dd348c; to 'Line2D' at 0x8dd43ec> <weakref at 0x8dd348c; to 'Line2D' at 0x8dd43ec> <weakref at 0x8dd348c; to 'Line2D' at 0x8dd43ec> 我在Ubuntu 10.04中使用matplotlib版本0.99.1.1。 - David Morton
2
@David Morton 我刚刚降级到了0.99.1,现在我也能够重现你的问题。我想我只能建议你升级到1.0.1版本。自从0.99.x版本以来,已经修复了很多bug。 - Paul
1
这里的问题很可能是引用停留在不应该存在的地方。我敢打赌,OP正在使用IPython测试一些东西。请参见我的答案。 - Vorticity
我认为该行为已经更新。在Matplotlib 3.5.3中,当从ax.lines中弹出或查找并调用l.remove()后,该行将被删除。在从列表中弹出后尝试调用remove()会引发ValueError: list.remove(x): x not in list异常。 - SpaceMonkey55

18
我尝试了很多不同的答案和不同的论坛。我猜它取决于您正在开发的机器。但是我已经使用过这个语句。
ax.lines = []

并且完美地运行。我不使用cla(),因为它会删除我对图表所做的所有定义。

例如:

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

但我已经尝试删除这些行多次了。在删除时,我还使用了weakref库来检查对该行的引用,但对我来说都没有用。

希望这对其他人有用 =D


这里的问题很可能是引用在不应存在的情况下挂起了。我打赌OP正在使用IPython测试一些东西。请查看我的答案。 - Vorticity
在 matplotlib 3.5.3 中无法设置属性。 - SpaceMonkey55

10

希望这能帮到其他人:以上示例使用了ax.lines。 在更新的mpl(3.3.1)中,有ax.get_lines()。 这避免了调用ax.lines=[]的需要。

for line in ax.get_lines(): # ax.lines:
    line.remove()
# ax.lines=[] # needed to complete removal when using ax.lines

3
这应该在列表中更靠前,因为其他答案已经过时且令人困惑。 - eric

6
(使用上面那个人的例子)
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()

0

你可以按照以下方式从轴中删除具有特定索引的任何行:

import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(1, 2, 1)

axes = fig.axes  # = [ax1, ax2]

# add two lines to ax1
ax1.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40], lw=2, color='k', label='2 Hz')
ax1.plot([0, 1, 2, 3, 4], [15, 6, 25, 8, 45], lw=2, color='r', label='4 Hz')

# remove one line from ax1
ax1.lines[0].remove()  # remove the first line in ax1
# axes[0].lines[0].remove()  # equivalent to the line above


你可以选择调用方法 fig.get_axes() 来获取第五行的结果。 - bactone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,