|
| 1 | +Title: Asyncio part 2 - Now More Abstract |
| 2 | +Date: 2018-08-08 11:56 |
| 3 | +Modified: 2018-08-08 11:56 |
| 4 | +Category: Posts |
| 5 | +tags: python,ptotd,asyncio,coroutine,abstract,abc |
| 6 | +cover: static/imgs/python-logo-master-v3-TM.png |
| 7 | +summary: What happens when you mix coroutines with Abstract Base Classes? |
| 8 | + |
| 9 | +Continuing on with my asyncio learnings. Had the opportunity to look at a wrinkle I haven't seen discussed |
| 10 | +much -- using coroutines with [Abstract Base Classes](https://docs.python.org/3/library/abc.html). |
| 11 | + |
| 12 | +First question: can coroutines be abstract? Yes. Yes they can: |
| 13 | + |
| 14 | +```python |
| 15 | +from abc import ABC, abstractmethod |
| 16 | + |
| 17 | +import asyncio |
| 18 | + |
| 19 | + |
| 20 | +class AbstractBase(ABC): |
| 21 | + @abstractmethod |
| 22 | + async def meth1(self): |
| 23 | + pass |
| 24 | + |
| 25 | + |
| 26 | +class Concrete(AbstractBase): |
| 27 | + async def meth1(self): |
| 28 | + print("in concrete") |
| 29 | + |
| 30 | +if __name__ == "__main__": |
| 31 | + loop = asyncio.get_event_loop() |
| 32 | + loop.run_until_complete(Concrete().meth1()) # prints "in concrete" |
| 33 | +``` |
| 34 | + |
| 35 | +Nothing particularly surprising there. |
| 36 | + |
| 37 | +Next question: is an abstract class still uninstantiable? Yes. Yes it is: |
| 38 | + |
| 39 | +```python |
| 40 | +>>> AbstractBase() |
| 41 | +Traceback (most recent call last): |
| 42 | + File "<stdin>", line 1, in <module> |
| 43 | +TypeError: Can't instantiate abstract class AbstractBase with abstract methods meth1 |
| 44 | +``` |
| 45 | + |
| 46 | +Cool, so far so good. |
| 47 | + |
| 48 | +Ok, next question: what happens if an implementing class doesn't denote an overriding |
| 49 | +method as a coroutine? Nothing: |
| 50 | + |
| 51 | +```python |
| 52 | +class ConcreteButNotCoroutine(AbstractBase): |
| 53 | + def meth1(self): |
| 54 | + print("in concrete but not coroutine") |
| 55 | +``` |
| 56 | + |
| 57 | +However, this is where it gets a bit grey. `meth1()` on `ConcreteButNotCoroutine` is |
| 58 | +a method, not a coroutine, so it can't be passed to `run_until_complete()`: |
| 59 | + |
| 60 | +```python |
| 61 | +>>> loop.run_until_complete(ConcreteButNotCoroutine().meth1()) |
| 62 | +in concrete but not coroutine |
| 63 | +Traceback (most recent call last): |
| 64 | + File "<stdin>", line 1, in <module> |
| 65 | + 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 |
| 66 | + future = tasks.ensure_future(future, loop=self) |
| 67 | + 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 |
| 68 | + raise TypeError('An asyncio.Future, a coroutine or an awaitable is ' |
| 69 | +TypeError: An asyncio.Future, a coroutine or an awaitable is required |
| 70 | +``` |
| 71 | + |
| 72 | +This is kinda icky, now whomever is using a concrete implementation of `AbstractBase` |
| 73 | +has to know if the specific derived class marked `meth1()` as `async` or not. This |
| 74 | +seems to defeat much of the purpose of using an abstract base class in the first |
| 75 | +place as it's kind of a violation of the [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle). |
| 76 | +The LSP is typically stated as (this is taken from the Wikipedia page): |
| 77 | + |
| 78 | +> if S is a subtype of T, then objects of type T may be replaced with objects of |
| 79 | +> type S (i.e. an object of type T may be substituted with any object of a |
| 80 | +> subtype S) without altering any of the desirable properties of the program |
| 81 | +> (correctness, task performed, etc.) |
| 82 | + |
| 83 | +That is, you should be able to use a `ConcreteButNotCoroutine` instance anywhere you |
| 84 | +can use a `Concrete` instance without having things go boom boom. |
| 85 | + |
| 86 | +Python's long had a history of being a little lax with the typing (some would call |
| 87 | +that a feature, some would call it a shortcoming), but this feels a bit unfortunate. |
| 88 | + |
| 89 | +I did some Googling and someone asked [this question on Stackoverflow](https://stackoverflow.com/questions/47555934/how-require-that-an-abstract-method-is-a-coroutine) |
| 90 | +which provides a workaround to check if an implementing class overrides an abstract coroutine |
| 91 | +with a coroutine, but still feels rather cumbersome. It also still results in a runtime error, |
| 92 | +just at object instantiation time instead of at method call time. |
0 commit comments