10

If I create an Axes object in matplotlib and mutate it (i.e. by plotting some data) and then I call a function without passing my Axes object to that function then that function can still mutate my Axes. For example:

import matplotlib.pyplot as plt
import numpy as np

def innocent_looking_function():
    #let's draw a red line on some unsuspecting Axes!
    plt.plot(100*np.random.rand(20), color='r')

fig, ax = plt.subplots()
ax.plot(100*np.random.rand(20), color='b') #draw blue line on ax
#ax now has a blue line, as expected

innocent_looking_function()
#ax now unexpectedly has a blue line and a red line!

My question is: can I prevent this global-variable behaviour in general? I know I can call plt.close() before calling any innocent_looking_function() but is there some way to make this the default?

nicolaskruchten
  • 26,384
  • 8
  • 83
  • 101
  • But does that really make sense? `plt.plot` just uses the currently active axis and plots. If you want to avoid a third party code to plot to your figure, you can e.g. close it. Using `sca` to unset your created axis as the `currently active` axis should work as well. In my opinion a default behavior that forces `plt.plot` to close all open plots is probably hard to motivate :) – cel Feb 20 '15 at 15:05
  • From an object-oriented/information-hiding/scoping mindset, it absolutely makes sense: I created an object and I don't want other code to mutate it without my knowledge. – nicolaskruchten Feb 20 '15 at 15:08
  • 1
    I totally understand you concerns here, but I think python code is a little different from other OO-Languages, like e.g. Java/C#. In python information-hiding is a convention between caller and callee. E.g. there are no private member attributes as in many other languages but a convention that attributes starting with an underscore should be treated as `private`. But this is not enforced. I think your example is a similar situation. I would consider unconditionally calling `plt.plot` bad style, but not forbidden in any way. – cel Feb 20 '15 at 15:25
  • As I see it today, `matplotlib` has made terrible design decisions with respect to global variables (which is not particularly pythonic), and I'm trying to understand if there is a way to mitigate the damage possible. – nicolaskruchten Feb 20 '15 at 15:36
  • I think the problem of `matplotlib` is, that it tried to mimic matlab functionality. I wouldn't call it `terrible design decisions`, but of course some things seem rather messy. – cel Feb 20 '15 at 15:39
  • 1
    Fine. This isn't really helping me answer my question, you're basically telling me I shouldn't worry about it :) – nicolaskruchten Feb 20 '15 at 15:41
  • 1
    No, I am saying: I don't think there is an easy, straightforward solution to this. But of course there are legitimate reasons where this can be a problem. But for these cases there is probably a simple workaround: If you make sure, that `plt.gca()` does not return your axis then other functions should not be able to manipulate your plot anymore. And there is `sca()`... – cel Feb 20 '15 at 15:47
  • 1
    https://github.com/matplotlib/matplotlib/pull/2624 <- dormant PR starting to pull this apart. – tacaswell Feb 20 '15 at 18:19
  • **I'm absolutely with nicolaskruchten on this.** The `pyplot`-centric Matplotlib API is an unmitigated disaster on rails for applications attempting to integrate Matplotlib, particularly in a non-blocking manner. While it's great that a modicum of effort is being directed towards unwinding this ill-designed non-architecture, I'm not necessarily holding my breath on such a core rewrite. – Cecil Curry Jan 18 '16 at 02:00

1 Answers1

20

Sure! What you need to do is bypass the pyplot state machine entirely when you make your figure.

It's more verbose, as you can't just call fig = plt.figure().


First off, let me explain how plt.gca() or plt.gcf() works. When using the pyplot interface, matplotlib stores all created-but-not-displayed figure managers. Figure managers are basically the gui wrapper for a figure.

plt._pylab_helpers.Gcf is the singleton object that stores the figure managers and keeps track of which one is currently active. plt.gcf() returns the active figure from _pylab_helpers.Gcf. Each Figure object keeps track of it's own axes, so plt.gca() is just plt.gcf().gca().

Normally, when you call plt.figure(), it:

  1. Creates the figure object that's returned
  2. Creates a FigureManager for that figure using the appropriate backend
  3. The figure manager creates a FigureCanvas, gui window (as needed), and NavigationToolbar2 (zoom buttons, etc)
  4. The figure manager instance is then added to _pylab_helpers.Gcf's list of figures.

It's this last step that we want to bypass.


Here's a quick example using a non-interactive backend. Note that because we're not worried about interacting with the plot, we can skip the entire figure manager and just create a Figure and FigureCanvas instance. (Technically we could skip the FigureCanvas, but it will be needed as soon as we want to save the plot to an image, etc.)

import matplotlib.backends.backend_agg as backend
from matplotlib.figure import Figure

# The pylab figure manager will be bypassed in this instance. `plt.gca()`
# can't access the axes created here.
fig = Figure()
canvas = backend.FigureCanvas(fig)
ax = fig.add_subplot(111)

Just to prove that gca can't see this axes:

import matplotlib.pyplot as plt
import matplotlib.backends.backend_agg as backend
from matplotlib.figure import Figure

# Independent figure/axes
fig = Figure()
canvas = backend.FigureCanvas(fig)
ax = fig.add_subplot(111)
ax.plot(range(10))

# gca() is completely unaware of this axes and will create a new one instead:
ax2 = plt.gca()
print 'Same axes?:', id(ax) == id(ax2)

# And `plt.show()` would show the blank axes of `ax2`

With an interactive backed, it's a touch more complicated. You can't call plt.show(), so you need to start the gui's mainloop yourself. You can do it all "from scratch" (see any of the "embedding matplotlib" examples), but the FigureManager abstracts the backed-specific parts away:

As an example using the TkAgg backend:

import matplotlib.backends.backend_tkagg as backend
from matplotlib.figure import Figure

fig = Figure()
ax = fig.add_subplot(111)

manager = backend.new_figure_manager_given_figure(1, fig)
manager.show()
backend.show.mainloop()

To use one of the other backends, just change the backend import. For example, for Qt4:

import matplotlib.backends.backend_qt4agg as backend
from matplotlib.figure import Figure

fig = Figure()
ax = fig.add_subplot(111)

manager = backend.new_figure_manager_given_figure(1, fig)
manager.show()
backend.show.mainloop()

This actually even works with the nbagg backend used in IPython notebooks. Just change the backend import to import matplotlib.backends.backend_nbagg as backend

Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • Excellent! Is there any way I can use this approach in something like IPython Notebook? How would I display `fig`? – nicolaskruchten Feb 20 '15 at 16:28
  • It's a touch more complicated and backend-specific to use interactive figures. I'm working on those examples now. I think it might not be possible with an ipython notebook, though. (I almost never use notebooks, so I'm not terribly familiar with them.) I'm assuming IPython uses pyplot's figure manager to find the figure you create, and I don't know offhand how to tell IPython to use a figure manager that's been manually created. – Joe Kington Feb 20 '15 at 16:31
  • 1
    @nicolaskruchten - Ignore that part about it not working with IPython notebooks. It actually works exactly the same as the other backends! – Joe Kington Feb 20 '15 at 17:24
  • Thanks @JoeKington! I was in the process of starting to type this up, got distracted and came back to find it done! – tacaswell Feb 20 '15 at 18:14