From 64479b4e02ca64fd6bafdbfab6c740ef5c50a6a7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 24 Jul 2016 02:35:38 -0700 Subject: [PATCH] Break reference cycle Line2D <-> Line2D._lineFunc. Upon drawing, Line2D objects would store a reference to one of their own bound methods as their `_lineFunc` argument. This would lead to them being gc'ed not when going out of scope, but only when the "true" gc kicks in; additionally this led to some pickle-related bugs (#3627). One can easily sidestep this problem by not storing this bound method. To check the behavior, try (py3.4+ only): ``` import gc import weakref from matplotlib import pyplot as plt def f(): fig, ax = plt.subplots() img = ax.imshow([[0, 1], [2, 3]]) weakref.finalize(img, print, "gc'ing image") l, = plt.plot([0, 1]) weakref.finalize(l, print, "gc'ing line") fig.canvas.draw() img.remove() l.remove() f() print("we have left the function") gc.collect() print("and cleaned up our mess") ``` Before the patch, the AxesImage is gc'ed when the function exits but the Line2D only upon explicit garbage collection. After the patch, both are collected immediately. --- lib/matplotlib/lines.py | 13 ++----------- lib/matplotlib/tests/test_pickle.py | 14 -------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 1f52371c67f7..e20b79af286c 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -445,12 +445,6 @@ def __init__(self, xdata, ydata, self.set_data(xdata, ydata) - def __getstate__(self): - state = super(Line2D, self).__getstate__() - # _linefunc will be restored on draw time. - state.pop('_lineFunc', None) - return state - def contains(self, mouseevent): """ Test whether the mouse event occurred on the line. The pick @@ -784,7 +778,7 @@ def draw(self, renderer): if funcname != '_draw_nothing': tpath, affine = transf_path.get_transformed_path_and_affine() if len(tpath.vertices): - self._lineFunc = getattr(self, funcname) + line_func = getattr(self, funcname) gc = renderer.new_gc() self._set_gc_clip(gc) @@ -807,7 +801,7 @@ def draw(self, renderer): if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) - self._draw_lines(renderer, gc, tpath, affine.frozen()) + line_func(renderer, gc, tpath, affine.frozen()) gc.restore() if self._marker and self._markersize > 0: @@ -1250,9 +1244,6 @@ def set_dashes(self, seq): else: self.set_linestyle((0, seq)) - def _draw_lines(self, renderer, gc, path, trans): - self._lineFunc(renderer, gc, path, trans) - def _draw_solid(self, renderer, gc, path, trans): gc.set_linestyle('solid') gc.set_dashes(self._dashOffset, self._dashSeq) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 7c86da82ae1c..5727c1316281 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -229,20 +229,6 @@ def test_image(): pickle.dump(fig, BytesIO()) -@cleanup -def test_grid(): - from matplotlib.backends.backend_agg import new_figure_manager - manager = new_figure_manager(1000) - fig = manager.canvas.figure - ax = fig.add_subplot(1, 1, 1) - ax.grid() - # Drawing the grid triggers instance methods to be attached - # to the Line2D object (_lineFunc). - manager.canvas.draw() - - pickle.dump(ax, BytesIO()) - - @cleanup def test_polar(): ax = plt.subplot(111, polar=True)