Все callbacks Python используют последний ID: как связать значение в цикле
Планировщик собирает список функций для отложенной обработки трёх заказов. На этапе создания всё выглядит правильно: цикл проходит по трём идентификаторам. Когда очередь вызывает функции, каждая обрабатывает последний заказ. Между созданием callback и его исполнением прошло достаточно времени, чтобы переменная цикла изменилась.
Почему значение меняется позже
Замыкание сохраняет доступ к переменной внешней области видимости. Оно не обязано сохранять отдельную копию её текущего значения при каждом создании функции. Внутри одной внешней функции все лямбды из цикла обращаются к одной ячейке. После окончания цикла в ней остаётся последнее значение. Потоки и asyncio здесь не нужны: ошибка воспроизводится при последовательном запуске списка функций.
Первый шаг диагностики — вынести исполнение за пределы цикла. Если callback немедленно вызывается на каждой итерации, он ещё успевает увидеть ожидаемое значение, и тест скрывает дефект. Именно отложенность отличает очередь заданий, регистрацию маршрутов или построение команд интерфейса от обычного вычисления в цикле.
Фабрика функций создаёт новую область видимости для каждого идентификатора. Параметр фабрики становится отдельной переменной конкретного вызова, а возвращённая функция сохраняет доступ к ней. В примере мы сравниваем сломанный набор и исправленный, после чего проверяем бизнес-результат: каждый заказ получает собственную запись. Исключительно проверка длины списка callbacks не заметила бы проблему.
Создать отдельную привязку
def make_broken(ids):
return [lambda: f"order:{order_id}" for order_id in ids]
def bind_order(order_id):
def run():
return f"order:{order_id}"
return run
ids = [17, 29, 42]
broken = make_broken(ids)
fixed = [bind_order(order_id) for order_id in ids]
wrong = [job() for job in broken]
result = [job() for job in fixed]
assert wrong == ["order:42"] * 3
assert result == ["order:17", "order:29", "order:42"]
assert len(set(result)) == len(ids)
print("broken:", wrong)
print("fixed:", result)
Аргумент по умолчанию тоже вычисляется при создании функции, поэтому запись lambda order_id=order_id: ... подходит для небольшого callback. Однако она добавляет параметр в публичную сигнатуру. Если диспетчер передаст туда позиционное значение, сохранённый идентификатор будет заменён. Фабрика с внутренней функцией без аргументов яснее выражает контракт «запустить заранее выбранный заказ».
Что именно сохраняет callback
Ещё один вариант — functools.partial с обычной функцией run_order. Он связывает аргумент без написания вложенного def и удобен, когда функция уже существует. Выбирайте его по читаемости места регистрации. Для метода класса существуют отдельные правила привязки, поэтому перенос partial на уровень атрибута класса требует внимания к self.
Ни фабрика, ни аргумент по умолчанию не копируют изменяемый объект. Если вместо числа передать один общий словарь заказа и затем менять его содержимое, все callbacks ссылаются на тот же словарь. Чтобы зафиксировать данные, выберите неизменяемый идентификатор или явно сформируйте снимок нужных полей. Не копируйте весь граф объектов автоматически: задача очереди обычно должна хранить небольшой устойчивый ключ.
Проверьте поведение после окончания регистрации, затем измените исходный список и вызовите функции ещё раз. Для числовых идентификаторов прежние callbacks сохранят выбранные значения. Если отложенная работа должна читать актуальное состояние заказа, пусть она получает его из хранилища по этому ключу при запуске. Так разделяются два решения: какой объект обрабатывать и насколько свежими должны быть его данные.
В рабочем проекте найдите lambda и вложенные def внутри циклов регистрации. Для каждого места спросите, когда реально вызывается функция. Тест с двумя разными ID и исполнением после цикла часто обнаруживает дефект лучше, чем большой сценарий с единственным заказом.
Продолжить практику
В «Python для профессионалов» вы продолжите работу с функциями, декораторами и functools. Откройте программу курса и разберите, как область видимости и связанное состояние влияют на отложенный вызов: это основа читаемых обработчиков и генераторов поведения.
Продолжить на практике
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
