На маленьком проекте подмена времени стоит 1406,7 микросекунды против 1,5, на большом — 40 971,3 против тех же 1,5
Адам Джонсон, автор
Стенд простой: генерируются пустые модули по 25 атрибутов, каждый десятый держит ссылки на
🔘 259 модулей, то есть почти голый интерпретатор: 1406,7 мкс против 1,5 мкс, разница в 940 раз;
🔘 1259 модулей, размер небольшого проекта на Django: разница в 2490 раз;
🔘 16 259 модулей, крупный проект с обвесом зависимостей: 40 971,3 мкс против неизменных 1,5 мкс, разница в 27 300 раз;
🔘 время
🔘 в наборе из 2000 тестов с подменой времени это 82 секунды чистой замены ссылок против 3 миллисекунд;
🔘 причина в том, что
У обхода есть и содержательная плата, помимо скорости: он не находит ссылки в атрибутах классов, значениях по умолчанию, замыканиях и C-расширениях, и они продолжают отдавать настоящее время. Плюс подставленные объекты видны по типу,
Автор оговаривает, что модули в стенде синтетические, это
Полная статья: https://adamj.eu/tech/2026/08/03/python-time-machine-o1-freezegun-on/
@zen_of_python
Адам Джонсон, автор
time-machine, замерил 3 августа, как две библиотеки подмены текущего времени ведут себя с ростом проекта. Прошлый такой замер он делал в 2021 году и получил разницу в 100–200 раз, теперь захотел показать не отношение, а саму зависимость от размера кодовой базы.Стенд простой: генерируются пустые модули по 25 атрибутов, каждый десятый держит ссылки на
date, datetime и time, как будто их импортировали по имени. Замеряется цикл включения и выключения подмены. Python 3.15, MacBook на M1, freezegun 1.5.5 и time-machine 3.3.0.freezegun укладывается в прямую: около 1,4 мс постоянных расходов плюс 2,5 мкс на каждый модуль;freeze_time.start() обходит весь sys.modules и подменяет каждую найденную ссылку на настоящие функции; даже при попадании в кэш приходится перечислить и захешировать имена атрибутов всех модулей.У обхода есть и содержательная плата, помимо скорости: он не находит ссылки в атрибутах классов, значениях по умолчанию, замыканиях и C-расширениях, и они продолжают отдавать настоящее время. Плюс подставленные объекты видны по типу,
datetime.__name__ внутри подмены становится FakeDatetime, и код, который смотрит на типы, может повести себя иначе.time-machine вместо обхода переписывает указатель ml_meth в структуре PyMethodDef у встроенных функций, читающих часы. Таких функций десять, и это ровно десять записей в память независимо от размера проекта.Автор оговаривает, что модули в стенде синтетические, это
types.ModuleType, положенные прямо в sys.modules, а не настоящие импорты, и что замеряется только цикл подмены, из нескольких прогонов берётся минимальное время.Полная статья: https://adamj.eu/tech/2026/08/03/python-time-machine-o1-freezegun-on/
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🤔1
Процесс дорос до 57 гигабайт памяти и был убит ядром, потому что к каждой задаче прикреплялся живой стек вызовов
Хорхе Эскобар описал 27 июля в трекере playwright-python редкий вид утечки, где виноваты сразу две стороны. Синхронная обёртка библиотеки на каждый вызов создаёт задачу asyncio и вешает на неё атрибут со значением
Сама по себе такая привязка живёт ровно до конца вызова. Но на Python с 3.14.0 по 3.14.6 была регрессия:
🔘 нагрузка простая: скриншот на каждый кадр, PNG 3840×2160 декодируется через PIL прямо в той функции, которая вызывает
🔘 на итерацию оставалось около 40 МБ: примерно 33 МБ распакованного изображения и ещё около 8 МБ уменьшенной копии, обе как локальные переменные удержанного кадра;
🔘 после примерно 1900 итераций процесс занял около 57 ГБ и получил SIGKILL;
🔘
🔘 цепочка ссылок читается целиком: изображение, кадр вызывающей функции,
🔘 перестройка своего кода так, чтобы во время вызова в кадрах не было больших локальных переменных, снизила утечку с 40 МБ до 0,94 МБ на итерацию.
Обе стороны уже починены: регрессию в CPython закрыли 2 июля, а в playwright-python убрали хранение живых кадров, и 30 июля обсуждение закрыли. Автор замерял на конкретной нагрузке и на macOS с Python 3.14.6, но структурно то же поведение видел и на 3.12.
Вывод из этой истории шире одной библиотеки: пока задача жива, живо и всё, на что смотрят её кадры. Прикреплять
Обсуждение целиком: https://github.com/microsoft/playwright-python/issues/3157
@zen_of_python
Хорхе Эскобар описал 27 июля в трекере playwright-python редкий вид утечки, где виноваты сразу две стороны. Синхронная обёртка библиотеки на каждый вызов создаёт задачу asyncio и вешает на неё атрибут со значением
inspect.stack(0). Это не строки, а объекты FrameInfo, внутри которых лежат живые кадры стека вместе со всеми локальными переменными вызывающего кода.Сама по себе такая привязка живёт ровно до конца вызова. Но на Python с 3.14.0 по 3.14.6 была регрессия:
asyncio.wait с FIRST_COMPLETED навсегда оставлял вызывающую задачу в множестве ожидающих у того будущего, которое так и не завершилось. Playwright задевает это на каждом обращении к браузеру, потому что гонит ответ на команду наперегонки с долгоживущим будущим ошибки транспорта. В итоге каждая завершённая операция утаскивала за собой задачу, кадры и всё их содержимое.page.screenshot();gc.collect() не помогает вообще: запись в множестве ожидающих является сильным корнем, а не циклической ссылкой;FrameInfo, завершённая задача скриншота, множество ожидающих у будущего ошибки транспорта, которое живёт всю сессию браузера;Обе стороны уже починены: регрессию в CPython закрыли 2 июля, а в playwright-python убрали хранение живых кадров, и 30 июля обсуждение закрыли. Автор замерял на конкретной нагрузке и на macOS с Python 3.14.6, но структурно то же поведение видел и на 3.12.
Вывод из этой истории шире одной библиотеки: пока задача жива, живо и всё, на что смотрят её кадры. Прикреплять
inspect.stack() к объектам, время жизни которых вы не контролируете, означает подписаться на удержание чужих локальных переменных.Обсуждение целиком: https://github.com/microsoft/playwright-python/issues/3157
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2
Проверка одной группы объектов стоит времени, пропорционального размеру группы, а полный проход сборщика — всему содержимому процесса
28 июля на discuss.python.org появилось предложение научить сборщик мусора принимать подсказки: разработчик или анализатор кода говорит, что вот эта группа объектов, скорее всего, больше никому не нужна, а сборщик проверяет и освобождает её, не запуская полный обход поколения.
Смысл в том, как в CPython устроено освобождение памяти. Основную работу делает подсчёт ссылок, он мгновенный и точный. Но объекты, ссылающиеся друг на друга по кругу, счётчиками не убираются: у запроса лежит ссылка на его статус, у статуса обратная ссылка на запрос, оба недостижимы снаружи, а счётчики у обоих равны единице. За такие случаи отвечает отдельный сборщик циклов, и он останавливает все потоки, чтобы обойти память.
🔘 предлагаемая проверка простая: просуммировать счётчики ссылок объектов группы и посчитать, сколько ссылок на них идёт изнутри самой группы. Совпало — снаружи на группу никто не смотрит, можно освобождать;
🔘 в примере с запросом и статусом обе суммы равны двум, поэтому пара удаляется без обхода кучи;
🔘 подсказка ничего не гарантирует и всегда проверяется, а если из какой-то категории приходит слишком много ложных подсказок, её можно перестать слушать;
🔘 автор ссылается на известный случай Instagram, где сборщик отключали и просто перезапускали машины по мере роста памяти, и на статистику, по которой сборка мусора съедает от 5 до 30 процентов процессорного времени;
🔘 в обсуждении сразу возразили: Терри Ридди спрашивает, насколько часто цикл нельзя просто разорвать руками, а Грег Юинг — есть ли алгоритм, который окажется дешевле честной сборки;
🔘 ещё одно возражение практическое: подсказки придётся обновлять при каждой правке структур данных и как-то проверять тестами, а если уж писать такой тест, проще искать сами циклы и разрывать их.
Замеров у автора нет, работающего кода тоже: это предложение и спор вокруг него. Но читать его полезно из-за самого разбора механики: видно, что подсчёт ссылок и сборщик циклов решают разные задачи, и что цена второго определяется размером всей кучи, а не количеством мусора.
Обсуждение: https://discuss.python.org/t/improving-python-garbage-collection-performance-by-providing-unreachable-cycles-hints/108307
@zen_of_python
28 июля на discuss.python.org появилось предложение научить сборщик мусора принимать подсказки: разработчик или анализатор кода говорит, что вот эта группа объектов, скорее всего, больше никому не нужна, а сборщик проверяет и освобождает её, не запуская полный обход поколения.
Смысл в том, как в CPython устроено освобождение памяти. Основную работу делает подсчёт ссылок, он мгновенный и точный. Но объекты, ссылающиеся друг на друга по кругу, счётчиками не убираются: у запроса лежит ссылка на его статус, у статуса обратная ссылка на запрос, оба недостижимы снаружи, а счётчики у обоих равны единице. За такие случаи отвечает отдельный сборщик циклов, и он останавливает все потоки, чтобы обойти память.
Замеров у автора нет, работающего кода тоже: это предложение и спор вокруг него. Но читать его полезно из-за самого разбора механики: видно, что подсчёт ссылок и сборщик циклов решают разные задачи, и что цена второго определяется размером всей кучи, а не количеством мусора.
Обсуждение: https://discuss.python.org/t/improving-python-garbage-collection-performance-by-providing-unreachable-cycles-hints/108307
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4❤1
Бинарный поиск ускорили в 8 раз, не меняя ни алгоритм, ни язык: 16,6 процента промахов предсказателя ветвлений превратились в ноль
Итамар Тёрнер-Трауринг разобрал шаг из градиентного бустинга в scikit-learn: миллион чисел с плавающей точкой нужно разложить по 255 корзинам, для чего по массиву границ гоняется бинарный поиск. Код уже скомпилированный и уже параллелится по ядрам, алгоритм оптимальный. Статья опубликована 11 июля и обновлена 18-го, работа сделана в рамках Quansight.
Остался запас, который не виден на уровне алгоритма. Современное ядро процессора выполняет несколько инструкций одновременно и угадывает, куда пойдёт ветвление. В бинарном поиске сравнение по определению непредсказуемо: каждая итерация с равной вероятностью идёт влево или вправо, и предсказатель ошибается примерно в половине случаев, а конвейер каждый раз сбрасывается.
🔘 исходная версия: 45 200,4 микросекунды, 16,6 процента неверных предсказаний, 0,7 инструкции за такт, около 27 инструкций ветвления на одно значение;
🔘 первая переделка убирает ветвление, заменяя его условным присваиванием через
🔘 предвычисление половины диапазона и доступ без проверки границ дают 7 280,4 мкс и обрушивают число инструкций ветвления до 6 020 571 на весь прогон;
🔘 финальная версия обрабатывает значения кусками по 16 штук с внешним циклом по шагам поиска: 5 453,4 мкс и 4,9 инструкции за такт;
🔘 любопытно, что она выполняет больше инструкций, чем предыдущая, но идёт быстрее: независимые куски работы позволяют процессору выполнять их одновременно;
🔘 итог — примерно восьмикратное ускорение на том же алгоритме, том же языке и одном ядре.
Автор оговаривает, что это упрощённый пример, а не полная реализация из scikit-learn, и что статья не заменяет учебник по устройству процессора. В обновлении он честно пишет, что раньше ошибся со сравнением чисел с плавающей точкой, и после исправления SIMD перестал давать выигрыш, хотя код от этого стал только быстрее. Дальнейший запас автор видит в параллелизме по ядрам.
Польза для питониста прямая: когда расчёт уже переписан на расширение и всё равно кажется медленным, следующий уровень — не алгоритм, а поведение процессора на этом коде.
Полная статья: https://pythonspeed.com/articles/branchless-binary-search/
@zen_of_python
Итамар Тёрнер-Трауринг разобрал шаг из градиентного бустинга в scikit-learn: миллион чисел с плавающей точкой нужно разложить по 255 корзинам, для чего по массиву границ гоняется бинарный поиск. Код уже скомпилированный и уже параллелится по ядрам, алгоритм оптимальный. Статья опубликована 11 июля и обновлена 18-го, работа сделана в рамках Quansight.
Остался запас, который не виден на уровне алгоритма. Современное ядро процессора выполняет несколько инструкций одновременно и угадывает, куда пойдёт ветвление. В бинарном поиске сравнение по определению непредсказуемо: каждая итерация с равной вероятностью идёт влево или вправо, и предсказатель ошибается примерно в половине случаев, а конвейер каждый раз сбрасывается.
select_unpredictable: 9 685,2 мкс, промахов ноль, 3,2 инструкции за такт, ветвлений 19 на значение;Автор оговаривает, что это упрощённый пример, а не полная реализация из scikit-learn, и что статья не заменяет учебник по устройству процессора. В обновлении он честно пишет, что раньше ошибся со сравнением чисел с плавающей точкой, и после исправления SIMD перестал давать выигрыш, хотя код от этого стал только быстрее. Дальнейший запас автор видит в параллелизме по ядрам.
Польза для питониста прямая: когда расчёт уже переписан на расширение и всё равно кажется медленным, следующий уровень — не алгоритм, а поведение процессора на этом коде.
Полная статья: https://pythonspeed.com/articles/branchless-binary-search/
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4✍2
Новые модели выходят быстрее, чем все успевают их внедрять, а прогнозы про ИИ устаревают ещё быстрее, поэтому на кафедре техпреда МФТИ запустили «Точку сборки».
Это серия открытых вебинаров, на которых спикеры делятся опытом внедрения агентов, быстрой сборки MVP и конкретными кейсами своих команд.
Больше о «Точке сборки», о первом вебинаре и о том, как попасть на следующие читайте в новом материале на Tproger: https://tproger.ru/articles/pochemu-opyt-razrabotchikov-luchwe-prognozov-pro-ii
Это серия открытых вебинаров, на которых спикеры делятся опытом внедрения агентов, быстрой сборки MVP и конкретными кейсами своих команд.
Больше о «Точке сборки», о первом вебинаре и о том, как попасть на следующие читайте в новом материале на Tproger: https://tproger.ru/articles/pochemu-opyt-razrabotchikov-luchwe-prognozov-pro-ii
❤1
Django научился превращать классический N+1 в два запроса без единой правки в коде цикла
Django 6.1 вышел 5 августа. Главное изменение в релизе — режимы дозагрузки полей, то есть настройка того, что происходит в момент обращения к полю, которое из базы не приехало.
Раньше поведение было одно: подтягиваем недостающее поле для этого объекта. Теперь оно называется
🔘 режим ставится методом
🔘
🔘 у
🔘 плата за это честно описана:
🔘 настройка
🔘 число итераций в хешировании паролей PBKDF2 поднято с 1 200 000 до 1 500 000.
По мелочи: добавлены функции
Поддерживаются Python 3.12, 3.13 и 3.14. Обычная поддержка релиза продлится примерно до апреля 2027 года, расширенная до декабря. В заметках отдельно перечислены несовместимости, так что перед обновлением их стоит прочитать: смена семантики сигналов при каскадном удалении на стороне базы как раз тот случай, когда код продолжает работать, а побочные действия молча исчезают.
@zen_of_python
Django 6.1 вышел 5 августа. Главное изменение в релизе — режимы дозагрузки полей, то есть настройка того, что происходит в момент обращения к полю, которое из базы не приехало.
Раньше поведение было одно: подтягиваем недостающее поле для этого объекта. Теперь оно называется
FETCH_ONE и остаётся по умолчанию, а рядом появились ещё два. FETCH_PEERS подтягивает поле сразу для всех объектов, приехавших из того же запроса, то есть работает как prefetch_related по требованию. FETCH_RAISE запрещает неявные обращения к базе вовсе.QuerySet.fetch_mode(), и цикл, который в каждой итерации трогает внешний ключ, начинает укладываться в два запроса вместо N+1;FETCH_RAISE полезен в критичных по скорости участках: любой незамеченный поход в базу превращается в ошибку, а не в тихий лишний запрос;ForeignKey.on_delete появились варианты на стороне базы: DB_CASCADE, DB_SET_NULL и DB_SET_DEFAULT работают через SQL ON DELETE, и объекты для удаления загружать не нужно;DB_CASCADE не вызывает сигналы pre_delete и post_delete, потому что Python в удалении не участвует;MAILERS позволяет описать несколько почтовых движков с разными параметрами, как это давно сделано для кэшей и баз; в Django 7.0 она заменит EMAIL_BACKEND, пока старая настройка работает с предупреждением;По мелочи: добавлены функции
UUID4 и UUID7, вычисляемые поля получили виртуальные колонки на PostgreSQL 18 и выше, RedirectView с сохранением метода запроса теперь отвечает кодами 307 и 308 вместо 302 и 301, а OpenLayers в админке обновлён с 7.2.2 до 10.9.0.Поддерживаются Python 3.12, 3.13 и 3.14. Обычная поддержка релиза продлится примерно до апреля 2027 года, расширенная до декабря. В заметках отдельно перечислены несовместимости, так что перед обновлением их стоит прочитать: смена семантики сигналов при каскадном удалении на стороне базы как раз тот случай, когда код продолжает работать, а побочные действия молча исчезают.
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4
У библиотеки websockets появилась реализация на Trio
Версия 17.0 вышла 29 июля. Раньше выбора не было: под капотом всегда
Релиз ломающий, и вот что придётся проверить перед обновлением:
🔘 минимальная версия Python поднята до 3.11, последней с поддержкой 3.10 остаётся ветка 16.1;
🔘 удалены псевдонимы модулей, которые перенесли ещё в девятой версии;
🔘 несколько логических аргументов у
🔘 не-ASCII заголовки рукопожатия теперь кодируются в
🔘 в реализации на потоках аргумент
🔘
Из добавленного:
@zen_of_python
Версия 17.0 вышла 29 июля. Раньше выбора не было: под капотом всегда
asyncio либо потоки. Теперь третий вариант — Trio, для тех, кто уже строит на нём приложение.Релиз ломающий, и вот что придётся проверить перед обновлением:
send(), ping() и broadcast() стали передаваться только по имени;ISO-8859-1; прежнее поведение было недокументированным и не совпадало со спецификацией HTTP;socket переименован в sock;process_request теперь получает и запросы с методом, отличным от GET, и запросы по HTTP/1.0, вместо того чтобы соединение просто закрывалось.Из добавленного:
broadcast() и Server.connections в реализации на потоках, аккуратное закрытие соединений при остановке, reconnect_delays для настройки пауз между попытками переподключения, а на некорректное рукопожатие сервер теперь отвечает кодами 405 и 505 вместо тишины.@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👌1
Ruff включил по умолчанию 413 правил вместо 59
Версия 0.16.0 вышла 23 июля. Заодно 18 самых спорных правил групп pycodestyle и pyflakes из набора по умолчанию убрали.
🔘 форматирование блоков Python внутри Markdown включено по умолчанию — документация в репозитории теперь тоже под форматтером;
🔘 подавляющий комментарий
🔘 исправления показываются прямо в выводе
🔘
🔘 в JSON-выводе поля
🔘 стабилизированы двенадцать правил.
Обновление стоит планировать отдельной задачей: на первом прогоне вылезет то, что раньше молчало, и часть находок потребует решения — включать правило, исключать или править код.
@zen_of_python
Версия 0.16.0 вышла 23 июля. Заодно 18 самых спорных правил групп pycodestyle и pyflakes из набора по умолчанию убрали.
ruff: ignore можно ставить в конце строки, как noqa, или на строке перед диагностикой;check и format --check, вместе с кусочком диффа;format --check получил форматы вывода линтера, включая github и gitlab для аннотаций в сборке;filename, location и end_location теперь могут быть null — разборщики придётся проверить;Обновление стоит планировать отдельной задачей: на первом прогоне вылезет то, что раньше молчало, и часть находок потребует решения — включать правило, исключать или править код.
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤🔥2
Пять игр на Python, которые интересно почитать: они лежат в репозитории с настройками рабочего окружения Arch, и четыре из пяти обходятся стандартной библиотекой, а две самые большие рисуют интерфейс на
🔘
🔘
🔘 обе игры печатают через
🔘 рекорды пишутся в общий
🔘
@zen_of_python
curses.tetris.py, 766 строк: все повороты фигур считаются заранее в build_rotations, при повороте у стены пробуются сдвиги, есть удержание фигуры и превью следующих, а когда разом уходят четыре линии, по экрану летят частицы;snake.py, 423 строки: поле подгоняется под размер терминала, интервал тика уменьшается с каждым уровнем, голова заворачивается через край;safe_addstr: он глотает curses.error, а в змейке ещё и обрезает строку по краю окна — иначе запись в последнюю ячейку роняет игру;game_high_scores.json так, чтобы не затирать записи соседних игр;tetris_pygame.py — та же игра на pygame и dataclass, удобно сравнить два подхода к одной механике.@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
12 августа вышли security-релизы Python 3.12.14, 3.11.16 и 3.10.21. Список исправлений длинный, и почти всё в нём про стандартную библиотеку:
🔘
🔘
🔘
🔘
🔘 квадратичное и экспоненциальное поведение убрано из
🔘
Все три ветки в стадии security-only: релизы только исходниками, без установщиков. 3.10 получает исправления до октября этого года, 3.12 — до октября 2028-го.
@zen_of_python
tarfile: несколько обходов фильтра data_filter(), через которые архив мог положить symlink за пределы каталога назначения; один из них — обход прошлогоднего CVE-2025-4330;webbrowser.open() отклоняет URL с ведущим дефисом, чтобы аргумент не читался браузером как флаг; закрыт и обход этой проверки через префикс %action;HTTPConnection.set_tunnel() больше не пропускает CR/LF в заголовках, wsgiref — управляющие символы в статусе, http.cookies — в Morsel (CVE-2026-3644);SourcelessFileLoader открывает .pyc через io.open_code() (CVE-2026-2297), в xml.parsers.expat починен крэш от глубокой вложенности (CVE-2026-4224);unicodedata.normalize(), html.parser, configparser, ElementTree и csv.Sniffer;http.client ограничил число trailer-строк и промежуточных ответов 1xx сотней, чтобы сервер не мог держать клиента вечно.Все три ветки в стадии security-only: релизы только исходниками, без установщиков. 3.10 получает исправления до октября этого года, 3.12 — до октября 2028-го.
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2