[Python-checkins] r55315 - sandbox/trunk/abc/abc.py sandbox/trunk/abc/test_abc.py

guido.van.rossum python-checkins at python.org
Mon May 14 20:56:16 CEST 2007


Author: guido.van.rossum
Date: Mon May 14 20:56:14 2007
New Revision: 55315

Modified:
   sandbox/trunk/abc/abc.py
   sandbox/trunk/abc/test_abc.py
Log:
Move to the new ABCMeta metaclass machinery.
Specify the hash function for sets.


Modified: sandbox/trunk/abc/abc.py
==============================================================================
--- sandbox/trunk/abc/abc.py	(original)
+++ sandbox/trunk/abc/abc.py	Mon May 14 20:56:14 2007
@@ -1,15 +1,9 @@
 #!/usr/bin/env python3.0
 
-"""Abstract Base Classes experiment.  See PEP 3119.
+"""Abstract Base Classes (ABCs) experiment.  See PEP 3119.
 
-Note: this depends on the brand new Py3k feature that object.__ne__()
-is implemented by calling __eq__() and reversing the outcome (unless
-NotImplemented).
-
-XXX Should we use argument annotations here?
-
-XXX How to decide the order in which orthogonal base classes are
-listed?  For now, I'm putting the smaller API second.
+Note: this code depends on an unsubmitted patch:
+http://python.org/sf/1708353.
 """
 
 __author__ = "Guido van Rossum <guido at python.org>"
@@ -21,66 +15,31 @@
 ### ABC SUPPORT FRAMEWORK ###
 
 
-# Note: @abstractmethod will become a built-in; Abstract and
-# AbstractClass will disappear (the functionality will be subsumed in
-# object and type, respectively).
-
-
 def abstractmethod(funcobj):
     """A decorator indicating abstract methods.
 
-    Requires that the class (directly or indirectly) derives from
-    Abstract, and that the metaclass is AbstractClass or derived from it
-    (deriving from Abstract ensure this).  A class deriving from
-    Abstract cannot be instantiated unless all of its abstract methods
-    are overridden.  The abstract methods can be called using any of the
-    the normal 'super' call mechanisms.
+    Requires that the metaclass is ABCMeta or derived from it.  A
+    class that has a metaclass derived from ABCMeta cannot be
+    instantiated unless all of its abstract methods are overridden.
+    The abstract methods can be called using any of the the normal
+    'super' call mechanisms.
 
     Usage:
 
-        class C(Abstract):
+        class C(metaclass=ABCMeta):
             @abstractmethod
             def my_abstract_method(self, ...):
                 ...
-
-    When combining this with other decorators, this should come last:
-
-        class C(Abstract):
-            @classmethod
-            @abstractmethod
-            def my_abstract_class_method(self, ...):
-                ...
-
-    XXX This example doesn't currently work, since classmethod doesn't
-    pass through function attributes!  I think it should though.
     """
     funcobj.__isabstractmethod__ = True
     return funcobj
 
 
-class AbstractClass(type):
-
-    """Metaclass to support the abstractmethod decorator."""
-
-    def __new__(mcls, name, bases, namespace):
-        cls = super(AbstractClass, mcls).__new__(mcls, name, bases, namespace)
-        abstracts = {name
-                     for name, value in namespace.items()
-                     if getattr(value, "__isabstractmethod__", False)}
-        for base in bases:
-            for name in getattr(base, "__abstractmethods__", set()):
-                value = getattr(cls, name, None)
-                if getattr(value, "__isabstractmethod__", False):
-                    abstracts.add(name)
-        cls.__abstractmethods__ = abstracts
-        return cls
-
-
-class Abstract(metaclass=AbstractClass):
+class _Abstract(object):
 
-    """Base class to support the abstractmethod decorator.
+    """Helper class inserted into the bases by ABCMeta (using _fix_bases()).
 
-    This implicitly sets the metaclass to AbstractClass.
+    You should never need to explicitly subclass this class.
     """
 
     def __new__(cls, *args, **kwds):
@@ -89,52 +48,129 @@
             raise TypeError("can't instantiate abstract class %s "
                             "with abstract methods %s" %
                             (cls.__name__, ", ".join(sorted(am))))
-        return super(Abstract, cls).__new__(cls, *args, **kwds)
-
-
-### ORDERING ABCS ###
-
+        return super(_Abstract, cls).__new__(cls, *args, **kwds)
 
-class PartiallyOrdered(Abstract):
 
-    """Partial orderings define a consistent but incomplete < operator.
+def _fix_bases(bases):
+    """Helper method that inserts _Abstract in the bases if needed."""
+    for base in bases:
+        if issubclass(base, _Abstract):
+            # _Abstract is already a base (maybe indirectly)
+            return bases
+    if object in bases:
+        # Replace object with _Abstract
+        return tuple([_Abstract if base is object else base
+                      for base in bases])
+    # Append _Abstract to the end
+    return bases + (_Abstract,)
+
+
+class ABCMeta(type):
+
+    """Metaclass for defining Abstract Base Classes (ABCs).
+
+    Use this metaclass to create an ABC.  An ABC can be subclassed
+    directly, and then acts as a mix-in class.  You can also register
+    unrelated concrete classes (even built-in classes) and unrelated
+    ABCs as 'virtual subclasses' -- these and their descendants will
+    be considered subclasses of the registering ABC by the built-in
+    issubclass() function, but the registering ABC won't show up in
+    their MRO (Method Resolution Order) nor will method
+    implementations defined by the registering ABC be callable (not
+    even via super()).
 
