Ловушка замыканий: Почему ваши лямбды в цикле сломаны (Late Binding)
Вы пишете код, который генерирует список функций (например, колбэки для кнопок в UI или динамические фильтры). Кажется, что все логично, но на выходе получаете сюрприз.
Проблемный код:
Почему так происходит?
Это называется Late Binding (позднее связывание).
В Python замыкания (closures) захватывают переменные по ссылке, а не по значению.
Когда вы объявляете
К моменту, когда вы начинаете вызывать функции из списка
Как лечить?
Есть два каноничных способа заставить Python запомнить значение «здесь и сейчас».
1. Аргумент по умолчанию (Hack way)
Значения аргументов по умолчанию вычисляются в момент определения функции.
Это работает быстро, но выглядит немного грязно и может сбить с толку линтеры или коллег.
2.
Более чистый и явный способ.
Где это стреляет в реальной жизни?
- Генерация
- Динамическое создание
- Патчинг тестов в циклах.
Не дайте переменным пережить свое время.
#python #internals #functionalprogramming #gotchas
📲 Мы в MAX
👉@BookPython
Вы пишете код, который генерирует список функций (например, колбэки для кнопок в UI или динамические фильтры). Кажется, что все логично, но на выходе получаете сюрприз.
Проблемный код:
# Хотим создать 3 функции, которые возвращают 0, 1 и 2 соответственно
funcs = []
for i in range(3):
funcs.append(lambda: i)
# Проверяем
results = [f() for f in funcs]
print(results)
# Ожидание: [0, 1, 2]
# Реальность: [2, 2, 2]
Почему так происходит?
Это называется Late Binding (позднее связывание).
В Python замыкания (closures) захватывают переменные по ссылке, а не по значению.
Когда вы объявляете
lambda: i, Python не сохраняет текущее число 0, 1 или 2. Он сохраняет инструкцию: «когда меня вызовут, пойди в локальную область видимости, найди переменную с именем i и возьми ее значение».К моменту, когда вы начинаете вызывать функции из списка
results, цикл for уже завершился. Переменная i в этой области видимости навсегда осталась равной 2. Все три лямбды смотрят на одну и ту же переменную i.Как лечить?
Есть два каноничных способа заставить Python запомнить значение «здесь и сейчас».
1. Аргумент по умолчанию (Hack way)
Значения аргументов по умолчанию вычисляются в момент определения функции.
funcs = []
for i in range(3):
# i=i создает локальную переменную i внутри функции
# и присваивает ей текущее значение i из цикла
funcs.append(lambda i=i: i)
Это работает быстро, но выглядит немного грязно и может сбить с толку линтеры или коллег.
2.
functools.partial (Enterprise way)Более чистый и явный способ.
partial создает новый callable-объект, «замораживая» переданные аргументы.
from functools import partial
funcs = []
for i in range(3):
# Здесь значение i фиксируется жестко
funcs.append(partial(lambda x: x, i))
Где это стреляет в реальной жизни?
- Генерация
command для кнопок в Tkinter/PyQt.- Динамическое создание
task в asyncio циклах.- Патчинг тестов в циклах.
Не дайте переменным пережить свое время.
#python #internals #functionalprogramming #gotchas
👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3❤2🔥2
Protocol vs ABC: Утиная типизация на стероидах (Static Duck Typing)
В классическом ООП (Java, C#) и при использовании
Это создает жесткую связность (coupling): ваша реализация должна знать об интерфейсе и импортировать его.
С приходом
В чем суть?
Если класс имеет метод
Сравним код:
❌ Старый путь (ABC):
✅ Новый путь (Protocol):
Киллер-фича: Retroactive Abstraction (Ретроактивная абстракция)
Представьте, что вы используете стороннюю библиотеку (например,
С помощью
Нюансы для Middle+:
1. Runtime: По умолчанию
2. Свойства: В протоколе можно описывать не только методы, но и поля через
Используйте Протоколы, чтобы развязать зависимости между модулями. Это основа принципа Dependency Inversion в Python.
#python #typing #mypy #architecture #clean_code
📲 Мы в MAX
👉@BookPython
В классическом ООП (Java, C#) и при использовании
abc.ABC в Python мы привыкли к Nominal Subtyping (Именная подтипизация). Чтобы объект считался Bird, он должен явно наследоваться от Bird.Это создает жесткую связность (coupling): ваша реализация должна знать об интерфейсе и импортировать его.
С приходом
typing.Protocol (Python 3.8+) мы получили Structural Subtyping (Структурная подтипизация).В чем суть?
Если класс имеет метод
quack(), то это Утка. Неважно, от чего он наследуется. Это и есть та самая «утиная типизация», но теперь поддерживаемая статическим анализатором (mypy, pyright, IDE).Сравним код:
❌ Старый путь (ABC):
from abc import ABC, abstractmethod
# 1. Жестко определяем интерфейс
class SenderABC(ABC):
@abstractmethod
def send(self, msg: str) -> None: pass
# 2. Обязаны наследоваться!
class EmailService(SenderABC):
def send(self, msg: str) -> None:
print(f"Email: {msg}")
def alert(sender: SenderABC):
sender.send("Alert!")
✅ Новый путь (Protocol):
from typing import Protocol
# 1. Описываем, "что мы ждем от объекта"
class SenderProto(Protocol):
def send(self, msg: str) -> None: ...
# 2. Реализация НИЧЕГО не знает про Protocol
# Никаких импортов и наследования!
class SmsService:
def send(self, msg: str) -> None:
print(f"SMS: {msg}")
# Mypy счастлив: SmsService имеет нужную структуру (метод send)
def alert(sender: SenderProto):
sender.send("Alert!")
alert(SmsService())
Киллер-фича: Retroactive Abstraction (Ретроактивная абстракция)
Представьте, что вы используете стороннюю библиотеку (например,
boto3 или клиент Redis). Вы не можете заставить их классы наследоваться от ваших ABC.С помощью
Protocol вы можете создать интерфейс для уже существующего чужого кода, не меняя его, и типизировать свои функции.Нюансы для Middle+:
1. Runtime: По умолчанию
isinstance(obj, MyProtocol) выбросит ошибку. Протоколы - это compile-time фича. Если нужна проверка в рантайме, декорируйте протокол @runtime_checkable.2. Свойства: В протоколе можно описывать не только методы, но и поля через
@property или просто аннотации типов.Используйте Протоколы, чтобы развязать зависимости между модулями. Это основа принципа Dependency Inversion в Python.
#python #typing #mypy #architecture #clean_code
📲 Мы в MAX
👉@BookPython
👍5🔥1