Repository navigation
Closed
Closed
Copy link
Milestone
Description
Bug summary
I've got a class Charts that uses self.fig.canvas.mpl_connect('key_press_event', self.sendEvent) to handle keypresses, which works fine. But recently I tried changing my class to subclass dict, and – oddly – this seems to confuse Matplotlib. I get the error:
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/helipad/visualize.py", line 72, in launch
self.fig.canvas.mpl_connect('key_press_event', self.sendEvent)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/matplotlib/backend_bases.py", line 2476, in mpl_connect
return self.callbacks.connect(s, func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 228, in connect
if proxy in self._func_cid_map[signal]:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'Charts'
Which is weird because I'm not passing Charts at all.
Looking at MPL's code, it looks like the _weak_or_strong_ref function is getting a reference to Charts back out of the method I passed as a callback, putting it in the proxy variable, and hashing it, which subclassing dict prevents it from doing.
Code for reproduction
import matplotlib.pyplot as plt
class ThisWorks:
def __init__(self):
self.fig, plots = plt.subplots(1)
self.fig.canvas.mpl_connect('key_press_event', self.sendEvent)
self.keyListeners = {}
def sendEvent(self, event):
if event.name=='key_press_event' and event.key in self.keyListeners:
for f in self.keyListeners[event.key]: f(self.model, event)
class ThisDoesntWork(ThisWorks, dict): pass
x = ThisDoesntWork()Actual outcome
Traceback (most recent call last):
File "/Users/charwick/Desktop/example.py", line 15, in <module>
x = ThisDoesntWork()
^^^^^^^^^^^^^^^^
File "example.py", line 6, in __init__
self.fig.canvas.mpl_connect('key_press_event', self.sendEvent)
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/matplotlib/backend_bases.py", line 2476, in mpl_connect
return self.callbacks.connect(s, func)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/matplotlib/cbook/__init__.py", line 227, in connect
if proxy in self._func_cid_map[signal]:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'ThisDoesntWork'
Expected outcome
Code above doesn't actually do anything, but it shouldn't generate an error.
Additional information
No response
Operating system
MacOS 13.3.1
Matplotlib Version
3.7.1
Matplotlib Backend
No response
Python version
3.11.3
Jupyter version
No response
Installation
pip
Metadata
Metadata
Assignees
Labels
No labels
Activity
ksunden commentedon May 30, 2023
dictobjects are not hashable, and the hash we get fromweakref.WeakMethodis to the containing class, not the method of the class...I think there is a reasonable argument to be had that it should be the hash of the bound method, not the class, but that is upstream CPython behavior, not mpl behavior directly... I do not think we are likely to give up on using
WeakMethodfor this use-case all together, and don't see a particularly good way to resolve it on our end...As a workaround (but acknowledging that it is taking away some of the "dict-like" behavior of your subclass, you can define a
__hash__:A couple more thoughts:
dictcan be odd/hard to deal with in general (we do it in a small number of places here, but we are looking to stop doing so...)super().__init__()is potentially part of the problem (though seemingly not the only problem...)ThisWorkscaseself.sendEventis still afunctionobject (rather than abound method, this causes fallback behavior of keeping a strong reference rather than a weak reference.ThisDoesntWorkcaseself.sendEventis actually abound methodand thusweakref.WeakMethodwill wrap ittacaswell commentedon May 30, 2023
Another workaround is:
I would not over-ride
__hash__on a mutable object unless you are very sure you understand the consequences (see https://docs.python.org/3/reference/datamodel.html#object.__hash__). Falling back to theThisWorkshash method will fall back to usingid(self), however the__eq__implementation is presumably still inherited fromdictand would thus violate:I think this is expected and correct behavior (leaving aside the running discussion of if we should hold hard or weak references to the callbacks) and think we should close this with no action. If we do take any action, it should be to clarify what the constraints on the callbacks are.
I am now curious how
__eq__on bound methods is implemented astacaswell commentedon May 30, 2023
I think https://github.com/python/cpython/blob/4571eedca2f70768ddc4a2fd4fba2ae11b4c8037/Objects/descrobject.c#L1322-L1332 is the hash implementation and https://github.com/python/cpython/blob/4571eedca2f70768ddc4a2fd4fba2ae11b4c8037/Objects/descrobject.c#L1296-L1320 is the compare implementation.
I do not have a good explanation as to why
In [6]behaves that way. I had a guess that it was related to PyObjects getting reused (so the first tuple gets gc'd before the second one gets created), butso this must be something about the way Python handles literals that come in via the same expression/statement?!
anntzer commentedon May 30, 2023
@tacaswell I guess the peephole optimizer, or something similar, arranges to store only once the constant in the code object:
anntzer commentedon May 30, 2023
Actually it looks like the hashability requirement is only used to maintain _func_cid_map, which itself only serves to simplify the callback deduplication code, which I think is a misfeature anyways (#20210) (not the same misfeature as using weakrefs ;-))
In any case we can easily do without _func_cid_map, at the small cost of making callback registration O(already-present-callbacks) as we instead iterate over all previously present callbacks to check whether the new one is a duplicate or not (if we drop the deduplication (mis)feature, which is something that can be done with a normal deprecation (see discussion at #20210), we go back to O(1) callback registration). The patch should be approximately
(plus adjusting the tests that actually try to touch _func_cid_map).