Tworoutines in Python
threespeedlogic.com
threespeedlogic.com
I work in an ecosystem that has both Python 2 and Python 3.6 async applications, so my code has to play nice with both. I often end up writing APIs as such:
from __future__ import [Python 3 niceties]
class BaseApi(object):
def _method_that_does_io(self, *args, **kwargs):
raise NotImplementedError
def do_a_thing(self, stuff):
return self._method_that_does_io(stuff)
and then create a SyncAPI and AsyncAPI respectively in different files, each subclassing BaseApi implementing the respective io-bound helper-methods.So Python 2 people can do:
from SomeAPI.sync import SomeAPI
and Python 3 people can run: from SomeAPI.py3async import SomeAPI
The synchronous code is written in such a manner it's both 2 and 3 compatible, so every flavor can be happy.Let’s say we are making http requests, with the sync code using requests and the async code using aiohttp.
Your public methods all call a private _request method with a verb, a url, and optionally something to be seriaized to json.
Your async _request method does need to do a small bit of code around sessions: make one first if one hasn’t been made for the class instance yet (or override your __init__ and take an existing one as an arg) but at the end of the day it’s just returning a coroutine that’ll ultimately give you a deserialized response. Sync can just be more direct and return a dict. Both have the same signature.
You can do it, but I personally think it's messy. It's what I really dislike about Python's async model, which you almost necessarily encounter because the stdlib is mostly not async.
class Api(AsyncApi):
def __init__(self, token):
try:
self._loop = asyncio.get_event_loop()
except RuntimeError:
# when running in a thread, get_event_loop doesn't create another one
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._session = aiohttp.ClientSession(loop=self._loop)
super().__init__(token, self._loop, self._session)
@lru_cache(maxsize=None)
def __getattribute__(self, name):
attr = super().__getattribute__(name)
if name.startswith("_") or not asyncio.iscoroutinefunction(attr):
return attr
def call_sync(*args, **kwargs):
coro = attr(*args, **kwargs)
return self._loop.run_until_complete(coro)
return call_sync
It can run when there is no loop or even when a loop is already running.https://github.com/sid-code/nmoo/blob/master/src/nmoo/sidech...
There is a bit of glue code holding this together, but I find it really neat. There's no event loop hack, just static polymorphism.
In asyncio and similar systems, the choice between "is this code syncronous or asyncronous" is made at declaration time - either you define a sync function or an async one, and you can only use it in that way.
In gevent and similar systems, the same choice is made at call time - there's no distinction between sync and async functions, instead you can call syncronously with "foo()" or asyncronously with "future = gevent.spawn(foo)" (I'm taking some liberties by calling the returned greenlet object a future but it can be used as such).
I've gotten good at diagnosing these issues over many years as a heavy gevent user, and it doesn't stop me highly reccomending gevent to anyone who will listen, but it's a caveat that should be mentioned for anyone new starting out.
This is thankfully fairly rare, but it is something you need to be aware of.
Also, wouldn't asyncio probably have similar issues with native modules as well?
Theoretically, you shouldn't need to do that, but in practice there's always some big legacy component that you can't rewrite to make it asynchronous. In my experience it's such a common pattern that the language should support it. But in many languages it ends up being quite complex or inelegant.
Makes me feel like there's a bit of a schism between language designers and users: designers want an architecture that encourages clean code, users want duct tape.
The general idea is that main runs in a fiber (or process in Smalltalk lingo) out of the box. There's a way of starting new fibers, yielding to the next; and blocking calls yield automagically. And that's it.
But talk is cheap, and Smalltalk is not coming back anytime soon; which is why I'm building a new language [0] around the same ideas.
And it needs to be built into the core (as opposed to gevent), otherwise it wont run as fast as it could and can't integrate deeply enough with IO to be seamless.
However this is standard library magic and and user code will indeed run in the context of a goroutine which is NxM threading.
For us it caused no end of problems, and were glad to see it go (well, there are still probably some places using it...).
It's not surprising to me that it's discouraged.
Most of these could be worked around, but some of them are unreasonably difficult given OS event apis.
We replaced it with just making the calls that might need to be asynchronous use `async`/`await`.
https://ipython.readthedocs.io/en/stable/whatsnew/version7.h...
>>> import asyncio
>>> async def foo():
... return 'foo'
...
>>> bar = asyncio.run(asyncio.wait_for(foo(), timeout=15))
>>> print(bar)
foo
>>>https://www.seas.harvard.edu/sites/default/files/files/archi...
Take a look at Lua coroutines