Почему python asyncio loop.call_soon перезаписывает данные? - PullRequest
0 голосов
/ 11 мая 2018

Я создал сложную для отслеживания ошибку в нашем коде, но не понимаю, почему она возникает.Эта проблема возникает, если несколько раз нажать одну и ту же асинхронную функцию для быстрого вызова.Это не происходит с синхронными функциями.

Вот работающий пример проблемы:

import asyncio
import sys

class TestObj(object):

    def __init__(self):

        self.test_data = {'a': 1, 'b': 2, 'c': 3}
        self.loop = asyncio.get_event_loop()
        self.loop.call_later(1, lambda: asyncio.ensure_future(self.calling_func()))
        self.loop.call_later(2, self.calling_func_sync)
        self.loop.call_later(4, sys.exit)
        self.loop.run_forever()

    async def do_something(self, k, v):
        print("Values", k, v)

    async def calling_func(self):
        for k, v in self.test_data.items():
            print("Sending", k, v)
            self.loop.call_soon(lambda: asyncio.ensure_future(self.do_something(k, v)))

    def do_something_sync(self, k, v):
        print("Values_sync", k, v)

    def calling_func_sync(self):
        for k, v in self.test_data.items():
            print("Sending_sync", k, v)
            self.loop.call_soon(self.do_something_sync, k, v)


if __name__ == "__main__":
    a = TestObj()

Вывод:

Sending a 1
Sending b 2
Sending c 3
Values c 3
Values c 3
Values c 3
Sending_sync a 1
Sending_sync b 2
Sending_sync c 3
Values_sync a 1
Values_sync b 2
Values_sync c 3

Почему это происходит иЗачем?Только асинхронная функция наваливается.Я ожидал бы, что каждый вызов call_soon помещает новый указатель в стек, но кажется, что есть указатель на self.do_something, который перезаписывается.

Ответы [ 3 ]

0 голосов
/ 11 мая 2018

Ваша проблема на самом деле не имеет ничего общего с asyncio.k и v в lambda: asyncio.ensure_future(self.do_something(k, v)) по-прежнему относятся к переменным в вашей внешней области видимости.Их значения изменяются к тому времени, когда вы вызываете свою функцию:

i = 1
f = lambda: print(i)

f()  # 1
i = 2
f()  # 2

Распространенным решением является определение вашей функции и (ab) использование аргументов по умолчанию для создания локальной переменной для вашей функции, которая содержит значение i во время создания функции, а не при вызове:

i = 1
f = lambda i=i: print(i)

f()  # 1
i = 2
f()  # 1

Вы можете использовать f = lambda x=i: print(x), если вас смущает наименование.

0 голосов
/ 12 мая 2018

В дополнение к правильным объяснениям других относительно ошибки в lambda, также обратите внимание, что вам даже не нужен lambda. Поскольку do_something является сопрограммой, простое обращение к нему не выполнит никакого кода до следующей итерации цикла событий, поэтому вы автоматически получите эффект call_soon. (Это аналогично тому, как вызов функции генератора не начинает ее выполнять, пока вы не начнете исчерпывать возвращенный итератор.)

Другими словами, вы можете заменить

self.loop.call_soon(lambda: asyncio.ensure_future(self.do_something(k, v)))

с более простым

self.loop.create_task(self.do_something(k, v))

create_task предпочтительнее , чем ensure_future, когда вы имеете дело с сопрограммой.

0 голосов
/ 11 мая 2018

Это не имеет ничего общего с асинхронным кодом, но с lambda, который вы создаете в своем цикле.Когда вы пишете lambda: asyncio.ensure_future(self.do_something(k, v)), вы создаете замыкание, которое обращается к переменным k и v из окружающего пространства имен (и тоже self, но это не проблема).Когда вызывается лямбда-функция, она будет использовать значения, связанные этими именами во внешней области во время вызова, , а не значения, которые они имели при определении лямбды.Поскольку k и v изменяют значение на каждой итерации цикла, это приводит к тому, что все лямбда-функции видят одинаковые значения (последние).

Распространенным способом избежать этой проблемы являетсятекущие значения переменных значения по умолчанию для аргументов лямбда-функции:

self.loop.call_soon(lambda k=k, v=v: asyncio.ensure_future(self.do_something(k, v)))
...