ice-wind
ice-wind

Reputation: 868

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 !

Upvotes: 2

Views: 296

Answers (1)

MisterMiyagi
MisterMiyagi

Reputation: 50126

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)

Upvotes: 1

Related Questions