[Python-checkins] cpython: Issue #14631: Add a new :class:`weakref.WeakMethod` to simulate weak references

antoine.pitrou python-checkins at python.org
Sat Nov 17 19:00:01 CET 2012


http://hg.python.org/cpython/rev/27c20650aeab
changeset:   80476:27c20650aeab
user:        Antoine Pitrou <solipsis at pitrou.net>
date:        Sat Nov 17 18:57:38 2012 +0100
summary:
  Issue #14631: Add a new :class:`weakref.WeakMethod` to simulate weak references to bound methods.

files:
  Doc/library/weakref.rst  |   29 +++++
  Lib/test/test_weakref.py |  140 +++++++++++++++++++++++++++
  Lib/weakref.py           |   56 ++++++++++-
  Misc/NEWS                |    3 +
  4 files changed, 227 insertions(+), 1 deletions(-)


diff --git a/Doc/library/weakref.rst b/Doc/library/weakref.rst
--- a/Doc/library/weakref.rst
+++ b/Doc/library/weakref.rst
@@ -192,6 +192,35 @@
    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
 
    The type object for weak references objects.
diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py
--- a/Lib/test/test_weakref.py
+++ b/Lib/test/test_weakref.py
@@ -47,6 +47,11 @@
         return NotImplemented
     def __hash__(self):
         return hash(self.arg)
+    def some_method(self):
+        return 4
+    def other_method(self):
+        return 5
+
 
 class RefCycle:
     def __init__(self):
@@ -880,6 +885,140 @@
         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):
 
     COUNT = 10
@@ -1455,6 +1594,7 @@
 def test_main():
     support.run_unittest(
         ReferencesTestCase,
+        WeakMethodTestCase,
         MappingTestCase,
         WeakValueDictionaryTestCase,
         WeakKeyDictionaryTestCase,
diff --git a/Lib/weakref.py b/Lib/weakref.py
--- a/Lib/weakref.py
+++ b/Lib/weakref.py
@@ -27,7 +27,61 @@
 __all__ = ["ref", "proxy", "getweakrefcount", "getweakrefs",
            "WeakKeyDictionary", "ReferenceType", "ProxyType",
            "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):
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -127,6 +127,9 @@
 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
   conversions for special values to be consistent with those for float -> int
   and Decimal -> int.  Patch by Alexey Kachayev.

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list