-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
ENH: Switch to a private, simpler AxesStack. #7377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
If you want a simpler PR at first, you can make one that deprecates the public AxesStack and "just" creates the private one. We can go from there in simplifing the API. I'd wait on having the deprecation of the AxesStack vetted by others before moving forward. I think make AxesStack private is a good idea: it is only used in the figure object right now, in a private attribute, and probably aimed at being private from the start. |
3be290e
to
d48d6cd
Compare
lib/matplotlib/figure.py
Outdated
@@ -157,6 +158,60 @@ def __contains__(self, a): | |||
return a in self.as_list() | |||
|
|||
|
|||
class _AxesStack: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs to inherit from (object)
(for now).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
👍 in principle but we should try to support as much of the old API as possible. I know I have written SO answers which reached in and touched the AxesStack.... The balance against making everything private is that if you do not give users enough public knobs to get done what they need to get done, they will find the private knobs and just do it. Once enough user are using a private API it become defacto public and we are as bad off or worse than we would have been other wise. |
9f227e3
to
acd4bb2
Compare
@NelleV Hopefully everything should be fixed now (I didn't realize that I need to keep track both of the axes stack and of the order in which the axes were created). @tacaswell The only reference to AxesStack I could find on SO is http://stackoverflow.com/questions/24104990/how-to-get-a-list-of-axes-for-a-figure-in-pyplot which somehow ironically ends with your comment
In any case this PR literally doesn't change any public API -- the old AxesStack class is still there, it's just used nowhere :-) (and 1. the AxesStack object instantiated by the Figure class was a private attribute, so it's fair game to change it and 2. in fact, the new private AxesStack implements (nearly) the same API as the old one...). Honestly if we start having to worry about modifying private APIs then I don't know what's left to do other than adding features over features and never fixing old stuff because, well, it may always break someone else who is playing with the internals (that person should have asked for a public API, or known better). Again, I'm not removing AxesStack for now; if someone complains that they were actually using it, we can always consider undeprecating it. |
7eb590d
to
a288d93
Compare
The code looks good to me. |
lib/matplotlib/figure.py
Outdated
return next((ax for _, k, ax in self._items if k == key), None) | ||
|
||
def current_key_axes(self): | ||
"""Return the topmost key, axes pair, or `None, None` if empty. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
topmost? Is that a word I don't know?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should add some parentheses because right now it can read as either "a key" or "an axes pair" or None, None
, but it's really "(key, axes) pair" or None, None
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point, fixed
a288d93
to
265626f
Compare
Also related to #7335. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a silly question, but is the only difference between this and an OrderedDict
that it can do operations based on the value instead of the key? A comment/doc to that effect would be useful for the future.
lib/matplotlib/figure.py
Outdated
return key, ax | ||
|
||
def add(self, key, ax): | ||
"""Append a key, axes pair, unless the axes are already present. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add parentheses on this one too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
def as_list(self): | ||
"""Copy of the list of axes, in the order of insertion. | ||
""" | ||
return [ax for _, _, ax in sorted(self._items)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's originally sorted this way, but I wonder if that's even necessary...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "keep creation order" behavior is mandated by the tests right now, and I don't see a reason to get rid of it.
4c0ce14
to
1143621
Compare
@QuLogic also because keys may not be hashable. Added a comment to that effect. |
@anntzer It as always fun when 2.5 year old comments come back at me ;) |
This PR clearly supersedes #7335, thought I think the latter is 2.0 safe while this one isn't. |
def get(self, key): | ||
"""Find the axes corresponding to a key; defaults to `None`. | ||
""" | ||
return next((ax for _, k, ax in self._items if k == key), None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anntzer I consistently learn new python details from your code.
""" | ||
# Skipping existing Axes is needed to support calling `add_axes` with | ||
# an already existing Axes. | ||
if not any(a == ax for _, _, a in self._items): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably needs to handle the duplicate key case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each call to add
is actually preceded by a call to get
, so technically we can rely on the caller code to never try to add in a duplicate key. Also I worded the docstring carefully: the pair is not appended if the axes is already present but there's no check on whether the key is :-)
In practice I could make the behavior (no checking for duplicate key) clearer in the docstring, or add a check and error out if the key is already there; either is fine with me. Please let me know your preference.
👍 One small concern about the duplicate key, other wise LGTM I strongly prefer to not backport this for 2.0. |
We should never have duplicate keys, so what should be the expected behaviour? Raise an error? |
Lets go with raise (and keep the raise / API change in #7335). |
Hmm, so I just thought of something that the key function actually handles that isn't here, namely, that elements of rect = np.array([0.2, 0.2, 0.2, 0.2])
ax1 = fig.add_axes(rect, ...)
rect[:2] += 0.2
ax2 = fig.add_axes(rect, ...)
|
In fact I just realized there's a much bigger issue here: you can't use == to compare arrays for full equality, so something as simple as
will fail with
I guess there's no way around having some variant of |
do we need to support mutable/non-hashable keys? |
On 2016/11/11 8:23 AM, Antony Lee wrote:
I suspect we all agree on this. I think it is the result of starting |
If you're willing to break backcompatibility here, the good thing is that's there's an obvious deprecation path:
By the way I assume(?) it is possible to change the rectangle where an axes sit, in which case that provides another way to break the current implementation (create an axes, move it somewhere else, and either try to create another axes where the first one was, or try to create another axes where the first one is currently). In fact even MATLAB doesn't provide that behavior:
|
return self._axstack.as_list() | ||
|
||
axes = property(fget=_get_axes, doc="Read-only: list of axes in Figure") | ||
axes = property(lambda self: self._axstack.as_list(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this be
@property
def axes(self):
"""Read-only: list of axes in Figure"""
return self._axstack.as_list()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think one is more readable that the other?
Is there any interest in pushing this forward? |
f21c036
to
9b7bfdc
Compare
Why not :-) |
Current coverage is 62.01% (diff: 97.05%)@@ master #7377 diff @@
==========================================
Files 174 174
Lines 56065 56068 +3
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
- Hits 34820 34770 -50
- Misses 21245 21298 +53
Partials 0 0
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks good. I'd rather merge this sooner than later, if something is terribly wrong, we might pick it up before the 2.1 release.
Actually, see the discussion starting at #7377 (comment). I think the issue mentioned by @QuLogic cannot really be fixed with the current implementation so perhaps we should instead follow the path mentioned in #7377 (comment) (see comment by @efiring re: backcompatibility, too). |
This needs a rebase. |
The current implementation of AxesStack subclasses cbook.Stack, which requires hashable keys, which leads to additional complexity on the caller's side (`_make_key`). Instead, switch to using two lists (keys and axes) and relying on `list.index`, which makes the implementation much simpler. Also make the new class private and deprecate the previous one.
9b7bfdc
to
0022611
Compare
done |
Are we not worried about #7377 (comment)? |
@QuLogic I missed that when I was re-reading the comments last night, sorry. |
attn @NelleV
The current implementation of AxesStack subclasses cbook.Stack, which
requires hashable keys, which leads to additional complexity on the
caller's side (
_make_key
).Instead, switch to using two lists (keys and axes) and relying on
list.index
, which makes the implementation much simpler.Also make the new class private and deprecate the previous one.
(The original idea was to make
AxesStack
private to avoid having to deal with issues such as #7194. I originally thought that we could just replace it by anOrderedDict
, which is still an option, but you need a really need a two-way dict to implement remove-by-value (instead of by key), so I ended up dropping that idea -- the benefit of dropping_make_key
looked more enticing.)