Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c3afba1

Browse files
committed
Issue #14631: Add a new :class:weakref.WeakMethod to simulate weak references to bound methods.
1 parent 25bbe5e commit c3afba1

4 files changed

Lines changed: 227 additions & 1 deletion

File tree

Doc/library/weakref.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,35 @@ These method have the same issues as the and :meth:`keyrefs` method of
192192
discarded when no strong reference to it exists any more.
193193

194194

195+
.. class:: WeakMethod(method)
196+
197+
A custom :class:`ref` subclass which simulates a weak reference to a bound
198+
method (i.e., a method defined on a class and looked up on an instance).
199+
Since a bound method is ephemeral, a standard weak reference cannot keep
200+
hold of it. :class:`WeakMethod` has special code to recreate the bound
201+
method until either the object or the original function dies::
202+
203+
>>> class C:
204+
... def method(self):
205+
... print("method called!")
206+
...
207+
>>> c = C()
208+
>>> r = weakref.ref(c.method)
209+
>>> r()
210+
>>> r = weakref.WeakMethod(c.method)
211+
>>> r()
212+
<bound method C.method of <__main__.C object at 0x7fc859830220>>
213+
>>> r()()
214+
method called!
215+
>>> del c
216+
>>> gc.collect()
217+
0
218+
>>> r()
219+
>>>
220+
221+
.. versionadded:: 3.4
222+
223+
195224
.. data:: ReferenceType
196225

197226
The type object for weak references objects.

Lib/test/test_weakref.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def __lt__(self, other):
4747
return NotImplemented
4848
def __hash__(self):
4949
return hash(self.arg)
50+
def some_method(self):
51+
return 4
52+
def other_method(self):
53+
return 5
54+
5055

5156
class RefCycle:
5257
def __init__(self):
@@ -880,6 +885,140 @@ def callback(w):
880885
self.assertEqual(self.cbcalled, 0)
881886

882887

888+
class WeakMethodTestCase(unittest.TestCase):
889+
890+
def _subclass(self):
891+
"""Return a Object subclass overriding `some_method`."""
892+
class C(Object):
893+
def some_method(self):
894+
return 6
895+
return C
896+
897+
def test_alive(self):
898+
o = Object(1)
899+
r = weakref.WeakMethod(o.some_method)
900+
self.assertIsInstance(r, weakref.ReferenceType)
901+
self.assertIsInstance(r(), type(o.some_method))
902+
self.assertIs(r().__self__, o)
903+
self.assertIs(r().__func__, o.some_method.__func__)
904+
self.assertEqual(r()(), 4)
905+
906+
def test_object_dead(self):
907+
o = Object(1)
908+
r = weakref.WeakMethod(o.some_method)
909+
del o
910+
gc.collect()
911+
self.assertIs(r(), None)
912+
913+
def test_method_dead(self):
914+
C = self._subclass()
915+
o = C(1)
916+
r = weakref.WeakMethod(o.some_method)
917+
del C.some_method
918+
gc.collect()
919+
self.assertIs(r(), None)
920+
921+
def test_callback_when_object_dead(self):
922+
# Test callback behaviour when object dies first.
923+
C = self._subclass()
924+
calls = []
925+
def cb(arg):
926+
calls.append(arg)
927+
o = C(1)
928+
r = weakref.WeakMethod(o.some_method, cb)
929+
del o
930+
gc.collect()
931+
self.assertEqual(calls, [r])
932+
# Callback is only called once.
933+
C.some_method = Object.some_method
934+
gc.collect()
935+
self.assertEqual(calls, [r])
936+
937+
def test_callback_when_method_dead(self):
938+
# Test callback behaviour when method dies first.
939+
C = self._subclass()
940+
calls = []
941+
def cb(arg):
942+
calls.append(arg)
943+
o = C(1)
944+
r = weakref.WeakMethod(o.some_method, cb)
945+
del C.some_method
946+
gc.collect()
947+
self.assertEqual(calls, [r])
948+
# Callback is only called once.
949+
del o
950+
gc.collect()
951+
self.assertEqual(calls, [r])
952+
953+
@support.cpython_only
954+
def test_no_cycles(self):
955+
# A WeakMethod doesn't create any reference cycle to itself.
956+
o = Object(1)
957+
def cb(_):
958+
pass
959+
r = weakref.WeakMethod(o.some_method, cb)
960+
wr = weakref.ref(r)
961+
del r
962+
self.assertIs(wr(), None)
963+
964+
def test_equality(self):
965+
def _eq(a, b):
966+
self.assertTrue(a == b)
967+
self.assertFalse(a != b)
968+
def _ne(a, b):
969+
self.assertTrue(a != b)
970+
self.assertFalse(a == b)
971+
x = Object(1)
972+
y = Object(1)
973+
a = weakref.WeakMethod(x.some_method)
974+
b = weakref.WeakMethod(y.some_method)
975+
c = weakref.WeakMethod(x.other_method)
976+
d = weakref.WeakMethod(y.other_method)
977+
# Objects equal, same method
978+
_eq(a, b)
979+
_eq(c, d)
980+
# Objects equal, different method
981+
_ne(a, c)
982+
_ne(a, d)
983+
_ne(b, c)
984+
_ne(b, d)
985+
# Objects unequal, same or different method
986+
z = Object(2)
987+
e = weakref.WeakMethod(z.some_method)
988+
f = weakref.WeakMethod(z.other_method)
989+
_ne(a, e)
990+
_ne(a, f)
991+
_ne(b, e)
992+
_ne(b, f)
993+
del x, y, z
994+
gc.collect()
995+
# Dead WeakMethods compare by identity
996+
refs = a, b, c, d, e, f
997+
for q in refs:
998+
for r in refs:
999+
self.assertEqual(q == r, q is r)
1000+
self.assertEqual(q != r, q is not r)
1001+
1002+
def test_hashing(self):
1003+
# Alive WeakMethods are hashable if the underlying object is
1004+
# hashable.
1005+
x = Object(1)
1006+
y = Object(1)
1007+
a = weakref.WeakMethod(x.some_method)
1008+
b = weakref.WeakMethod(y.some_method)
1009+
c = weakref.WeakMethod(y.other_method)
1010+
# Since WeakMethod objects are equal, the hashes should be equal.
1011+
self.assertEqual(hash(a), hash(b))
1012+
ha = hash(a)
1013+
# Dead WeakMethods retain their old hash value
1014+
del x, y
1015+
gc.collect()
1016+
self.assertEqual(hash(a), ha)
1017+
self.assertEqual(hash(b), ha)
1018+
# If it wasn't hashed when alive, a dead WeakMethod cannot be hashed.
1019+
self.assertRaises(TypeError, hash, c)
1020+
1021+
8831022
class MappingTestCase(TestBase):
8841023

