How can I write asyncio coroutines that optionally act as regular functions?

前端 未结 3 1264
情话喂你
情话喂你 2020-12-02 16:06

I\'m writing a library that I\'d like end-users to be able to optionally use as if its methods and functions were not coroutines.

For example, given this function:

相关标签:
3条回答
  • 2020-12-02 16:33

    You need two functions -- asynchronous coroutine and synchronous regular function:

    @asyncio.coroutine
    def async_gettter():
        return (yield from http_client.get('http://example.com'))
    
    def sync_getter()
        return asyncio.get_event_loop().run_until_complete(async_getter())
    

    magically_determine_if_being_yielded_from() is actually event_loop.is_running() but I strongly don't recommend to mix sync and async code in the same function.

    0 讨论(0)
  • 2020-12-02 16:35

    I agree with Andrew's answer, I just want to add that if you're dealing with objects, rather than top-level functions, you can use a metaclass to add synchronous versions of your asynchronous methods automatically. See this example:

    import asyncio
    import aiohttp
    
    class SyncAdder(type):
        """ A metaclass which adds synchronous version of coroutines.
    
        This metaclass finds all coroutine functions defined on a class
        and adds a synchronous version with a '_s' suffix appended to the
        original function name.
    
        """
        def __new__(cls, clsname, bases, dct, **kwargs):
            new_dct = {}
            for name,val in dct.items():
                # Make a sync version of all coroutine functions
                if asyncio.iscoroutinefunction(val):
                    meth = cls.sync_maker(name)
                    syncname = '{}_s'.format(name)
                    meth.__name__ = syncname
                    meth.__qualname__ = '{}.{}'.format(clsname, syncname)
                    new_dct[syncname] = meth
            dct.update(new_dct)
            return super().__new__(cls, clsname, bases, dct)
    
        @staticmethod
        def sync_maker(func):
            def sync_func(self, *args, **kwargs):
                meth = getattr(self, func)
                return asyncio.get_event_loop().run_until_complete(meth(*args, **kwargs))
            return sync_func
    
    class Stuff(metaclass=SyncAdder):
        @asyncio.coroutine
        def getter(self, url):
            return (yield from aiohttp.request('GET', url))
    

    Usage:

    >>> import aio, asyncio
    >>> aio.Stuff.getter_s
    <function Stuff.getter_s at 0x7f90459c2bf8>
    >>> aio.Stuff.getter
    <function Stuff.getter at 0x7f90459c2b70>
    >>> s = aio.Stuff()
    >>> s.getter_s('http://example.com')
    <ClientResponse(http://example.com) [200 OK]>
    <CIMultiDictProxy {'ACCEPT-RANGES': 'bytes', 'CACHE-CONTROL': 'max-age=604800', 'DATE': 'Mon, 11 May 2015 15:13:21 GMT', 'ETAG': '"359670651"', 'EXPIRES': 'Mon, 18 May 2015 15:13:21 GMT', 'SERVER': 'ECS (ewr/15BD)', 'X-CACHE': 'HIT', 'X-EC-CUSTOM-ERROR': '1', 'CONTENT-LENGTH': '1270', 'CONTENT-TYPE': 'text/html', 'LAST-MODIFIED': 'Fri, 09 Aug 2013 23:54:35 GMT', 'VIA': '1.1 xyz.com:80', 'CONNECTION': 'keep-alive'}>
    >>> asyncio.get_event_loop().run_until_complete(s.getter('http://example.com'))
    <ClientResponse(http://example.com) [200 OK]>
    <CIMultiDictProxy {'ACCEPT-RANGES': 'bytes', 'CACHE-CONTROL': 'max-age=604800', 'DATE': 'Mon, 11 May 2015 15:25:09 GMT', 'ETAG': '"359670651"', 'EXPIRES': 'Mon, 18 May 2015 15:25:09 GMT', 'SERVER': 'ECS (ewr/15BD)', 'X-CACHE': 'HIT', 'X-EC-CUSTOM-ERROR': '1', 'CONTENT-LENGTH': '1270', 'CONTENT-TYPE': 'text/html', 'LAST-MODIFIED': 'Fri, 09 Aug 2013 23:54:35 GMT', 'VIA': '1.1 xys.com:80', 'CONNECTION': 'keep-alive'}>
    
    0 讨论(0)
  • 2020-12-02 16:48

    Also you can create a simple decorator which makes your function synchronious. This approach can be applied to global functions and to methods.

    An example.

    # the decorator
    def sync(f):
        ASYNC_KEY = 'async'
    
        def f_in(*args, **kwargs):
            if ASYNC_KEY in kwargs:
                async = kwargs.get(ASYNC_KEY)
                del kwargs[ASYNC_KEY]
            else:
                async = True
    
            if async:
                return f(*args, **kwargs)           
            else:
                return asyncio.get_event_loop().run_until_complete(f())
    
        return f_in
    
    # below: the usage    
    @sync
    async def test():
        print('In sleep...')
        await asyncio.sleep(1)
        print('After sleep')    
    
    
    # below: or asyncio.get_event_loop().create_task(test())
    asyncio.get_event_loop().run_until_complete(test()) 
    # and here is your syncronious version
    test(async=False)
    

    Moreover: it probably have sense to create special wrapper class not to pass async to every method call. The example is below.

    class SyncCallerWrapper(object):
        def __init__(self, obj, is_async=True):
            self._obj = obj 
            self._is_async = is_async
    
    
        def __getattr__(self, name):
            def sync_wrapper(obj_attr):
                def f(*args, **kwargs):
                    return asyncio.get_event_loop().run_until_complete(obj_attr(*args, **kwargs))
    
                return f
    
            obj_attr = getattr(self._obj, name)
    
            if not self._is_async and asyncio.iscoroutinefunction(obj_attr):
                return sync_wrapper(obj_attr)           
    
            return obj_attr
    
    
    class C(object):
        async def sleep1(self):
            print('In sleep1...')
            await asyncio.sleep(1)
            print('After sleep1')
    
    
        async def sleep2(self):
            print('In sleep2...')
            await asyncio.sleep(1)
            print('After sleep2')       
    
    
    # you don't want any concurrency in your code
    c_sync = SyncCallerWrapper(C(), is_async=False)
    c_sync.sleep1()
    c_sync.sleep2()
    
    # here you want concurrency: class methods are coroutines
    c_async = SyncCallerWrapper(C(), is_async=True)
    asyncio.get_event_loop().run_until_complete(c_async.sleep1())
    asyncio.get_event_loop().run_until_complete(c_async.sleep2())
    

    To be more elegant you can replace your class with a function (global constructor). Then a user could create class C passing is_async parameter and have desired behaviour: methods will act as regular (is_async=False) or as async functions (is_async=True).

    def C(*args, **kwargs):
        KEY_ISASYNC = 'is_async'
        if KEY_ISASYNC in kwargs:
            is_async = kwargs.get(KEY_ISASYNC)
            del kwargs[KEY_ISASYNC]
        else:
            is_async = False
        return SyncCallerWrapper(_C(*args, **kwargs), is_async=is_async)
    
    # you don't want any concurrency in your code
    c_sync = C(is_async=False)
    c_sync.sleep1()
    c_sync.sleep2()
    
    # here you want concurrency: class methods are coroutines
    c_async = C(is_async=True)
    asyncio.get_event_loop().run_until_complete(c_async.sleep1())
    asyncio.get_event_loop().run_until_complete(c_async.sleep2())
    
    0 讨论(0)
提交回复
热议问题