Ловушка замыканий: Почему ваши лямбды в цикле сломаны (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
👍5❤2
Pytest Patterns: Элегантный Teardown через
Если вы все еще пишете
Фикстуры (fixtures) - это не просто способ передать данные. Это полноценный механизм управления жизненным циклом зависимостей (DI).
1.
В Pytest фикстура может "замереть", отдать управление тесту, а потом продолжить выполнение. Это реализуется через генератор
Код до
Код после
Пример (временная база данных):
Это гарантирует, что ресурсы будут освобождены, и вам не нужны
2. Scopes: Не создавайте мир заново
По умолчанию фикстура имеет
Используйте
Паттерн "Изоляция при общем ресурсе":
Частая задача Middle+: иметь одну БД на весь прогон тестов (быстро), но чистые таблицы для каждого теста (изолированно).
Решение: комбинируем скоупы.
Итог:
🟢 Используйте
🟢 Тяжелые объекты (Engine, Client, Container) - в
🟢 Легкие объекты с состоянием (Session, User) - в
#pytest #testing #qa #bestpractices #python
📲 Мы в MAX
👉@BookPython
yield и оптимизация скоуповЕсли вы все еще пишете
def teardown_method(self): в классах тестов, вы не используете мощь Pytest на 100%.Фикстуры (fixtures) - это не просто способ передать данные. Это полноценный механизм управления жизненным циклом зависимостей (DI).
1.
yield вместо return: Встроенный TeardownВ Pytest фикстура может "замереть", отдать управление тесту, а потом продолжить выполнение. Это реализуется через генератор
yield.Код до
yield - это setUp.Код после
yield - это tearDown.Пример (временная база данных):
import pytest
from sqlalchemy import create_engine
@pytest.fixture
def db_engine():
# Setup: Поднимаем соединение
engine = create_engine("sqlite:///:memory:")
# Передаем объект в тест
yield engine
# Teardown: Этот код выполнится ПОСЛЕ завершения теста
# (даже если тест упал с ошибкой!)
engine.dispose()
Это гарантирует, что ресурсы будут освобождены, и вам не нужны
try/finally блоки внутри самих тестов.2. Scopes: Не создавайте мир заново
По умолчанию фикстура имеет
scope='function'. Она создается и умирает для каждого теста. Это безопасно, но медленно, если мы говорим о поднятии Docker-контейнера или коннекта к БД.Используйте
scope='session' для тяжелых ресурсов, которые можно переиспользовать.Паттерн "Изоляция при общем ресурсе":
Частая задача Middle+: иметь одну БД на весь прогон тестов (быстро), но чистые таблицы для каждого теста (изолированно).
Решение: комбинируем скоупы.
# Живет весь прогон тестов (создается 1 раз)
@pytest.fixture(scope="session")
def db_engine():
engine = create_engine(...)
yield engine
engine.dispose()
# Живет 1 тест (создается N раз)
@pytest.fixture(scope="function")
def db_session(db_engine):
# Берем engine из сессионной фикстуры
connection = db_engine.connect()
transaction = connection.begin() # Начали транзакцию
session = Session(bind=connection)
yield session
session.close()
# ROLLBACK транзакции после теста вернет базу в исходное состояние!
transaction.rollback()
connection.close()
Итог:
yield для очистки ресурсов.scope='session'.scope='function', наследуясь от тяжелых.#pytest #testing #qa #bestpractices #python
👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3❤1
Ранее мы затронули типизацию в фикстурах (косвенно), поэтому сегодня поговорим про:
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
Protocol vs ABC: Утиная типизация на стероидах (Static Duck Typing)
В классическом ООП (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
👍6