-    Invariant: a < b < c => a < c
-
-    It is possible that none of a < b, a == b, a > b hold.
     """
 
-    @abstractmethod
-    def __lt__(self, other):
-        return NotImplemented
-
-    def __le__(self, other):
-        if not isinstance(other, PartiallyOrdered):
-            return NotImplemented
-        # Note that bool(NotImplemented) is True!
-        return self == other or self.__lt__(other)
-
-    # It's not necessary to define __gt__ and __ge__; these will
-    # automatically be translated to __lt__ and __le__ calls with
-    # swapped arguments by the rich comparisons framework.
-
-
-class TotallyOrdered(PartiallyOrdered):
+    # A global counter that is incremented each time a class is
+    # registered as a virtual subclass of anything.  It forces the
+    # negative cache to be cleared before its next use.
+    __invalidation_counter = 0
 
-    """Total orderings guarantee that all values are ordered.
-
-    E.g. for any two a and b, exactly one of a < b, a == b, a > b holds.
+    def __new__(mcls, name, bases, namespace):
+        bases = _fix_bases(bases)
+        cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)
+        # Compute set of abstract method names
+        abstracts = {name
+                     for name, value in namespace.items()
+                     if getattr(value, "__isabstractmethod__", False)}
+        for base in bases:
+            for name in getattr(base, "__abstractmethods__", set()):
+                value = getattr(cls, name, None)
+                if getattr(value, "__isabstractmethod__", False):
+                    abstracts.add(name)
+        cls.__abstractmethods__ = abstracts
+        # Set up inheritance registry
+        cls.__abc_registry__ = set()
+        cls.__abc_cache__ = set()
+        cls.__abc_negative_cache__ = set()
+        cls.__abc_negative_cache_version__ = ABCMeta.__invalidation_counter
+        return cls
 
-    XXX What about float?  The properties of NaN make it strictly
-    PartiallyOrdered.  But having it TotallyOrdered makes more sense
-    for most practical purposes.
-    """
+    def register(cls, subclass):
+        """Register a virtual subclass of an ABC."""
+        if not isinstance(cls, type):
+            raise TypeError("Can only register classes")
+        if issubclass(subclass, cls):
+            return  # Already a subclass
+        # Subtle: test for cycles *after* testing for "already a subclass";
+        # this means we allow X.register(X) and interpret it as a no-op.
+        if issubclass(cls, subclass):
+            # This would create a cycle, which is bad for the algorithm below
+            raise RuntimeError("Refusing to create an inheritance cycle")
+        cls.__abc_registry__.add(subclass)
+        ABCMeta.__invalidation_counter += 1  # Invalidate negative cache
+
+    def _dump_registry(cls, file=None):
+        """Debug helper to print the ABC registry."""
+        if file is None:
+            file = sys.stdout
+        print("Class: %s.%s" % (cls.__module__, cls.__name__), file=file)
+        print("Inv.counter: %s" % ABCMeta.__invalidation_counter, file=file)
+        for name in sorted(cls.__dict__.keys()):
+            if name.startswith("__abc_"):
+                value = getattr(cls, name)
+                print("%s: %r" % (name, value), file=file)
+
+    def __instancecheck__(cls, instance):
+        """Override for isinstance(instance, cls)."""
+        return any(cls.__subclasscheck__(c)
+                   for c in {instance.__class__, type(instance)})
+
+    def __subclasscheck__(cls, subclass):
+        """Override for issubclass(subclass, cls)."""
+        # Check cache
+        if subclass in cls.__abc_cache__:
+            return True
+        # Check negative cache; may have to invalidate
+        if cls.__abc_negative_cache_version__ < ABCMeta.__invalidation_counter:
+            # Invalidate the negative cache
+            cls.__abc_negative_cache_version__ = ABCMeta.__invalidation_counter
+            cls.__abc_negative_cache__ = set()
+        elif subclass in cls.__abc_negative_cache__:
+            return False
+        # Check if it's a direct subclass
+        if cls in subclass.mro():
+            cls.__abc_cache__.add(subclass)
+            return True
+        # Check if it's a subclass of a registered class (recursive)
+        for rcls in cls.__abc_registry__:
+            if issubclass(subclass, rcls):
+                cls.__abc_registry__.add(subclass)
+                return True
+        # Check if it's a subclass of a subclass (recursive)
+        for scls in cls.__subclasses__():
+            if issubclass(subclass, scls):
+                cls.__abc_registry__.add(subclass)
+                return True
+        # No dice; update negative cache
+        cls.__abc_negative_cache__.add(subclass)
+        return False
 
 
 ### ONE TRICK PONIES ###
 
 
-class Hashable(Abstract):
+class Hashable(metaclass=ABCMeta):
 
     """A hashable has one method, __hash__()."""
 
@@ -143,7 +179,7 @@
         return 0
 
 
-class Iterable(Abstract):
+class Iterable(metaclass=ABCMeta):
 
     """An iterable has one method, __iter__()."""
 
@@ -175,14 +211,14 @@
         # Or: raise StopIteration
 
 
-class Sized(Abstract):
+class Sized(metaclass=ABCMeta):
 
     @abstractmethod
     def __len__(self):
         return 0
 
 
-class Container(Abstract):
+class Container(metaclass=ABCMeta):
 
     """A container has a __contains__() method."""
 
@@ -198,7 +234,7 @@
     # Perhaps later, when we have type annotations so you can write
     # Container[T], we can do this:
     #
-    # class Container(Abstract):
+    # class Container(metaclass=ABCMeta):
     #     def __contains__(self, val: T) -> bool: ...
     #
     # class Searchable(Container):
@@ -278,8 +314,22 @@
         freedom for __eq__ or __hash__.  We match the algorithm used
         by the built-in frozenset type.
         """
