I have the following code to compare a base class' current (empty) implementation of required functions to its sub-classes, which must implement them in some different way in order to be considered acceptable at runtime. Without using a metaclass=ABCMeta
and implementing @abstractmethod
decorators on these base class methods, how do I go about doing this? For now, I'm writing the following __init_subclass__
hook on my ad-hoc, metaclass-less abstract base classes in multiple places in my project, but it feels wrong.
import inspect
class AbstractThing:
def __init__(self, topic: str, thing: Thing):
thing.subscriptions[topic] = self.on_message
thing.on_connected.append(self.on_connected)
thing.on_disconnected.append(self.on_disconnected)
def __init_subclass__(cls):
required_methods = ['on_connected', 'on_disconnected', 'on_message']
for f in required_methods:
func_source = inspect.getsourcelines(getattr(cls, f))
# if this class no longer inherits from `Object`, the method resolution order will have updated
parent_func_source = inspect.getsourcelines(getattr(cls.__mro__[-2], f))
if func_source == parent_func_source:
raise NotImplementedError(f"You need to override method '{f}' in your class {cls.__name__}")
def on_connected(self, config: dict):
pass
def on_disconnected(self):
pass
def on_message(self, msg: str):
pass
Is there a better way to do this? Bonus points if I can get typechecking errors in my editor while defining sub classes of this AbstractThing
.
Indeed, you should not rely on inspect.getsourcelines
for any code that should be used in serious contexts (i.e. outside experimentation realm, or tools to deal with the source-code itself)
The plain and simple is
operator is enough to check if the method in a given class is the same as in the base class. (In Python 3. Python 2 users have to take care that methods are retrieved as unbound methods
instead of the raw-functions)
Other than that, you are taking several unneeded turns to get to the base-class itself - the little documented and little used special variable __class__
can help you with that: it is an automatic reference to the class body where it is written (do not mistake with self.__class__
which is a reference to the sub-class instead).
From the documentation:
This class object is the one that will be referenced by the zero-argument form of super(). __class__
is an implicit closure reference created by the compiler if any methods in a class body refer to either __class__
or super
. This allows the zero argument form of super()
to correctly identify the class being defined based on lexical scoping, while the class or instance that was used to make the current call is identified based on the first argument passed to the method.
So, while keeping your main approach, the whole thing can be quite simpler:
def __init_subclass__(cls):
required_methods = ['on_connected', 'on_disconnected', 'on_message']
for f in required_methods:
if getattr(cls, f) is getattr(__class__, f):
raise NotImplementedError(...)
If you have a complex hierarchy, and will have parent classes with other mandatory methods that the subclasses of those will have to implement - and therefore, can't hard code the needed methods in the required_methods
, you can still use the abstractmethod
decorator from abc
, without using the ABCMeta
metaclass. All the decorator does is to create an attribute on the method that is checked on the metaclass. Just make the same check in a __init_subclass__
method:
from abc import abstractmethod
class Base:
def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
for attr_name in dir(cls):
method = getattr(cls, attr_name)
if (getattr(method, '__isabstractmethod__', False) and
not attr_name in cls.__dict__):
# The second condition above allows
# abstractmethods to exist in the class where
# they are defined, but not on further subclasses
raise NotImplementedError(...)
class NetworkMixin(Base):
@abstractmethod
def on_connect(self):
pass
class FileMixin(Base):
@abstractmethod
def on_close(self):
pass
class MyFileNetworkThing(NetworkMixin, FileMixin):
# if any of the two abstract methods is not
# implemented, Base.__init_subclass__ will fail
Keep in mind this just checks for methods that show up in a class' dir
. But customizing __dir__
is used seldon enough for it to be reliable - just take care to document that.