Setting a descriptor in python3.5 asynchronously

2020-04-10 01:21发布

问题:

I can write a descriptor returning a future which could be awaited on.

class AsyncDescriptor:
    def __get__(self, obj, cls=None):
         # generate some async future here
         return future

    def __set__(self, obj, value):
         # generate some async future here
         return future

class Device:
    attr=AsyncDescriptor()

device=Device()

Now I can get the value in a coroutine with value=await device.attr.

How would I set this attribute?

  • await device.attr=5 -> SyntaxError: can't assign to await expression
  • await setattr(device, 'attr', 5) -> TypeError: object NoneType can't be used in 'await' expression
  • device.attr=5 -> RuntimeWarning: coroutine '__set__' was never awaited

回答1:

What you are trying to do is not possible (with Python 3.5).

While it may be sensible for __get__ to return a Future, making __set__ async is simply not supported by Python 3.5. The return value of __set__ is ignored by Python since there is no "return value" of assignments. And the call to __set__ is always synchronous. Something like a = (b.c = 5) actually raises a SyntaxError as you already noticed.

If async assignments like await device.attr = 5 were allowed, there would probably be a separate protocol for async descriptors, i.e. with coroutines __aget__ and __aset__ as special methods in analogy to async context manager (async with / __aenter__ ) and async iteration (async for / __aiter__). See PEP 492 for the design decisions behind the async / await support.

Also note that __get__ returning a future does not make __get__ a coroutine.

Without further context, it looks like you want to hide something behind the attribute access abstraction provided by the descriptor protocol which should better be done explicitly, but that's up to you of course.



回答2:

await setattr(device,'attr',5) construct is also possible and actually more decent than device.attr = 5, but don't overload the genuine setattr of course.

this one is actually usefull for async threadless while keeping easy readability. running code that set value without await will raise a nice "RuntimeWarning: coroutine attr.__set__ was never awaited"

import asyncio, sys

async def main():

    obj.attr = "not async value"
    print(obj.attr)

    print()
    print("now give set an async value")

    #DO NOT DO THAT use aio.asetattr(obj,'attr',5)
    setattr = aio.asetattr
    # ============== yes i can, thanks python ! ===========
    await setattr(obj,'attr',5)
    # ======================================

    print(obj.attr)

    print("bye")
    flush_io()

def flush_io():
    sys.stdout.flush()
    sys.stderr.flush()

class attr:
    def __init__(self, value):
        self.value = value

    def __get__(self, obj, objtype):
        if obj is None:
            return self
        return self.value

    async def __set__(self, obj, value):
        if value is not obj.__class__:
            print("    async updating", self, end=" ")
            for i in range(value):
                await asyncio.sleep(1)
                print(".", end="")
                flush_io()
            print()
            self.value = value
            print("set", obj, value)
            return
        print("__set__", obj, value)


    def __repr__(self):
        return "<async attr>"

class aobj:

    attr = attr("empty")

    def __repr__(self):
        return "<aobj>"

class aio:

    async def asetattr(self, obj, attr, value):
        await asyncio.sleep(1)
        a_attr = getattr( type(obj), attr)
        await a_attr.__set__(obj, value)
        print("done!")

aio = aio()
obj = aobj()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())


回答3:

but python is great so if you want to write device.attr = 5 but awaiting the set operation, you just can ( i don't say its a good idea, though it may be usefull on threadless python scenario) with the help of a context block:

import asyncio, sys

async def main():

    obj.attr = "not async value"
    print(obj.attr)

    print()
    print("now set with an async value")

    async with aio(obj):
        # ============== yes i can, thanks python ! ===
        obj.attr = 5
        # =============================================

    print(obj.attr)

    print("bye")
    flush_io()

def flush_io():
    sys.stdout.flush()
    sys.stderr.flush()

class attr:
    def __init__(self, value):
        self.value = value

    def __get__(self, obj, objtype):
        if obj is None:
            print("__get__", obj, objtype)
            return self
        print("get", obj, objtype)
        return self.value

    def __set__(self, obj, value):
        if value is not obj.__class__:
            if obj in aio.updating:
                aio.setlist.append([self, value])
                print("    future set", self, value)
                return

            self.value = value
            print("set", obj, value)
            return
        print("__set__", obj, value)

    async def setter(self, value):
        print("    async updating", self, end=" ")
        for i in range(value):
            await asyncio.sleep(1)
            print(".", end="")
            flush_io()
        print()
        self.value = value

    def __repr__(self):
        return "<async attr>"

class aobj:

    attr = attr("empty")

    def __repr__(self):
        return "<aobj>"

class aio:
    updating = []
    setlist = []

    def __call__(self, obj):
        self.updating.append(obj)
        return self

    async def __aenter__(self):
        print("aenter", self.updating)

    async def __aexit__(self, *tb):
        self.updating.pop()
        while len(self.setlist):
            obj, value = self.setlist.pop()
            await obj.setter(value)
        print("aexit")


aio = aio()
obj = aobj()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())