Discussion forum for David Beazley

Die abstractmethod Die!


#1

So, Python has this abstract base class feature that allows you to write code like this:

from abc import ABC, abstractmethod

class Interface(ABC):
    @abstractmethod
    def yow(self):
        pass

If you inherit from Interface and don’t implement that method, you’ll get an error:

>>> class Spam(Interface):
...     pass
... 
>>> s = Spam()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Spam with abstract methods yow
>>> 

Abstract base classes are kind of cool if you’re making a framework. However, the part that’s not as cool is having to separately import the @abstractmethod decorator. I mean, it’s 15 extra characters of typing on the import statement (16 if you follow PEP8).

So, one of the very random things I’ve been thinking about lately involves devious uses of the the __prepare__() method in metaclasses. For example, maybe you could reformulate abstract base classes just a tiny bit like this:

# abc_.py
import abc
from collections import ChainMap

class ABCMeta_(abc.ABCMeta):
    @classmethod
    def __prepare__(cls, name, bases):
        return ChainMap({}, {'abstractmethod': abc.abstractmethod })

    @staticmethod
    def __new__(meta, clsname, bases, methods):
        return super().__new__(meta, clsname, bases, methods.maps[0])

class ABC(metaclass=ABCMeta_):
    pass

The general idea here is that you use a ChainMap to inject an extra scope of interesting objects into the class body. You can use all of that stuff in the scope within the body of a class. However, the __new__() method tosses the magic scope right before class creation. So, all of that extra stuff just disappears from the namespace when you’re done.

Doing this, you can get rid of that extra import. Just write this:

from abc_ import ABC

class Interface(ABC):
    @abstractmethod
    def yow(self):
        pass

Ah, yes. Somehow through dark magic, the @abstractmethod decorator just magically materializes into the namespace whenever you inherit from ABC. It even makes a fair bit of sense–you’d basically never use @abstractmethod outside of a class body or in a situation where you didn’t inherit from ABC. And there’s no point in inheriting from ABC if you’re not going to use @abstractmethod someplace. It all works.

Of course, there is a small chance that having a name just show up out of thin air like that will break your IDE and about a dozen other Python program checkers. Good.

Anyways, use this knowledge responsibly…


#2

Occasionally there is a good use for

from ... import *

this might be one of them. I also patched my copy of pyflakes to work with star imports. :wink:


#3

Honestly, from ... import * should only be used in the REPL for quick-and-dirty stuff. Otherwise they typically cause more trouble than they are worth.

As for from abc import abstractmethod, why not just import abc? As long as you have less than 15/4 uses you will save characters in the long run (plus I have a personal bias against importing attribute directly off of modules).


#4

Saving typing on the import statement is sort of missing the big picture. I’m proposing that abstractmethod should simply be part of the environment when defining an abstract base class. Merely by inheriting from ABC, that decorator could be made available inside the class body. It makes a lot of sense because the only place you’d really use @abstractmethod is inside the body of an ABC. The __prepare__() method is cool.


#5

My criteria for star imports:

  • are the included module contents so proliferous that the module name becomes line noise?
  • are there numerous items from the module being used?

If the answer to both is Yes, then I use a star import.

As you can imagine, it’s fairly rare that both of those are yes, so at this point the only solid example I have is writing scripts that make use of my scription library.


#6

Yes, __prepare__ is very cool. :slight_smile: Open an issue, maybe it will make it in!


#7

That’s an interesting idea, but it might be view as too magical by some (won’t know until you ask).


#8

Everything is magical if you think about it long enough.


#9

Very true. :smile: So go for it and try proposing the change!


#10

I like it except for the fact that you cannot override the value of abstractmethod from outside the class definition (or anywhere, actually). I have doubts on the usefullness of doing this, but something like

def abstractmethod(c):
    from abc import abstracmethod
    print("ABSTRACT:", c)
    return abstractmethod(c)

class S(ABC):
    @abstractmethod
    def spam(self):
        pass

would not load the custom abstractmethod, only abc.abstractmethod as defined in the metaclass. If the ChainMap could check globals first for an already defined abstractmethod, then things would remain explict-unless-undefined.