How to handle async and inheritance?

Stephen Rosen sirosen at uchicago.edu
Tue Jun 30 11:59:11 EDT 2020


Hi all,

I'm looking at a conflict between code sharing via inheritance and async
usage. I would greatly appreciate any guidance, ideas, or best practices
which might help.

I'll speak here in terms of a toy example, but, if anyone wants to look at
the real code, I'm working on webargs. [1] Specifically, we have a `Parser`
class and an `AsyncParser` subclass, and the two have a lot of code
duplication to handle async/await. [2]


I've got an inheritance structure like this:

class MyAbstractType: ...
class ConcreteType(MyAbstractType): ...
class AsyncConcreteType(MyAbstractType): ...

One of my goals, of course, is to share code between ConcreteType and
AsyncConcreteType via their parent.
But the trouble is that there are functions defined like this:

class MyAbstractType:
    def foo(self):
        x = self.bar()
        y = self.baz(x)
        ... # some code here, let's say 20 lines

class AsyncConcreteType(MyAbstractType):
    async def foo(self):
        x = await self.bar()
        y = self.baz(x)
        ... # the same 20 lines as above, but with an `await` added
every-other line


I'm aware that I'm looking at "function color" and that my scenario is
pitting two language features -- inheritance and async -- against one
another. But I don't see a clean way out if we want to support an
"async-aware" version of a class with synchronous methods.

What I tried already, which I couldn't get to work, was to either fiddle
with things like `inspect` to see if the current function is async or to
use a class variable to indicate that the current class is the async
version. The idea was to write something like

class MyAbstractType:
    _use_async_calls = False
    def foo(self):
        x = self._await_if_i_am_async(self.bar)
        y = self.baz(x)
        ...

and that way, the async subclass just needs to change signatures to be
async with little stubs and set the flag,

class AsyncConcreteType(MyAbstractType):
    _use_async_calls = True
    async def foo(self):
        return super().foo()
    async def bar(self):
        return super().bar()

but this (some of you are ahead of me on this, I'm sure!) did not work at
all. I couldn't find any way to write `_await_if_i_am_async`, other than
possibly doing some weird things with `exec`.
Once I concluded that the python wouldn't let me decide whether or not to
use await at runtime, at least with tools of which I'm aware, I basically
gave up on that route.


However, it seems like there should be some clever technique for defining
`MyAbstractType.foo` such that it awaits on certain calls *if* there's some
indication that it should do so. It's obviously possible with `exec`, but I
don't want to convert all of the core codepaths into giant `exec` blocks.
Perhaps there's a way which is safer and more maintainable though?


If anyone has experience in this space and can offer up a good solution, I
would love to hear about it.
And if someone wants to go above-and-beyond and look at webargs, and
suggest a better way for us to support aiohttp, I'd obviously welcome that
kind of help as well!

Thanks in advance, and best regards,
-Stephen


[1] https://github.com/marshmallow-code/webargs
[2]
https://github.com/marshmallow-code/webargs/blob/6668d267fa4135cf3f653e422bd168298f2213a8/src/webargs/asyncparser.py#L24


More information about the Python-list mailing list