Search code examples
pythoninheritanceabstract-classpython-3.8

ABC refuses my subclass, despite it having no abstract methods


I have a collections.abc.MutableMapping subclass which implements the required abstract methods through monkey patching :

from collections.abc import MutableMapping

def make_wrappers(cls, methods = []):
    """This is used to eliminate code repetition, this file contains around 12
    classes with similar rewirings of self.method to self.value.method
    This approach is used instead of overriding __getattr__ and __getattribute__
    because those are bypassed by magic methods like add()
    """
    for method in methods:
        def wrapper(self, *args, _method=method, **kwargs):
            return getattr(self.value, _method)(*args, **kwargs)
        setattr(cls, method, wrapper)

class MySubclass(MutableMapping):
    def __init__(self, value = None):
        value = {} if value is None else value
        self.value = value

make_wrappers(
    MySubclass, 
    ['__delitem__', '__getitem__', '__iter__', '__len__', '__setitem__']
)

When trying to instanciate MySubclass, I get this error :

>>> c = MySubclass({'a':a, 'b':b})

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    c = MySubclass({'a':1, 'b':2})
TypeError: Can't instantiate abstract class MySubclass with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__

Yet this works :

>>> MySubclass.__setitem__

<function make_wrappers.<locals>.wrapper at 0x0000020F76A24AF0>

How do I force instantiation ?

I know those methods work, because when I put an additional layer of inheritance between MySubclass and collections.abc.MutableMapping they magically do !


Solution

  • Creating an ABC creates a set of missing abstract methods as soon as the class is created. This must be cleared to allow instantiating the class.

    >>> # setup as before
    >>> MySubclass.__abstractmethods__
    frozenset({'__delitem__', '__getitem__', '__iter__', '__len__', '__setitem__'})
    >>> MySubclass({'a':a, 'b':b})
    # TypeError: Can't instantiate abstract class MySubclass with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__
    >>> MySubclass.__abstractmethods__ = frozenset()  # clear cache of abstract methods
    >>> MySubclass({'a':a, 'b':b})
    <__main__.MySubclass at 0x1120a9340>
    

    Be aware that .__abstractmethods__ is not part of the Python Data Model or abc specification. Consider it version and implementation specific – always test whether your target version/implementation uses it. It should work on CPython (testet on Py3.6 and Py3.9) and PyPy3 (testet on Py3.6), however.


    The wrapper function can be adjusted to automatically remove monkey-patched methods from the abstract method cache. This makes the class eligible for instantiation if all methods are patched.

    def make_wrappers(cls, methods = []):
        """This is used to eliminate code repetition, this file contains around 12
        classes with similar rewirings of self.method to self.value.method
        This approach is used instead of overriding __getattr__ and __getattribute__
        because those are bypassed by magic methods like add()
        """
        for method in methods:
            def wrapper(self, *args, _method=method, **kwargs):
                return getattr(self.value, _method)(*args, **kwargs)
            setattr(cls, method, wrapper)
        cls.__abstractmethods__ = cls.__abstractmethods__.difference(methods)