Continuing on with my asyncio learnings. Had the opportunity to look at a wrinkle I haven't seen discussed much -- using coroutines with Abstract Base Classes.
First question: can coroutines be abstract? Yes. Yes they can:
from abc import ABC, abstractmethod import asyncio class AbstractBase(ABC): @abstractmethod async def meth1(self): pass class Concrete(AbstractBase): async def meth1(self): print("in concrete") if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(Concrete().meth1()) # prints "in concrete"
Nothing particularly surprising there.
Next question: is an abstract class still uninstantiable? Yes. Yes it is:
>>> AbstractBase() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class AbstractBase with abstract methods meth1
Cool, so far so good.
Ok, next question: what happens if an implementing class doesn't denote an overriding method as a coroutine? Nothing:
class ConcreteButNotCoroutine(AbstractBase): def meth1(self): print("in concrete but not coroutine")
However, this is where it gets a bit grey.
a method, not a coroutine, so it can't be passed to
>>> loop.run_until_complete(ConcreteButNotCoroutine().meth1()) in concrete but not coroutine Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/asyncio/base_events.py", line 547, in run_until_complete future = tasks.ensure_future(future, loop=self) File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/asyncio/tasks.py", line 588, in ensure_future raise TypeError('An asyncio.Future, a coroutine or an awaitable is ' TypeError: An asyncio.Future, a coroutine or an awaitable is required
This is kinda icky, now whomever is using a concrete implementation of
has to know if the specific derived class marked
async or not. This
seems to defeat much of the purpose of using an abstract base class in the first
place as it's kind of a violation of the Liskov Substitution Principle.
The LSP is typically stated as (this is taken from the Wikipedia page):
if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)
That is, you should be able to use a
ConcreteButNotCoroutine instance anywhere you
can use a
Concrete instance without having things go boom boom.
Python's long had a history of being a little lax with the typing (some would call that a feature, some would call it a shortcoming), but this feels a bit unfortunate.
I did some Googling and someone asked this question on Stackoverflow which provides a workaround to check if an implementing class overrides an abstract coroutine with a coroutine, but still feels rather cumbersome. It also still results in a runtime error, just at object instantiation time instead of at method call time.