Skip to content

gh-127750: Fix singledispatchmethod caching #127839

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ def __init__(self, func):
self.func = func

import weakref # see comment in singledispatch function
self._method_cache = weakref.WeakKeyDictionary()
self._method_cache = weakref.WeakValueDictionary()

def register(self, cls, method=None):
"""generic_method.register(cls, func) -> func
Expand All @@ -1039,15 +1039,12 @@ def register(self, cls, method=None):
return self.dispatcher.register(cls, func=method)

def __get__(self, obj, cls=None):
if self._method_cache is not None:
try:
_method = self._method_cache[obj]
except TypeError:
self._method_cache = None
except KeyError:
pass
else:
return _method
try:
_method = self._method_cache[id(obj)]
except KeyError:
pass
else:
return _method

dispatch = self.dispatcher.dispatch
funcname = getattr(self.func, '__name__', 'singledispatchmethod method')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe moving this line just above the TypeError would be a bit more readable?
The performance benefit this offers would only matter if there was a code which actively depends on handling this exception.

Expand All @@ -1057,12 +1054,12 @@ def _method(*args, **kwargs):
'1 positional argument')
return dispatch(args[0].__class__).__get__(obj, cls)(*args, **kwargs)

_method.__self_reference = _method # create strong reference to _method. see gh-127751
_method.__isabstractmethod__ = self.__isabstractmethod__
_method.register = self.register
update_wrapper(_method, self.func)

if self._method_cache is not None:
self._method_cache[obj] = _method
self._method_cache[id(obj)] = _method

return _method

Expand Down
47 changes: 47 additions & 0 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import collections
import collections.abc
import copy
import gc
from itertools import permutations
import pickle
from random import choice
Expand Down Expand Up @@ -3228,6 +3229,52 @@ def f(arg):
def _(arg: undefined):
return "forward reference"

def test_singledispatchmethod_hash_comparision_equal(self):
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
value: int

@functools.singledispatchmethod
def dispatch(self, x):
return id(self)

t1 = A(1)
t2 = A(1)

assert t1 == t2
assert id(t1) == t1.dispatch(2)
assert id(t2) == t2.dispatch(2) # gh-127750

def test_singledispatchmethod_object_references(self):
class ReferenceTest:
instance_counter = 0

def __init__(self):
ReferenceTest.instance_counter = ReferenceTest.instance_counter + 1

def __del__(self):
ReferenceTest.instance_counter = ReferenceTest.instance_counter - 1

@functools.singledispatchmethod
def go(self, item):
pass

assert ReferenceTest.instance_counter == 0
t=ReferenceTest()
assert ReferenceTest.instance_counter == 1
x = []
for ii in range(1000):
t = ReferenceTest()
t.go(ii)
x.append(t)
del t
del x
gc.collect()
assert ReferenceTest.instance_counter == 0


class CachedCostItem:
_cost = 1

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update caching of :class:`functools.singledispatchmethod` to avoid collisions of objects which compare equal.
Loading