8851024
COUNT = 10
@@ -1455,6 +1594,7 @@ def _reference(self):
14551594
def test_main():
14561595
support.run_unittest(
14571596
ReferencesTestCase,
1597+
WeakMethodTestCase,
14581598
MappingTestCase,
14591599
WeakValueDictionaryTestCase,
14601600
WeakKeyDictionaryTestCase,

Lib/weakref.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,61 @@
2727
__all__ = ["ref", "proxy", "getweakrefcount", "getweakrefs",
2828
"WeakKeyDictionary", "ReferenceType", "ProxyType",
2929
"CallableProxyType", "ProxyTypes", "WeakValueDictionary",
30-
"WeakSet"]
30+
"WeakSet", "WeakMethod"]
31+
32+
33+
class WeakMethod(ref):
34+
"""
35+
A custom `weakref.ref` subclass which simulates a weak reference to
36+
a bound method, working around the lifetime problem of bound methods.
37+
"""
38+
39+
__slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__"
40+
41+
def __new__(cls, meth, callback=None):
42+
try:
43+
obj = meth.__self__
44+
func = meth.__func__
45+
except AttributeError:
46+
raise TypeError("argument should be a bound method, not {}"
47+
.format(type(meth))) from None
48+
def _cb(arg):
49+
# The self-weakref trick is needed to avoid creating a reference
50+
# cycle.
51+
self = self_wr()
52+
if self._alive:
53+
self._alive = False
54+
if callback is not None:
55+
callback(self)
56+
self = ref.__new__(cls, obj, _cb)
57+
self._func_ref = ref(func, _cb)
58+
self._meth_type = type(meth)
59+
self._alive = True
60+
self_wr = ref(self)
61+
return self
62+
63+
def __call__(self):
64+
obj = super().__call__()
65+
func = self._func_ref()
66+
if obj is None or func is None:
67+
return None
68+
return self._meth_type(func, obj)
69+
70+
def __eq__(self, other):
71+
if isinstance(other, WeakMethod):
72+
if not self._alive or not other._alive:
73+
return self is other
74+
return ref.__eq__(self, other) and self._func_ref == other._func_ref
75+
return False
76+
77+
def __ne__(self, other):
78+
if isinstance(other, WeakMethod):
79+
if not self._alive or not other._alive:
80+
return self is not other
81+
return ref.__ne__(self, other) or self._func_ref != other._func_ref
82+
return True
83+
84+
__hash__ = ref.__hash__
3185

3286

3387
class WeakValueDictionary(collections.MutableMapping):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ Core and Builtins
127127
Library
128128
-------
129129

130+
- Issue #14631: Add a new :class:`weakref.WeakMethod` to simulate weak
131+
references to bound methods.
132+
130133
- Issue #16469: Fix exceptions from float -> Fraction and Decimal -> Fraction
131134
conversions for special values to be consistent with those for float -> int
132135
and Decimal -> int. Patch by Alexey Kachayev.

0 commit comments

Comments
 (0)