Creating an abstract abstract class that retains a constructor

Suppose you have a specific class

class Knight(object):
    def __init__(self, name):
        self._name = name
    def __str__(self):
        return "Sir {} of Camelot".format(self.name)

      

Now there is a change in the class hierarchy. Knight

should become an abstract base class with a bunch of concrete subclasses for knights of different castles. Easy enough:

class Knight(metaclass=ABCMeta):  # Python 3 syntax
    def __str__(self):
        return "Sir {} of {}".format(self.name, self.castle)
    @abstractmethod
    def sing():
        pass

class KnightOfTheRoundTable(Knight):
    def __init__(self, name):
        self.name = name
    @property
    def castle(self):
        return "Camelot"
    @staticmethod
    def sing():
        return ["We're knights of the round table",
                "We dance whenever we're able"]

      

But now all the code I used Knight("Galahad")

to build Knight

is broken. Instead, we can save Knight

as is and enter BaseKnight

, but then the code that checks for existence isinstance(x, Knight)

and should work on any knight might have to be replaced instead BaseKnight

.

How can I make a concrete class abstract, saving both constructor and isinstance

checks?

+3


source to share


2 answers


Make the existing class a base class, but overload__new__

to return the subclass when trying to instantiate the base class:

class Knight(metaclass=ABCMeta):
    def __new__(cls, *args, **kwargs):
        if cls is Knight:
            # attempt to construct base class, defer to default subclass
            return KnightOfTheRoundTable(*args, **kwargs)
        else:
            obj = super(Knight, cls).__new__(cls)
            obj.__init__(*args, **kwargs)
            return obj
    def __str__(self):
        return "Sir {} of {}".format(self.name, self.castle)
    @abstractmethod
    def sing():
        pass

      



Now Knight("Galahad")

keeps running but returns KnightOfTheRoundTable

. isinstance(Knight("Robin"), Knight)

return True

, just like checking isinstance(x, Knight)

for any other instance of the subclass.

+2


source


Your messing with solution __new__

mostly works, but it has a disadvantage where it Knight(...)

doesn't give you Knight

, but Knight

simply ABC.

The recording is a BaseKnight

little cleaner, but then you have a problem

which checks for presence isinstance(x, Knight)

and should work on any knight might need to be modified to use BaseKnight instead.

which can be fixed by adding

    def __subclasscheck__(object):
        return issubclass(object, BaseKnight)

      

before Knight

. You don't want this to affect subclasses Knight

, so you also make this a horribly hacky one:

    @classmethod
    def __subclasscheck__(cls, object):
        if cls is Knight:
            return issubclass(object, BaseKnight)
        else:
            return ABCMeta.__subclasscheck__(cls, object)

      




from abc import ABCMeta, abstractmethod

class BaseKnight(metaclass=ABCMeta):  # Python 3 syntax
    def __str__(self):
        return "Sir {} of {}".format(self.name, self.castle)

    @abstractmethod
    def sing():
        pass

      

In the base, then you have a specific Knight

one that redirects isinstance

and issubclass

checks:

class Knight(BaseKnight):
    def __init__(self, name, castle="Camelot"):
        self._name = name

    @abstractmethod
    def sing(self):
        return ["Can't read my,",
                "Can't read my",
                "No he can't read my poker face"]

    @classmethod
    def __subclasscheck__(cls, object):
        if cls is Knight:
            return issubclass(object, BaseKnight)
        else:
            return ABCMeta.__subclasscheck__(cls, object)

      

Finally, some tests:

class KnightOfTheRoundTable(Knight):
    def __init__(self, name):
        self.name = name

    @property
    def castle(self):
        return "Camelot"

    def sing():
        return ["We're knights of the round table",
                "We dance whenever we're able"]

class DuckKnight(BaseKnight):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return "Knight Quacker of Quack"

    def sing():
        return ["Quack!"]

isinstance(KnightOfTheRoundTable("John"), Knight)
#>>> True

isinstance(DuckKnight("Quacker"), Knight)
#>>> True

      

When editing to delegate back to, ABCMeta.__subclasscheck__(cls, object)

I no longer use any of the solutions.

It should be noted that

  • You are probably instantiating Knight

    more than you are using isinstance

    for a particular type Knight

    (before ABC). It makes sense to keep the crazy behavior confined to small spaces.

  • Change isinstance

    means you don't need to change what Knight

    , which means less damage to the codebase.

+2


source







All Articles