Skip to content

artist.axes and .figure should never be reassignable #6793

Closed

Description

@anntzer

#3835 ensured that the .axes attribute of an artist cannot be assigned to a new Axes when it is already set to an earlier Axes. However, it does not prevent one from doing so by going via None. Also, nothing is done regarding reassignment of .figure.

In practice there's plenty of code that assumes these invariants (see #6785 for how I ended up here), so I'd suggest tightening the restrictions to

  • .axes can only ever be set to a non-None value once. Upon removal of the artist from an axes, the private attribute ._discarded is set to prevent further reuse. (If there is truly need for it, we could add a .copy() method that would return, well, a copy of the artist, but with ._discarded unset, so that it can be reassigned to a new Axes.)
  • .figure follows the same rules, and is additionally automatically set/unset whenever .axes is set/unset.

Thoughts?

Activity

  1. jenshnielsen commented on Jul 18, 2016

    @jenshnielsen
    Member

    It sounds sensible to me but I might me missing some subtle issues

  2. added this to the milestone on Jul 20, 2016
  3. tacaswell commented on Jul 20, 2016

    @tacaswell
    Member

    I have a vague memory of there being a good reason to allow this behavior (it may have been scaffolding for allowing (careful) moving of aritsts between axes in the future).

    There is some controls on figure via set_figure.

    Artists can be created without being assigned to an axes or figure (and axes can exist without a figure) so those attributes being valid objects is not a strict invariant.

    Just removing the clearing of axes/figure in the default remove should be enough? I do not fully follow the use case for _discarded.

  4. anntzer commented on Jul 20, 2016

    @anntzer
    ContributorAuthor

    I guess you could get away with not clearing .axes/.figure and not using ._discarded in remove() but then you'd need a separate way to check whether an artist is actually part of an axes/figure or not. Something like replacing all calls to artist.axes is not None to artist in artist.axes.<the proper container> (cf. #3944 too). (So in practice I think having an extra bit of state would just avoid a bunch of subtle errors.)

    I know that artists can be created without being linked to an axes or figure; I simply suggest that once they are attached to one, they cannot be reassigned to another (you suggest that there may be a use case for such a reassignment?). WRT axes existing independently of figure, this could probably be solved with some pattern like (untested)

    @property
    def axes(self):
        return self._axes
    
    @axes.setter
    def axes(self, value):
        if self._axes is None:
            if value is None: # no change
                pass
            elif self._discarded: # cannot reuse
                raise ValueError
            else: # initializing
                self._axes = value
        else: # has already been set
            if value is None: # discarding
                self._discarded = True
                self._value = None
            elif value is self._axes: # resetting to same value
                pass
            else: # resetting to other value
                raise ValueError
    
    @property
    def figure(self):
        if self._figure is not None:
            return self._figure
        elif self._axes is not None:
            return self._axes.figure
        else:
            return None
    
    # figure.setter similar to axes.setter, but also checks whether self.axes has been set
    
  5. tacaswell commented on Jul 20, 2016

    @tacaswell
    Member

    I am nominally 👍 on this, but doesn't that implementation still have the problem that the axes/figure can still become None at some point in the lifecycle?

  6. anntzer commented on Jul 20, 2016

    @anntzer
    ContributorAuthor

    Indeed, the setters for figure and axes should also set the "other" attribute to None when discarding the artist, and that should be enough, right? Or am I missing something?

  7. modified the milestones: , on Oct 3, 2017
  8. modified the milestones: needs sorting, v3.1 on Aug 30, 2018
  9. modified the milestones: v3.1.0, v3.2.0 on Feb 25, 2019
  10. sparky-corona commented on Feb 28, 2019

    @sparky-corona

    Since this Problem is still not closed for me in anaconda2 / python 2.7 / matplotlib 2.2.3

    I tried another workaround:

    My Problem: I'm creating a lot of plots in a loop. In every element of this loop I call a self defined
    function SavePlot which gets handed over the name of the png to be produced, a handle of the figure element. By using matplotlib.use('agg') and matplotlib.pyplot as plt I do use multiprocessing.Pool.map to call the handler.savefig function which gets the filename as argument of the p.map routine.

    def SavePlot( savepath, handler, title, fmt):
        gc.enable()
        filename = title
        for char in ' -:/;':
            filename = filename.replace(char, '')
        filename = savepath + filename + fmt
        print( 'saving {}\n'.format( filename))
        taskspercpu = psutil.cpu_count( logical = True)
        agents = psutil.cpu_count( logical = False) - 1
        p = Pool( processes = agents, maxtasksperchild = taskspercpu)
        res = p.map( handler.savefig, ( filename,))
        p.close()
        p.join()
        #handler.savefig( filename, dpi=200)
        print( 'Deleting object from handler\n\t{}:'.format( handler))
        for ax in handler.axes:
            for line in ax.lines:
                #del line
                line.remove()
            ax.remove()
        for el in dir( handler):
            print( '\n\t{} deleted...'.format( el))
            del el
    
        print( 'CLOSING {} after saving with handler {}'.format( filename, handler))
        for i in plt.get_fignums():
            plt.close(i)
        
        inloopvarlist = list( locals().iteritems())
    
        print( 'Deleting object from locals():\n\t{}:'.format( title))
        for f in inloopvarlist:
            for k in f:
                print( '\n\t{} deleting...'.format( k))
                del k
            print( '\n\t{} deleting...'.format( f))
            del f
        
        plt.clf()
        plt.close()
        
        
        gc.collect(2)
        del gc.garbage[:]
        return

    There nearly NO memory leak anymore since the savefigìs now handed over to a PID which is closed by p.close() and p.join().

    Any ideas why there are problems with this savefig?

    greets

    [ @tacaswell edited to fix markup ]

  11. added a commit that references this issue on Dec 4, 2022
    30aec46
  12. added a commit that references this issue on Dec 15, 2022
    9a25c5f
  13. added a commit that references this issue on Mar 31, 2023
    3cab932
  14. github-actions commented on Jan 29, 2024

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  15. anntzer commented on Jan 30, 2024

    @anntzer
    ContributorAuthor

    Let's not bother for now and assume that if someone "goes through None" to reassign axes/figure, they know what they're doing.

  16. added a commit that references this issue on Mar 13, 2024
    bbe4b95
  17. added a commit that references this issue on May 22, 2026
    7c895b6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: inactiveMarked by the “Stale” Github Action

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions