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?
source to share
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.
source to share
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 usingisinstance
for a particular typeKnight
(before ABC). It makes sense to keep the crazy behavior confined to small spaces. -
Change
isinstance
means you don't need to change whatKnight
, which means less damage to the codebase.
source to share