2

I have been experimenting a little with the abc module in python. A la

>>> import abc

In the normal case you expect your ABC class to not be instantiated if it contains an unimplemented abstractmethod. You know like as follows:

>>> class MyClass(metaclass=abc.ABCMeta):
...     @abc.abstractmethod
...     def mymethod(self):
...         return -1
...
>>> MyClass()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyClass with abstract methods mymethod

OR for any derived Class. It all seems to work fine until you inherit from something ... say dict or list as in the following:

>>> class YourClass(list, metaclass=abc.ABCMeta):
...     @abc.abstractmethod
...     def yourmethod(self):
...         return -1
...
>>> YourClass()
[]

This is surprising because type is probably the primary factory or metaclass -ish thing anyway or so I assume from the following.

>>> type(abc.ABCMeta)
<class 'type'>
>>> type(list)
<class 'type'>

From some investigation I found out that it boils down to something as simple as adding an __abstractmethod__ attribute to the class' object and rest happens by itself:

>>> class AbstractClass:
...     pass
...
>>> AbstractClass.__abstractmethods__ = {'abstractmethod'}
>>> AbstractClass()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class AbstractClass with abstract methods abstractmethod

So one can simply avoid the check by intentionally overriding the __new__ method and clearing out __abstractmethods__ as in below:

>>> class SupposedlyAbstractClass(metaclass=abc.ABCMeta):
...     def __new__(cls):
...         cls.__abstractmethods__ = {}
...         return super(AbstractClass, cls).__new__(cls)
...     @abc.abstractmethod
...     def abstractmethod(self):
...         return -1
...
>>> SupposedlyAbstractClass()
<__main__.SupposedlyAbstractClass object at 0x000001FA6BF05828>

This behaviour is the same in Python 2.7 and in Python 3.7 as I have personally checked. I am not aware if this is the same for all other python implementations.

Finally, down to the question ... Why has this been made to behave like so? Is it wise we should never make abstract classes out of list, tuple or dict? or should I just go ahead and add a __new__ class method checking for __abstractmethods__ before instantiation?

LazyLeopard
  • 194
  • 1
  • 11
  • 1
    That can likely be filed as an enhacement report to Python, if not outright as a bug. – jsbueno Aug 08 '19 at 15:50
  • I am not sure if this is the correct way to do it, but I have filed an [issue](https://bugs.python.org/issue37927) in the bug tracker – LazyLeopard Aug 23 '19 at 07:55

1 Answers1

3

The problem

If you have the next class:

from abc import ABC, abstractmethod
class Foo(list, ABC):
    @abstractmethod
    def yourmethod(self):
        pass

the problem is that and object of Foo can be created without any error because Foo.__new__(Foo) delegates the call directly to list.__new__(Foo) instead of ABC.__new__(Foo) (which is responsible of checking that all abstract methods are implemented in the class that is going to be instantiated)

We could implement __new__ on Foo and try to call ABC.__new__:

class Foo(list, ABC):
    def __new__(cls, *args, **kwargs):
        return ABC.__new__(cls)

    @abstractmethod
    def yourmethod(self):
        pass
Foo()

But he next error is raised:

TypeError: object.__new__(Foo) is not safe, use list.__new__()

This is due to ABC.__new__(Foo) invokes object.__new__(Foo) which seems that is not allowed when Foo inherits from list

A possible solution

You can add additional code on Foo.__new__ in order to check that all abstract methods in the class to be instantiated are implemented (basically do the job of ABC.__new__).

Something like this:

class Foo(list, ABC):
    def __new__(cls, *args, **kwargs):
        if hasattr(cls, '__abstractmethods__') and len(cls.__abstractmethods__) > 0:
            raise TypeError(f"Can't instantiate abstract class {cls.__name__} with abstract methods {', '.join(cls.__abstractmethods__)}")
        return super(Foo, cls).__new__(cls)


    @abstractmethod
    def yourmethod(self):
        return -1

Now Foo() raises an error. But the next code runs without any issue:

class Bar(Foo):
     def yourmethod(self):
         pass
Bar()
Victor Ruiz
  • 1,192
  • 9
  • 9
  • 2
    Indeed. I made some experimentation here - but abstract method checking is hard coded in C at `object.__new__` which, in turn, refuses to run if the class inherits from a built-in collection type. So, manually reimplementing the abstract check is the only option left. – jsbueno Aug 08 '19 at 18:54
  • 1
    Okay, lets us know your final solution or accept my answer if my version is fine – Victor Ruiz Aug 08 '19 at 21:27
  • 1
    I track the "metaclass" tag, and try to answer whatever pops in - but any answer besides this would be redundant. – jsbueno Aug 08 '19 at 22:52
  • 1
    Reimplementing `__new__` seems to work. But I feel that it may not be the most optimal solution. – LazyLeopard Aug 09 '19 at 06:59
  • 1
    @jsbueno it seems like [`__abstractmethods__`](https://github.com/python/cpython/search?q=repo%3Apython%2Fcpython+__abstractmethods__&unscoped_q=repo%3Apython%2Fcpython+__abstractmethods__) is only checked in [object_new](https://github.com/python/cpython/blob/c4cacc8c5eab50db8da3140353596f38a01115ca/Objects/typeobject.c) which is not used in other built-in objects ... its seems more lazy than intentional. – LazyLeopard Aug 09 '19 at 07:15
  • Maybe you can optimize it with something like this: Only check ```__abstractmethods__``` in the first call to ```__new__```. The next calls will not make the check and will only instantiate or throw the error. Also you can implement ```__setattr__``` on the metaclass to know when a new method is added to the class. If so, the next call to ```__new__``` should check again the implementation of the abstract methods – Victor Ruiz Aug 09 '19 at 08:49