-        # XXX This is not a very efficient implementation
-        return hash(frozenset(self))
+        MAX = sys.maxint
+        MASK = 2 * MAX + 1
+        n = len(self)
+        h = 1927868237 * (n + 1)
+        h &= MASK
+        for x in self:
+            hx = hash(x)
+            h ^= (hx ^ (hx << 16) ^ 89869747)  * 3644798167
+            h &= MASK
+        h = h * 69069 + 907133923
+        h &= MASK
+        if h > MAX:
+            h -= MASK + 1
+        if h == -1:
+            h = 590923713
+        return h
 
 
 # XXX Should this derive from Set instead of from ComposableSet?
@@ -505,10 +555,6 @@
         return False
 
 
-# XXX HashableMapping
-# XXX MutableMapping
-
-
 ### SEQUENCES ###
 
 
@@ -662,31 +708,6 @@
         return len(self) <= len(other)
 
 
-class HashableSequence(Sequence, Hashable):
-
-    def __hash__(self):
-        """The hash value must match __eq__.
-
-        Since we want sequences to be equal iff their elements are equal
-        (regardless of the sequence type), this algorithm is (XXX
-        hopefully) identical to tuple.__hash__().
-        """
-        mask = sys.maxint*2 + 1
-        mult = 1000003
-        h = 0x345678
-        n = len(self)
-        for i, elem in enumerate(self):
-            h = ((h ^ hash(elem)) * mult) & mask
-            mult = (mult + (82520 - 2 + 2*(n-i))) & mask
-        h += 97531
-        h &= mask
-        if h > sys.maxint:
-            h -= mask + 1
-        if h == -1:
-            h = -2
-        return h
-
-
 ### ADAPTERS ###
 
 

Modified: sandbox/trunk/abc/test_abc.py
==============================================================================
--- sandbox/trunk/abc/test_abc.py	(original)
+++ sandbox/trunk/abc/test_abc.py	Mon May 14 20:56:14 2007
@@ -9,7 +9,7 @@
 class ABCTestCase(unittest.TestCase):
 
     def test_abstract_method_machinery(self):
-        class C(abc.Abstract):
+        class C(metaclass=abc.ABCMeta):
             @abc.abstractmethod
             def foo(self): pass
             def bar(self): pass
@@ -21,22 +21,25 @@
             def foo(self): pass
         E()
 
-    def test_hashable_sequence_hash_matches_tuple_hash(self):
-        class C(abc.HashableSequence):
+    def test_hashable_set_hash_matches_frozenset_hash(self):
+        class C(abc.Set):
             def __new__(cls, values):
                 obj = super(C, cls).__new__(cls)
-                obj.__values = list(values)
+                obj.__values = set(values)
                 return obj
             def __len__(self):
                 return len(self.__values)
-            def __getitem__(self, i):
-                return self.__values[i]
+            def __contains__(self, x):
+                return x in self.__values
+            def __iter__(self):
+                return iter(self.__values)
         for l in ([], [0], [1], [0, 1],
                   list(range(-sys.maxint, sys.maxint, 100000)),
                   "The quick brown fox jumps over the lazy dog".split()):
-            hcl = hash(C(l))
-            htl = hash(tuple(l))
-            self.assertEqual(hcl, htl, repr((l, hcl, htl)))
+            hc = C(l)._hash()
+            hfs = hash(frozenset(l))
+            self.assertEqual(hc, hfs, repr((l, hc, hfs)))
+
 
     def test_adapt_to_sequence(self):
         a = abc.AdaptToSequence(range(10))
@@ -45,7 +48,6 @@
         self.assertEqual(a[9], 9)
         self.assertEqual(a[-1], 9)
         self.assertEqual(list(a), list(range(10)))
-        #self.assertEqual(a, range(10))
         b = a[1:-1]
         self.assertEqual(b.__class__, abc.AdaptToSequence)
         self.assertEqual(list(b), list(range(1, 9)))


More information about the Python-checkins mailing list