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

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

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

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

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

Регистрация в перечне РКН: https://tprg.ru/xZOL
Download Telegram
Донхи На и Никита Соболев предложили PEP 841: запись f{1, 2, 3} создаёт frozenset, f{'a': 1} создаёт frozendict, f{} даёт пустой frozendict. Черновик написан 16 июля, обсуждение открыли 20 июля, целятся в Python 3.16.

Сейчас frozenset({1, 2, 3}) сначала собирает обычное множество, потом копирует его в неизменяемое, и на каждом выполнении ищет имя frozenset в области видимости. Соптимизировать это компилятор не может: имя могут переопределить, а у вызова могут быть побочные эффекты. Один частный случай CPython уже спрямляет, но только справа от in. Присвойте тот же литерал переменной, и оптимизация исчезает.

Что предлагает PEP:

🔘 неизменяемость гарантирует сама грамматика, поэтому её не приходится выводить из того, как значение используется дальше;
🔘 константный f{...} сворачивается в один LOAD_CONST и попадает в .pyc, так что на каждом следующем выполнении конструирование бесплатно;
🔘 добавляются токен FLBRACE, четыре узла AST и две инструкции байткода: BUILD_FROZENSET и BUILD_FROZENMAP;
🔘 работают и генераторные выражения: f{x for x in xs}, f{k: v for k, v in items}, а также распаковка f{*xs} и f{**d};
🔘 f{ сегодня синтаксическая ошибка, поэтому старый код ничего не теряет. Запись f {1} с пробелом ошибкой и останется, путаницы с f-строками нет;
🔘 в стандартной библиотеке авторы насчитали около 105 вызовов frozenset и 65 вызовов frozendict, из них 46 и 22 можно переписать новой записью.

Большая цель за синтаксисом — подтолкнуть людей к неизменяемым контейнерам перед эпохой сборок без GIL и субинтерпретаторов: субинтерпретаторы уже делят между собой frozenset, на очереди frozendict. Про JIT авторы намеренно ничего не обещают, потому что руководящий совет в июне запретил новую разработку JIT в main до принятия отдельного PEP.

Читается ли f{1, 2, 3} лучше, чем frozenset({1, 2, 3}), или лишний префикс только запутает?

@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
12🔥2😍2
Сервис убивало по нехватке памяти каждые несколько часов, но утечки в нём не было

RSS рос ступенями: 620 МБ, 890 МБ, 1,4 ГБ, 2,3 ГБ, 3,1 ГБ. Трафик, загрузка процессора и p99 при этом стояли ровно. Число объектов Python не увеличивалось, ни разбухшего словаря, ни списка, ни кэша найти не удалось. gc.collect() честно собирал циклический мусор, а RSS после него почти не опускался. Сакшам Шарма разобрал этот случай 8 июля.

Дело в разделении обязанностей. Сборщик мусора решает, какие объекты ещё достижимы, а аллокатор решает, вернутся ли освободившиеся страницы операционной системе. Освобождённый объект просто уходит обратно в кучу процесса, и пока в той же странице памяти лежит хоть один живой объект, отдать её ядру нельзя. У glibc malloc к этому добавляются кэш освобождённых блоков tcache и отдельные арены на каждый поток, так что многопоточный сервис держит несколько независимых запасов. В одном из примеров автора живые объекты занимали около 700 МБ при RSS около 2,8 ГБ.

🔘 признак, отличающий это от настоящей утечки: tracemalloc показывает стабильный объём, а RSS продолжает расти;
🔘 лечение обошлось без единой правки в коде Python, jemalloc подключается через LD_PRELOAD;
🔘 в продакшене к нему добавили PYTHONMALLOC=malloc и MALLOC_CONF=narenas:2,background_thread:true;
🔘 автор сообщает примерно о вдвое меньшем потреблении оперативной памяти после перехода;
🔘 проверить гипотезу можно и без замены аллокатора, через MALLOC_ARENA_MAX=2 и вызов malloc_trim(0).

Оговорка автора: malloc не универсально лучше встроенного pymalloc, на миллионах мелких объектов выигрыш может оказаться обратным, поэтому замену нужно мерить на своей нагрузке.

Приходилось ловить рост RSS при стабильном числе объектов, и чем в итоге объяснялось?

@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
uv 0.12.0: uv init теперь создаёт проект со сборочной конфигурацией

Astral выпустила uv 0.12.0 28 июля, первый крупный релиз после 0.11.0 в марте. Изменения касаются дефолтов инициализации, проверки хэшей и разбора архивов пакетов.

Раньше uv init создавал плоскую структуру без сборочной системы, и сборочную конфигурацию со структурой src приходилось добавлять отдельно. Теперь проект сразу пригоден для сборки.

🔘 uv init прописывает [build-system] на uv_build, кладёт код в src/<name> и добавляет точку входа в [project.scripts], прежнее поведение возвращает флаг --no-package;
🔘 отклоняются исходные дистрибутивы в форматах .tar.bz2 и .tar.xz, по PEP 625 допустим .tar.gz, старые .zip пока принимаются ради совместимости; заодно запрещены записи со сжатием bzip2 и LZMA внутри wheel;
🔘 wheel, способный перезаписать сам интерпретатор (файлы вида Python, python.py, Python.exe, записи в .data/scripts), больше не устанавливается;
🔘 режим предрелизов по умолчанию сменился с if-necessary-or-explicit на if-necessary, то есть предрелизная версия ставится только когда без неё зависимости не разрешаются;
🔘 директива --require-hashes внутри requirements.txt действительно включает проверку хэшей, а хэши только по MD5 отвергаются, нужен минимум SHA-256;
🔘 строже валидируется pylock.toml: обязателен массив packages, имя файла допустимо только вида pylock.toml или pylock.dev.toml, проверяется размер артефактов.

Авторы пишут, что большинству обновление не потребует правок. Исключение составляют проекты, где зафиксирована верхняя граница сборочного бэкенда: там нужно поднять до uv_build>=0.11.32,<0.13.

У вас в CI uv закреплён конкретной версией или подтягивается последний?

@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
Rust научился ускорять операции с плавающей точкой, сохраняя контроль за точностью

Если вы держите Rust под рукой для кусков, где Python не хватает скорости, в Rust 1.98 появился повод присмотреться. Новый API разрешает компилятору агрессивнее оптимизировать операции с плавающей точкой, но сохраняет контроль за точностью округления. Это значит, что Rust-код, который вызывается из Python, может считать быстрее без привычного компромисса «скорость против корректности».

Раньше в Rust не было стабильного способа управлять этим компромиссом. Теперь можно явно указать компилятору, где безопасно ускорять вычисления, а где важна минимальная погрешность.

Разобрался Itamar Turner-Trauring: если пишете Rust-расширения для Python или интересуетесь, откуда берётся скорость в числовых библиотеках, загляните.
1👍1