I'm trying to understand the benefits of using abstract base classes. Consider these two pieces of code:
Abstract base class:
from abc import ABCMeta, abstractmethod, abstractproperty
class CanFly:
__metaclass__ = ABCMeta
#abstractmethod
def fly(self):
pass
#abstractproperty
def speed(self):
pass
class Bird(CanFly):
def __init__(self):
self.name = 'flappy'
#property
def speed(self):
return 1
def fly(self):
print('fly')
b = Bird()
print(isinstance(b, CanFly)) # True
print(issubclass(Bird, CanFly)) # True
Plain inheritance:
class CanFly(object):
def fly(self):
raise NotImplementedError
#property
def speed(self):
raise NotImplementedError()
class Bird(CanFly):
#property
def speed(self):
return 1
def fly(self):
print('fly')
b = Bird()
print(isinstance(b, CanFly)) # True
print(issubclass(Bird, CanFly)) # True
As you see, both methods support inflection using isinstance and issubclass.
Now, one difference I know is that, if you try to instantiate a subclass of an abstract base class without overriding all abstract methods/properties, your program will fail loudly. However, if you use plain inheritance with NotImplementedError, your code won't fail until you actually invoke the method/property in question.
Other than that, what makes using abstract base class different?
The most notable answer in terms of concrete specifics, besides what you mentioned in your question, is that the presence of the #abstractmethod or #abstractproperty1 decorators, along with inheriting from ABC (or having the ABCMeta metaclass) prevents you from instantiating the object at all.
from abc import ABC, abstractmethod
class AbsParent(ABC):
#abstractmethod
def foo(self):
pass
AbsParent()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class AbsParent with abstract methods foo
However, there's more at play here. Abstract Base Classes were introduced to Python in PEP 3119. I'd recommend reading through the "Rationale" section for Guido's take on why they were introduced in the first place. My sophomoric summary would be that they're less about their concrete features and more about their philosophy. Their purpose is to signal to external inspectors that the object is inheriting from the ABC, and because it's inheriting from an ABC it will follow a good-faith agreement. This "good-faith agreement" is that the child object will follow the intention of the parent. The actual implementation of this agreement is left up to you, which is why it's a good-faith agreement, and not an explicit contract.
This primarily shows up through the lens of the register() method. Any class that has ABCMeta as its metaclass (or simply inherits from ABC) will have a register() method on it. By registering a class with an ABC you are signaling that it inherits from the ABC, even though it technically doesn't. This is where the good-faith agreement comes in.
from abc import ABC, abstractmethod
class MyABC(ABC):
#abstractmethod
def foo(self):
"""should return string 'foo'"""
pass
class MyConcreteClass(object):
def foo(self):
return 'foo'
assert not isinstance(MyConcreteClass(), MyABC)
assert not issubclass(MyConcreteClass, MyABC)
While MyConcreteClass, at this point is unrelated to MyABC, it does implement the API of MyABC according to the requirements laid out in the comments. Now, if we register MyConcreteClass with MyABC, it will pass isinstance and issubclass checks.
MyABC.register(MyConcreteClass)
assert isinstance(MyConcreteClass(), MyABC)
assert issubclass(MyConcreteClass, MyABC)
Again, this is where the "good-faith agreement" comes into play. You do not have to follow the API laid out in MyABC. By registering the concrete class with the ABC we are telling any external inspectors that we, the programmers, are adhering to the API we're supposed to.
1 note that #abstractproperty is no longer preferred. Instead you should use:
#property
#abstractmethod
def foo(self):
pass
Related
I have a base class that looks something like this:
class myBaseClass:
def __init__(self):
self.name = None # All subclasses must define this
def foo(self): # All subclasses must define this
raise NotImplementedError
def bar(self): # Optional -- not all subclasses will define this
raise NotImplementedError
My API specification stipulates that anyone creating a subclass of myBaseClass must provide a meaningful value for .name, and for the function .foo(). However, .bar() is optional and calling code should be able to handle the case where that results in a NotImplementedError.
When and how should I check that subclasses contributed by third parties meet these requirements?
The options seem to be:
Build subclasses exclusively via metaclasses. However, this approach will be unfamiliar and potentially confusing to most of the contributors to my project, who tend not to be expert developers.
Add an __init_subclass__ method to the base class and use this to infer whether the subclass has overridden everything it is supposed to override. Seems to work, but feels a bit 'kludgy'.
Write build-time tests to instantiate each subclass, call each 'required' method, and verify that they do not raise a NotImplementedError. Seems like an excessive computational effort to answer such a simple question (calling .foo() may be expensive).
Ignore the issue. Deal with it if and when it causes something else to break.
I'm sure I'm not the only person who needs to deal with this issue - is there a 'correct' approach here?
Here's how I would structure it.
First off, what you're looking for here is an abstract base class. Using the built-in modules you can easily define it as such and have methods be forced to have an implementation, otherwise the class will raise an exception when instantiated.
If the name attribute needs to be set always, then you should make it part of the constructor arguments.
Because bar is not always required I wouldn't define it as a method in the base class you have. Instead I would make a child class that is also abstract and define it there as required. When checking to see if the method is available you can use isinstance.
This is what my final code would look like:
from abc import ABC, abstractmethod
class FooBaseClass(ABC):
def __init__(self, name):
self.name = name
#abstractmethod
def foo(self):
"""Some useful docs for foo"""
class FooBarBaseClass(FooBaseClass, ABC):
#abstractmethod
def bar(self):
"""Some useful docs for bar"""
When creating instances you can pick the base class you want and will be forced to define the methods.
class FooClass(FooBaseClass):
def __init__(self):
super().__init__("foo")
def foo(self):
print("Calling foo from FooClass")
class FooBarClass(FooBarBaseClass):
def __init__(self):
super().__init__("foobar")
def foo(self):
print("Calling foo from FooBarClass")
def bar(self):
print("Calling bar from FooBarClass")
Example checking if bar is callable:
def do_operation(obj: FooBaseClass):
obj.foo()
if isinstance(obj, FooBarBaseClass):
obj.bar()
Example:
do_operation(FooClass())
do_operation(FooBarClass())
Calling foo from FooClass
Calling foo from FooBarClass
Calling bar from FooBarClass
An example of invalid code
class InvalidClass(FooBaseClass):
def __init__(self):
super().__init__("foo")
InvalidClass()
Traceback (most recent call last):
File "C:\workspace\so\test.py", line 52, in <module>
InvalidClass()
TypeError: Can't instantiate abstract class InvalidClass with abstract method foo
from abc import ABC, abstractmethod
class A(ABC):
def __init__(self, name):
self.name = name
#abstractmethod
def something():
pass
class B(A):
pass
I'm still new to learning OOP so I would like to ask this. I know that an abstract class is considered a superclass, but can an abstract class be a subclass as well?
Using the code as an example, B inherits from A but does not override the abstract methods in A, so does this mean that B is still considered an abstract class as well as as a subclass of A?
First, what is an abstract class? It is a class that is to be used as a "skeleton" for a subclass.
Now to your question....
So does this mean that B is still considered an abstract class as well as as a subclass of A?
Yes, because all of the methods are not overridden, and they are abstract.
Take this code for example:
from abc import ABC, abstractmethod
class A(ABC):
#abstractmethod
def foo(self):
pass
#abstractmethod
def bar(self):
pass
class B(A):
def foo(self):
return 'Foo implementation'
Here, the class B is still abstract since one of the methods is not overridden that is abstract. So if you try to create an instance of that, you'll get this error:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class B with abstract methods bar
Here we can see that this class inherits from A, but is a concrete class, since the methods are overridden:
class C(A):
def foo(self):
pass
def bar(self):
pass
c = C()
c.foo()
This code runs without errors.
In short, a subclass of an abstract class is still an abstract class as long as the methods are not overridden.
Yes. As long as you don't override all the abstract methods, the subclass is still abstract.
I am trying to create a simple abstract base class Abstract that along with its own methods provides the methods of two others abstract base classes: Publisher and Subscriber. When I try to initialize the concrete class Concrete, built on Abstract I get this error: Cannot create a consistent method resolution order (MRO) for bases ABC, Publisher, Subscriber. What is the right way to do it?
from abc import ABC, abstractmethod
class Publisher(ABC):
subscribers = set()
def register(self, obj):
self.subscribers.add(obj)
def unregister(self, obj):
self.subscribers.remove(obj)
def dispatch(self, event):
print("dispatching", event)
class Subscriber(ABC):
#abstractmethod
def handle_event(self, event):
raise NotImplementedError
class Abstract(ABC, Publisher, Subscriber):
#abstractmethod
def do_something(self, event):
raise NotImplementedError
class Concrete(Abstract):
def handle_event(self, event):
print("handle_event")
def do_something(self, event):
print("do_something")
c = Concrete()
Abstract classes don't have to have abc.ABC in their list of bases. They have to have abc.ABCMeta (or a descendant) as their metaclass, and they have to have at least one abstract method (or something else that counts, like an abstract property), or they'll be considered concrete. (Publisher has no abstract methods, so it's actually concrete.)
Inheriting from ABC is just a way to get ABCMeta as your class's metaclass, for people more comfortable with inheritance than metaclasses, but it's not the only way. You can also inherit from another class with ABCMeta as its metaclass, or specify metaclass=ABCMeta explicitly.
In your case, inheriting from Publisher and Subscriber will already set Abstract's metaclass to ABCMeta, so inheriting from ABC is redundant. Remove ABC from Abstract's base class list, and everything should work.
Alternatively, if you really want ABC in there for some reason, you can move it to the end of the base class list, which will resolve the MRO conflict - putting it first says you want ABC methods to override methods from the other classes, which conflicts with the fact that the other classes are subclasses of ABC.
Change from this:
class Abstract(ABC, Publisher, Subscriber):
To this:
class Abstract(Publisher, Subscriber):
The two subclasses are already abstract, thus you don't need to inherit from ABC again.
I'm trying to figure out how to ensure that a method of a class inheriting from an ABC is created using the appropriate decorator. I understand (hopefully) how ABCs work in general.
from abc import ABCMeta, abstractmethod
class MyABC(metaclass=ABCMeta):
#abstractmethod
def my_abstract_method(self):
pass
class MyClass(MyABC):
pass
MyClass()
This gives "TypeError: Can't instantiate abstract class MyClass with abstract methods my_abstract_method". Great, makes sense. Just create a method with that name.
class MyClass(MyABC):
def my_abstract_method(self):
pass
MyClass()
Boom. You're done. But what about this case?
from abc import ABCMeta, abstractmethod
class MyABC(metaclass=ABCMeta):
#property
#abstractmethod
def my_attribute(self):
pass
class MyClass(MyABC):
def my_attribute(self):
pass
MyClass()
The MyClass() call works even though my_attribute is not a property. I guess in the end all ABCs do is ensure that a method with a given name exists. Thats it. If you want more from it, you have to look at MyABC's source code and read the documentation. The decorators and comments there will inform you of how you need to construct your sub-class.
Do I have it right or am I missing something here?
You're correct that ABCs do not enforce that. There isn't a way to enforce something like "has a particular decorator". Decorators are just functions that return objects (e.g., property returns a property object). ABCMeta doesn't do anything to ensure that the defined attributes on the class are anything in particular; it just makes sure they are there. This "works" without errors:
class MyABC(metaclass=ABCMeta):
#abstractmethod
def my_abstract_method(self):
pass
class MyClass(MyABC):
my_abstract_method = 2
MyClass()
That is, ABCMeta doesn't even ensure that the abstract method as provided on the subclass is a method at all. There just has to be an attribute of some kind with that name,
You could certainly write your own metaclass that does more sophisticated checking to ensure that certain attributes have certain kinds of values, but that's beyond the scope of ABCMeta.
I initially defined the following abstract class:
from abc import ABC, abstractmethod
class Primitive(ABC):
Now I want to create another abstract class that inherits from Primitive:
class InstrumentName(Primitive)
I need this class to be abstract since I ultimately want to create the following two concrete classes:
class CurrencyInstrumentName(InstrumentName)
class MetalInstrumentName(InstrumentName)
I have read the documentation and searched SO, but they mostly pertain to sublcassing concrete classes from abstract classes, or discussing how Python handles abstraction
Just subclass, you don't need to do anything special.
A class only becomes concrete when there are no more abstractmethod and abstractproperty objects left in the implementation.
Let's illustrate this:
from abc import ABC, abstractmethod
class Primitive(ABC):
#abstractmethod
def foo(self):
pass
#abstractmethod
def bar(self):
pass
class InstrumentName(Primitive):
def foo(self):
return 'Foo implementation'
Here, InstrumentName is still abstract, because bar is left as an abstractmethod. You can't create an instance of that subclass:
>>> InstrumentName()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class InstrumentName with abstract methods bar
Subclasses can also add #abstractmethod or #abstractproperty methods as needed.
Under the hood, all subclasses inherit the ABCMeta metaclass that enforces this, and it simply checks if there are any #abstractmethod or #abstractproperty attributes left on the class.
As #MartijnPieters wrote, you don't need to do anything special for Python, but PyCharm will warn:
Class InstrumentName must implement all abstract methods
One way to suppress that warning:
import abc
class Primitive(abc.ABC):
#abc.abstractmethod
def foo(self):
pass
# noinspection PyAbstractClass
class InstrumentName(Primitive):
def is_tuba(self):
return False
Another way:
...
class InstrumentName(Primitive, metaclass=abc.ABCMeta):
def is_tuba(self):
return False