Zen of Python
18.7K subscribers
1.49K photos
202 videos
38 files
3.7K links
Полный Дзен Пайтона в одном канале

Разместить рекламу: @tproger_sales_bot

Правила общения: https://tprg.ru/rules

Другие каналы: @tproger_channels

Сайт: https://tprg.ru/site

Регистрация в перечне РКН: https://tprg.ru/xZOL
Download Telegram
Pydantic может создать модель с неверными данными

Наличие объекта BaseModel ещё не доказывает, что данные прошли проверку. В Pydantic V2 model_construct() создаёт модель без валидации. Если поле объявлено вложенной моделью, переданный туда словарь сам в неё не превратится.

Метод не отключает вообще всё: значения полей по умолчанию по-прежнему подставляются. Зато extra="forbid" при таком создании не даст привычной ошибки за лишнее поле. Оно будет проигнорировано. Поэтому контракт обычного конструктора нельзя механически переносить на model_construct.

В документации метода его предлагают для уже проверенных или заведомо доверенных данных. Выбирать этот путь только ради скорости тоже рано: в V2 разрыв сократился, а на простых моделях обычная валидация может оказаться быстрее. Прежде чем убрать проверку, измерьте свой сценарий и определите, кто теперь отвечает за корректность вложенных объектов.
bind принимает строку в параметр int

inspect.Signature.bind проверяет, можно ли разложить аргументы по параметрам функции. Аннотация int при этом не превращается в проверку типа:

from inspect import signature

def ship(item: int, *, retries=2):
return item, retries

bound = signature(ship).bind("text")
print(bound.arguments)
bound.apply_defaults()
print(bound.arguments)


Первый вывод: {'item': 'text'}. Второй добавляет 'retries': 2. bind не вставляет пропущенные значения по умолчанию в arguments, для этого нужен apply_defaults(). Саму ship этот код не вызывает.

Так можно разбирать вызовы в декораторе или диспетчере и получать TypeError при несовместимых аргументах. Но проверку значений нужно организовать отдельно. В документации bind есть и bind_partial, который разрешает пропустить обязательные параметры. Для проверки полного вызова это уже слишком мягкий контракт.
Подмена в pytest может закончиться раньше теста

Тест сначала проверяет код с подменённой зависимостью, а затем хочет вызвать настоящий объект. Если все изменения сделаны через обычную фикстуру monkeypatch, автоматический откат произойдёт лишь после завершения теста или использующей её фикстуры.

Сузить срок жизни можно через with monkeypatch.context() as m. Подмены, зарегистрированные через m.setattr и другие методы этого контекста, откатятся при выходе из блока. После него можно проверять поведение с восстановленной зависимостью, не завершая тест.

В примере pytest так ограничивают подмену functools.partial. Это важно: стандартной библиотекой пользуется и сам pytest, поэтому глобальная подмена может сломать тестовый раннер. Короткий контекст уменьшает время воздействия, но не делает любую подмену безопасной. Для своего кода документация рекомендует явную передачу зависимостей, если это возможно.
bisect ждёт ключ, insort ждёт запись

У двух функций из одного модуля разные требования к искомому значению. Если записи отсортированы по числовому полю, в bisect_left передайте число, а в insort саму запись:

from bisect import bisect_left, insort

rows = [("Ada", 10), ("Lin", 30)]
print(bisect_left(rows, 20, key=lambda row: row[1]))
insort(rows, ("Sam", 20), key=lambda row: row[1])
print(rows)


Поиск вернёт 1, а вставка поставит ("Sam", 20) между Ada и Lin. У bisect_left функция key применяется только к элементам списка. У insort она применяется и к вставляемому объекту для поиска позиции, но в список попадает исходный объект.

Параметр key доступен с Python 3.10. Список должен быть уже отсортирован по тому же ключу. И ещё одно ограничение из документации bisect: логарифмический поиск не делает вставку дешёвой. Сдвиг элементов списка по-прежнему занимает O(n).
urljoin спокойно заменит ваш домен

Базовый адрес в первом аргументе не удерживает результат внутри вашего сайта. Второй аргумент может содержать собственный домен даже без схемы:

from urllib.parse import urljoin

print(urljoin(
"https://safe.example/users/",
"//other.example/admin",
))


Получится https://other.example/admin. Сетевой путь //host берёт схему из базы, а домен задаёт свой. Полный абсолютный URL во втором аргументе тоже может заменить адрес. Это штатное поведение, о котором прямо предупреждает документация urljoin.

Если собираете ссылку из пользовательского ввода для перенаправления или серверного запроса, нужна отдельная политика допустимых адресов. urlsplit поможет выделить схему, хост и путь, но сам по себе не проверит, можно ли им доверять. Успешно разобранный URL ещё не означает, что по нему безопасно переходить.
❤1
Цикл в графе: независимые задачи ещё можно выполнить

Задача a ждёт b, а b ждёт a. Полностью упорядочить такой граф нельзя. Но если рядом есть независимая задача free, ошибка цикла не обязана останавливать и её.

У graphlib.TopologicalSorter вызов prepare() поднимет CycleError. После обработки этого исключения можно продолжить через get_ready(): он выдаст узлы, которым не мешает цикл. В примере с a ↔ b и отдельным free доступен только free. Завершив его обработку, вызовите done("free").

Важно различать «готов к запуску» и «выполнен». Именно done разблокирует зависящие узлы. Если после независимой части is_active() вернул False, это не доказывает завершение всего графа: цикл остался. Такой частичный прогресс разрешён контрактом TopologicalSorter с Python 3.9. Сам CycleError сообщает один обнаруженный цикл, а не все сразу.
ChainMap читает все слои, а пишет только в первый

Для конфигурации удобно читать переопределения поверх значений по умолчанию. ChainMap делает это без предварительного слияния словарей:

from collections import ChainMap

defaults = {"timeout": 30}
overrides = {}
config = ChainMap(overrides, defaults)
config["timeout"] = 5
print(overrides, defaults)


Вывод: {'timeout': 5} {'timeout': 30}. Чтение ищет ключ в слоях по очереди, а присваивание меняет только первый словарь. Туда же направлены update и удаление. Ключ, который есть лишь в нижнем слое, удалить через цепочку не получится.

Это живое представление исходных словарей, а не снимок: изменения в них видны через ChainMap. И копирования вложенных объектов здесь тоже нет. Если прочитанное значение оказалось общим списком, его изменение затронет этот список в исходном слое. Правило записи в первый словарь не означает изоляцию всех доступных через него данных.
❤1🌚1
Кто закроет async-генератор после break

Вы читаете поток до первого подходящего элемента и выходите через break. Цикл закончился, но сам по себе этот выход не гарантирует немедленное выполнение finally у async-генератора. Если там освобождается ресурс, его срок жизни лучше задать явно.

В Python 3.10+ для этого есть contextlib.aclosing. Оберните генератор в async with, как на карточке. При выходе из блока менеджер вызовет await generator.aclose(), в том числе после раннего выхода или исключения.

Очистка пройдёт в том же контексте, что и перебор. Это существенно, если она использует контекстные переменные или зависит от ещё работающей задачи. В примере из документации очистка выполняется при выходе из блока, без ожидания сборки мусора.
🔥1
NewType различает ID без новых объектов

Идентификаторы пользователя и заказа могут быть обычными int, но случайно поменять их местами всё равно ошибка. UserId = NewType("UserId", int) позволяет описать это различие для статического анализатора.

Функция с параметром UserId принимает UserId(42), а на голое 42 проверяющий должен пожаловаться. В обратную сторону преобразование не нужно: UserId допустим там, где ожидается int. После арифметики вроде UserId(5) + 1 статический тип результата снова int.

Во время выполнения вызов возвращает тот же объект. Это отдельный тип для проверяющего, а не валидатор входных данных и не runtime-подкласс. Использовать его вторым аргументом isinstance нельзя. Проверку внешнего ID всё ещё придётся написать.
match в pytest ищет регулярное выражение

Тест с pytest.raises(ValueError, match="bad.file") может пройти на сообщении badXfile. Точка здесь означает любой символ, а не буквальную точку в имени файла. И совпадать со всем сообщением шаблон тоже не обязан.

Параметр match использует re.search. Для буквального фрагмента передайте re.escape("bad.file"). Это защитит метасимволы, но по-прежнему будет искать фрагмент внутри более длинной строки. Если важно полное равенство, отдельно сравните str(exc.value) с ожидаемым текстом.

Ещё одна деталь контракта pytest: поиск учитывает заметки __notes__ из PEP 678. На Python 3.11+ совпадение может найтись в добавленной заметке, хотя его нет в основном сообщении исключения.
❤1
iterator не ограничивает память драйвера

Для одноразового обхода большой выборки у QuerySet есть iterator(). Он не наполняет обычный кеш результатов самого QuerySet. Если выборка уже была вычислена, вызов iterator выполнит запрос ещё раз.

В Django 5.2 его можно сочетать с prefetch_related(), явно передав chunk_size. Более крупная порция уменьшает число запросов для подгрузки связей, но требует больше памяти. При нескольких связях учитывайте ещё и разрастание списка для SQL IN.

Гарантии «в памяти только одна порция» здесь нет. Документация разделяет уровни: MySQL-драйвер загружает весь результат, а потоковое чтение PostgreSQL зависит от серверных курсоров и настройки пула соединений. Проверять расход памяти нужно на конкретном backend.