mirror of
https://github.com/python/cpython.git
synced 2025-10-09 08:31:26 +00:00
Issue #14631: Add a new :class:weakref.WeakMethod
to simulate weak references to bound methods.
This commit is contained in:
parent
25bbe5e0bc
commit
c3afba104a
4 changed files with 227 additions and 1 deletions
|
@ -192,6 +192,35 @@ These method have the same issues as the and :meth:`keyrefs` method of
|
||||||
discarded when no strong reference to it exists any more.
|
discarded when no strong reference to it exists any more.
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: WeakMethod(method)
|
||||||
|
|
||||||
|
A custom :class:`ref` subclass which simulates a weak reference to a bound
|
||||||
|
method (i.e., a method defined on a class and looked up on an instance).
|
||||||
|
Since a bound method is ephemeral, a standard weak reference cannot keep
|
||||||
|
hold of it. :class:`WeakMethod` has special code to recreate the bound
|
||||||
|
method until either the object or the original function dies::
|
||||||
|
|
||||||
|
>>> class C:
|
||||||
|
... def method(self):
|
||||||
|
... print("method called!")
|
||||||
|
...
|
||||||
|
>>> c = C()
|
||||||
|
>>> r = weakref.ref(c.method)
|
||||||
|
>>> r()
|
||||||
|
>>> r = weakref.WeakMethod(c.method)
|
||||||
|
>>> r()
|
||||||
|
<bound method C.method of <__main__.C object at 0x7fc859830220>>
|
||||||
|
>>> r()()
|
||||||
|
method called!
|
||||||
|
>>> del c
|
||||||
|
>>> gc.collect()
|
||||||
|
0
|
||||||
|
>>> r()
|
||||||
|
>>>
|
||||||
|
|
||||||
|
.. versionadded:: 3.4
|
||||||
|
|
||||||
|
|
||||||
.. data:: ReferenceType
|
.. data:: ReferenceType
|
||||||
|
|
||||||
The type object for weak references objects.
|
The type object for weak references objects.
|
||||||
|
|
|
@ -47,6 +47,11 @@ class Object:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.arg)
|
return hash(self.arg)
|
||||||
|
def some_method(self):
|
||||||
|
return 4
|
||||||
|
def other_method(self):
|
||||||
|
return 5
|
||||||
|
|
||||||
|
|
||||||
class RefCycle:
|
class RefCycle:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -880,6 +885,140 @@ class SubclassableWeakrefTestCase(TestBase):
|
||||||
self.assertEqual(self.cbcalled, 0)
|
self.assertEqual(self.cbcalled, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class WeakMethodTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def _subclass(self):
|
||||||
|
"""Return a Object subclass overriding `some_method`."""
|
||||||
|
class C(Object):
|
||||||
|
def some_method(self):
|
||||||
|
return 6
|
||||||
|
return C
|
||||||
|
|
||||||
|
def test_alive(self):
|
||||||
|
o = Object(1)
|
||||||
|
r = weakref.WeakMethod(o.some_method)
|
||||||
|
self.assertIsInstance(r, weakref.ReferenceType)
|
||||||
|
self.assertIsInstance(r(), type(o.some_method))
|
||||||
|
self.assertIs(r().__self__, o)
|
||||||
|
self.assertIs(r().__func__, o.some_method.__func__)
|
||||||
|
self.assertEqual(r()(), 4)
|
||||||
|
|
||||||
|
def test_object_dead(self):
|
||||||
|
o = Object(1)
|
||||||
|
r = weakref.WeakMethod(o.some_method)
|
||||||
|
del o
|
||||||
|
gc.collect()
|
||||||
|
self.assertIs(r(), None)
|
||||||
|
|
||||||
|
def test_method_dead(self):
|
||||||
|
C = self._subclass()
|
||||||
|
o = C(1)
|
||||||
|
r = weakref.WeakMethod(o.some_method)
|
||||||
|
del C.some_method
|
||||||
|
gc.collect()
|
||||||
|
self.assertIs(r(), None)
|
||||||
|
|
||||||
|
def test_callback_when_object_dead(self):
|
||||||
|
# Test callback behaviour when object dies first.
|
||||||
|
C = self._subclass()
|
||||||
|
calls = []
|
||||||
|
def cb(arg):
|
||||||
|
calls.append(arg)
|
||||||
|
o = C(1)
|
||||||
|
r = weakref.WeakMethod(o.some_method, cb)
|
||||||
|
del o
|
||||||
|
gc.collect()
|
||||||
|
self.assertEqual(calls, [r])
|
||||||
|
# Callback is only called once.
|
||||||
|
C.some_method = Object.some_method
|
||||||
|
gc.collect()
|
||||||
|
self.assertEqual(calls, [r])
|
||||||
|
|
||||||
|
def test_callback_when_method_dead(self):
|
||||||
|
# Test callback behaviour when method dies first.
|
||||||
|
C = self._subclass()
|
||||||
|
calls = []
|
||||||
|
def cb(arg):
|
||||||
|
calls.append(arg)
|
||||||
|
o = C(1)
|
||||||
|
r = weakref.WeakMethod(o.some_method, cb)
|
||||||
|
del C.some_method
|
||||||
|
gc.collect()
|
||||||
|
self.assertEqual(calls, [r])
|
||||||
|
# Callback is only called once.
|
||||||
|
del o
|
||||||
|
gc.collect()
|
||||||
|
self.assertEqual(calls, [r])
|
||||||
|
|
||||||
|
@support.cpython_only
|
||||||
|
def test_no_cycles(self):
|
||||||
|
# A WeakMethod doesn't create any reference cycle to itself.
|
||||||
|
o = Object(1)
|
||||||
|
def cb(_):
|
||||||
|
pass
|
||||||
|
r = weakref.WeakMethod(o.some_method, cb)
|
||||||
|
wr = weakref.ref(r)
|
||||||
|
del r
|
||||||
|
self.assertIs(wr(), None)
|
||||||
|
|
||||||
|
def test_equality(self):
|
||||||
|
def _eq(a, b):
|
||||||
|
self.assertTrue(a == b)
|
||||||
|
self.assertFalse(a != b)
|
||||||
|
def _ne(a, b):
|
||||||
|
self.assertTrue(a != b)
|
||||||
|
self.assertFalse(a == b)
|
||||||
|
x = Object(1)
|
||||||
|
y = Object(1)
|
||||||
|
a = weakref.WeakMethod(x.some_method)
|
||||||
|
b = weakref.WeakMethod(y.some_method)
|
||||||
|
c = weakref.WeakMethod(x.other_method)
|
||||||
|
d = weakref.WeakMethod(y.other_method)
|
||||||
|
# Objects equal, same method
|
||||||
|
_eq(a, b)
|
||||||
|
_eq(c, d)
|
||||||
|
# Objects equal, different method
|
||||||
|
_ne(a, c)
|
||||||
|
_ne(a, d)
|
||||||
|
_ne(b, c)
|
||||||
|
_ne(b, d)
|
||||||
|
# Objects unequal, same or different method
|
||||||
|
z = Object(2)
|
||||||
|
e = weakref.WeakMethod(z.some_method)
|
||||||
|
f = weakref.WeakMethod(z.other_method)
|
||||||
|
_ne(a, e)
|
||||||
|
_ne(a, f)
|
||||||
|
_ne(b, e)
|
||||||
|
_ne(b, f)
|
||||||
|
del x, y, z
|
||||||
|
gc.collect()
|
||||||
|
# Dead WeakMethods compare by identity
|
||||||
|
refs = a, b, c, d, e, f
|
||||||
|
for q in refs:
|
||||||
|
for r in refs:
|
||||||
|
self.assertEqual(q == r, q is r)
|
||||||
|
self.assertEqual(q != r, q is not r)
|
||||||
|
|
||||||
|
def test_hashing(self):
|
||||||
|
# Alive WeakMethods are hashable if the underlying object is
|
||||||
|
# hashable.
|
||||||
|
x = Object(1)
|
||||||
|
y = Object(1)
|
||||||
|
a = weakref.WeakMethod(x.some_method)
|
||||||
|
b = weakref.WeakMethod(y.some_method)
|
||||||
|
c = weakref.WeakMethod(y.other_method)
|
||||||
|
# Since WeakMethod objects are equal, the hashes should be equal.
|
||||||
|
self.assertEqual(hash(a), hash(b))
|
||||||
|
ha = hash(a)
|
||||||
|
# Dead WeakMethods retain their old hash value
|
||||||
|
del x, y
|
||||||
|
gc.collect()
|
||||||
|
self.assertEqual(hash(a), ha)
|
||||||
|
self.assertEqual(hash(b), ha)
|
||||||
|
# If it wasn't hashed when alive, a dead WeakMethod cannot be hashed.
|
||||||
|
self.assertRaises(TypeError, hash, c)
|
||||||
|
|
||||||
|
|
||||||
class MappingTestCase(TestBase):
|
class MappingTestCase(TestBase):
|
||||||
|
|
||||||
COUNT = 10
|
COUNT = 10
|
||||||
|
@ -1455,6 +1594,7 @@ __test__ = {'libreftest' : libreftest}
|
||||||
def test_main():
|
def test_main():
|
||||||
support.run_unittest(
|
support.run_unittest(
|
||||||
ReferencesTestCase,
|
ReferencesTestCase,
|
||||||
|
WeakMethodTestCase,
|
||||||
MappingTestCase,
|
MappingTestCase,
|
||||||
WeakValueDictionaryTestCase,
|
WeakValueDictionaryTestCase,
|
||||||
WeakKeyDictionaryTestCase,
|
WeakKeyDictionaryTestCase,
|
||||||
|
|
|
@ -27,7 +27,61 @@ ProxyTypes = (ProxyType, CallableProxyType)
|
||||||
__all__ = ["ref", "proxy", "getweakrefcount", "getweakrefs",
|
__all__ = ["ref", "proxy", "getweakrefcount", "getweakrefs",
|
||||||
"WeakKeyDictionary", "ReferenceType", "ProxyType",
|
"WeakKeyDictionary", "ReferenceType", "ProxyType",
|
||||||
"CallableProxyType", "ProxyTypes", "WeakValueDictionary",
|
"CallableProxyType", "ProxyTypes", "WeakValueDictionary",
|
||||||
"WeakSet"]
|
"WeakSet", "WeakMethod"]
|
||||||
|
|
||||||
|
|
||||||
|
class WeakMethod(ref):
|
||||||
|
"""
|
||||||
|
A custom `weakref.ref` subclass which simulates a weak reference to
|
||||||
|
a bound method, working around the lifetime problem of bound methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__"
|
||||||
|
|
||||||
|
def __new__(cls, meth, callback=None):
|
||||||
|
try:
|
||||||
|
obj = meth.__self__
|
||||||
|
func = meth.__func__
|
||||||
|
except AttributeError:
|
||||||
|
raise TypeError("argument should be a bound method, not {}"
|
||||||
|
.format(type(meth))) from None
|
||||||
|
def _cb(arg):
|
||||||
|
# The self-weakref trick is needed to avoid creating a reference
|
||||||
|
# cycle.
|
||||||
|
self = self_wr()
|
||||||
|
if self._alive:
|
||||||
|
self._alive = False
|
||||||
|
if callback is not None:
|
||||||
|
callback(self)
|
||||||
|
self = ref.__new__(cls, obj, _cb)
|
||||||
|
self._func_ref = ref(func, _cb)
|
||||||
|
self._meth_type = type(meth)
|
||||||
|
self._alive = True
|
||||||
|
self_wr = ref(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
obj = super().__call__()
|
||||||
|
func = self._func_ref()
|
||||||
|
if obj is None or func is None:
|
||||||
|
return None
|
||||||
|
return self._meth_type(func, obj)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, WeakMethod):
|
||||||
|
if not self._alive or not other._alive:
|
||||||
|
return self is other
|
||||||
|
return ref.__eq__(self, other) and self._func_ref == other._func_ref
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
if isinstance(other, WeakMethod):
|
||||||
|
if not self._alive or not other._alive:
|
||||||
|
return self is not other
|
||||||
|
return ref.__ne__(self, other) or self._func_ref != other._func_ref
|
||||||
|
return True
|
||||||
|
|
||||||
|
__hash__ = ref.__hash__
|
||||||
|
|
||||||
|
|
||||||
class WeakValueDictionary(collections.MutableMapping):
|
class WeakValueDictionary(collections.MutableMapping):
|
||||||
|
|
|
@ -127,6 +127,9 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #14631: Add a new :class:`weakref.WeakMethod` to simulate weak
|
||||||
|
references to bound methods.
|
||||||
|
|
||||||
- Issue #16469: Fix exceptions from float -> Fraction and Decimal -> Fraction
|
- Issue #16469: Fix exceptions from float -> Fraction and Decimal -> Fraction
|
||||||
conversions for special values to be consistent with those for float -> int
|
conversions for special values to be consistent with those for float -> int
|
||||||
and Decimal -> int. Patch by Alexey Kachayev.
|
and Decimal -> int. Patch by Alexey Kachayev.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue