Работает — не трожь: зачем обновлять Python в долгоживущих проектах
Всем привет! Меня зовут Сергей Яхницкий. Я пишу на Python уже больше шести лет, техлид в Яндекс Такси, Python‑евангелист и член Python‑комитета Яндекса (аналог Python Steering Council).
Человек я простой, звёзд с Гитхаба не хватал: до того, как я устроился в Такси, я мирно писал маленькие бэкенды на Python. А потом меня прорвало: кодогенерации, CI/CD, кучи тестов, монорепа и прочее. Вот тут‑то моя питоничья душа и воспряла. Решил я всё автоматизировать, обновить всё, что движется, а что не движется — подвигать и обновить. Из этого вышел мой рассказ.
Этот пост широко освещает изменения последних нескольких лет и куда в принципе движется Python. Пост будет полезен как новичкам, которые только ещё изучают Python, так и опытным специалистам, которые думают о языке разработки в долгосрочной перспективе.
Так зачем обновлять Python?
Сразу отвечу на вопрос, который может первым возникнуть в голове у читателя. Дело в том, что этот язык суперпопулярен среди сообщества. Python с момента своего создания быстро догонял другие языки, и в 2015-м попал в топ-3 и до сих пор удерживает свои позиции.
Python в разных топах
Язык популярен не только среди сообщества, но и среди корпораций: например, Dropbox, Bloomberg, Reddit и других. Хотелось бы отдельно отметить Meta (признана экстремистской организацией, а её продукты, Facebook и Instagram, запрещены на территории РФ), которая вложилась грантом 300 тысяч долларов в Python Software Foundation и продлила вакансию Developer in Residence. То есть в Python уже есть человек, который помогает с его развитием, поддерживает сайт PyPI, помогает core‑девелоперам, смотрит пул‑реквесты и даже вёл несколько релизов.
В Python существуют Python Enhancement Proposal — это такие своеобразные ГОСТы в мире Python. Самый первый из них был опубликован в далёком 2000 году, в нём есть индекс всех других важных изменений. Именно о них я и буду рассказывать.
Там же в PEP 602 зафиксирован график выхода релизов и сроки их поддержки. Сейчас актуальными версиями Python считаются с 3.8 по 3.12. Но 3.8 и 3.9 вышли довольно давно, поэтому мы будем фокусироваться на версиях 3.10 и более поздних. И немножко даже затронем версию, которая уже в бете — 3.13.
Язык
В этом разделе перечисляются значимые изменения в самом ядре Python — его синтаксисе.
grammar
Когда мы говорим про Python, то сразу вспоминаются мемы про точку с запятой и фигурные скобочки. У нас это не принято — мы всё делаем через отступы. Не все знают, почему в Python так можно, а в других языках — нет.
C незапамятных времён в Python существовал парсер для LL (1)‑грамматики. Это та вещь, которая позволяет выражать все конструкции языка Python и превращать их в абстрактное синтаксическое дерево AST, а затем в байткод, понятный виртуальной машине Python.
Но за все долгие годы —, а Python появился в 1991 году — он сильно вырос. Некоторые конструкции даже не подходили под эту грамматику, поэтому отсюда выросло множество костылей, которые его подпирали, а новые вещи туда втиснуть было практически невозможно.
В версии 3.9 сам Гвидо ван Россум (автор Python) заменил его новым PEG‑парсером, благодаря которому стали доступны новые конструкции. Это изменение не такое свежее (2020-й год), но достаточно важное, и является основой для последующих улучшений, о которых пойдёт речь.
match-case
Раньше приходилось писать много условий, переборов, считать количество параметров. Теперь этого делать не нужно: появилось структурное сопоставление с образцом.
Сначала разработчики сделали один большой PEP. Он оказался настолько новым и сложным, что его пришлось разбить на три поменьше:
PEP 634 — Structural Pattern Matching: Specification → Что?
PEP 635 — Structural Pattern Matching: Motivation and Rationale → Почему?
PEP 636 — Structural Pattern Matching: Tutorial → Как?
Вот как match‑case
выглядит на практике:
В таблице видно, насколько упростился код. А если вам не до конца понятна суть применения match‑case
, то можете почитать статью коллег из Практикума.
И ведь нашлись те, кому оказалось мало! match‑case
оказался настолько удобным, особенно в различных библиотеках, парсинге деревьев и случаях, когда у вас много различных условий с переборами, что на Python Language Summit 2023 попросили расширить эту конструкцию. Мне лично показалось, что предложенное решение очень похоже на PEP 647. Вот что попросили добавить:
возможность писать вспомогательные функции для сопоставления;
сопоставлять строку с полярными координатами;
сопоставлять комплексные числа с полярными координатами;
вычислять ленивые списки при сопоставлении;
сопоставлять с JSON или XML.
Ждём новый PEP от Michael «Sully» Sullivan.
except*
Новая грамматика позволила ввести новую терминологию — группы исключений. Отец asyncio, Юрий Селиванов, когда прочитал у разработчика Trio интересные идеи про Structured Concurrency, решил занести в Python группы задач — возможность запускать несколько синхронных задач из одного и того же места.
Возможно, вы встречались с asyncio.gather
. Если там возникли исключения, то они возвращаются просто списком, но только при условии, что вы передали флаг return_exceptions
— в противном случае весь блок падает на первом исключении. Теперь авторы языка сделали новый синтаксис, который позволяет обрабатывать эти исключения более удобным способом. То есть в таск‑группе может возникнуть больше одного исключения, и с помощью except*
вы можете обрабатывать сразу несколько.
try:
low_level_os_operation()
except* OSError as eg:
for e in eg.exceptions:
print(type(e).__name__)
Press F
Хорошая новость: в версии 3.12, которая вышла 2 октября 2023 года, наконец‑то починили f‑строки. Кто пользовался шаблонами в f‑строках, знает, что внутри нельзя использовать те же самые кавычки, которые используются для самой f‑строки. Постоянно приходилось делать выкрутасы с одинарными или двойными кавычками. И даже IDE довольно сложно распознавать внутри, как эта конструкция работает.
Ребята наконец‑то сделали чёткую грамматику для f‑строк благодаря новому парсеру. Теперь шаблоны можно бесконечно вкладывать друг в друга и не париться, какие кавычки у вас снаружи, а какие внутри. Также это здорово облегчило работу разработчикам IDE.
std
Python — это не только сам код, но ещё и его стандартная библиотека (в народе — «батарейки»). На cаммитах 2021 и 2023 годов многие разработчики задавали вопросы:, а что такое вообще эта стандартная библиотека и что в ней должно быть?
Результатом этих изысканий стал PEP 594. В нём записаны модули, которые уже неактуальны в Python. А ещё там есть расписание, по которому их будут убирать. Большинство этих библиотек родом из 1990-х — скорее всего, вы ими никогда не пользовались.
Также под нож попала не такая уж и древняя библиотека distutils. Возможно, вы слышали про неё, если поставляли свои собственные библиотеки в PyPI.org. Этот модуль хотели сделать стандартным, но он им так и не стал. В итоге выиграл setuptools. Но проектов, использующих distutils всё ещё слишком много, поэтому процесс перехода вынесли в отдельный PEP. И в версии 3.12 его уберут из стандартной поставки Python.
Но ребята не только убирают старые модули — ещё и новые завозят. Например, в версии 3.9 завезли библиотеку zoneinfo. Теперь не обязательно таскать с собой pytz каждый раз, когда вы работаете с датами.
>>> from zoneinfo import ZoneInfo
>>> zone = ZoneInfo("Pacific/Kwajalein")
>>> dt = datetime(2020, 4, 1, 3, 15, tzinfo=zone)
>>> f"{dt.isoformat()} [{dt.tzinfo}]"
'2020-04-01T03:15:00+12:00 [Pacific/Kwajalein]'
Для конфигурации проектов уже давно стандартом стал формат TOML. Теперь с версией 3.11 вы можете не таскать с собой дополнительные библиотеки, а просто прочитать конфиги прямо из стандартной.
import tomllib
with open("pyproject.toml", "rb") as f:
data = tomllib.load(f)
toml_str = """
python-version = "3.11.0"
python-implementation = "CPython"
"""
data = tomllib.loads(toml_str)
Аннотации
С обязательной частью всё. Дальше пойдет опциональная. Кто‑нибудь знает, что к ней относится? Правильно, тайп‑хинты! Или, если взять чуть шире, аннотации.
Когда интерпретатор читает ваш код, он не использует аннотации кода при выполнении инструкций. Эти инструкции доступны только вам, разработчикам. Например, здорово, когда вы пишете типы в своих функциях. Люди, которые будут их использовать, быстро поймут, что же хотят эти функции получить на вход.
Образец подсказки с типизацией в PyCharm
Хорошо, когда на код‑ревью человек может посмотреть интерфейс функции, а ещё лучше, когда автоматически за вас это может сделать mypy. Библиотека mypy проверяет, что в функции передаются аргументы тех типов, которые вы указали.
Эта фича появилась очень давно — ещё в версии 3.5, —, но именно благодаря библиотеке mypy в Python произошло множество изменений в районе аннотаций.
Deferred annotation
Несмотря на то что аннотации — это только подсказки и никак не используются интерпретатором, они всё равно давали нагрузку при вычислениях. Члены комитета долго не могли решить, что с этим делать. Было несколько подходов. Как временное решение был хак from __future__ import annotations
, который нужно было писать в каждом файле. Но нужен был подход, который удовлетворит всех: разработчиков статических анализаторов, авторов библиотек и пользователей‑программистов. В мае 2023 года наконец‑то пришли к единому решению: с версии 3.14 аннотации будут вычисляться только по требованию. Это позволяет использовать аннотации без лишнего оверхеда и без необходимости в forward‑declaration.
X | Y в 3.10
С помощью этого нового оператора можно писать int | str
вместо Union[int, str]
. В дополнение к аннотациям данный синтаксис можно использовать в проверках isinstance()
и issubclass()
.
TypeAlias в 3.10
Раньше приходилось писать объявления типов в глобальном пространстве имён. И отличить их от прочих констант можно было только неявно: по именованию или использованию в аннотациях. Теперь вы можете точно сказать, что это не какая‑то константа, а ваш новый алиас для типа.
Self в 3.11
Если вы часто пользуетесь ООП (возможно, когда создаёте объекты с помощью методов класса в фабрике), теперь не нужно писать в кавычках из форвард‑референсов. Вы можете написать, что этот метод возвращает инстанс этого класса: typing.self
. При наследовании возвращается тип наследника без переопределения метода и эксплицитного указания идентичного хинта в наследнике. То есть благодаря typing.self
при наследовании автоматом указывается нужный тип.
TypedDict
Многие знают, что Python состоит почти полностью из диктов. Даже в аннотациях придумали свой дикт — TypedDict. Он появился ещё в версии 3.8, а сейчас для него сделано несколько улучшений.
Суть PEP 655 заключается в том, что Optional — не совсем то, что подразумевается в словарях. В словарях может вообще какого‑то ключа не быть, а не то что None можно передать как значение. То есть существуют некоторые различия в семантике: {"title": "some_title", "year": None}
vs {"title": "some_title"}
. И специально эти различия смогли вынести через аннотации Required
и NotRequired
.
А в версии 3.12 поняли, что TypedDict сильно похожи на кварги. Раньше в кваргах можно было написать только тип ключа и тип значения — он всегда должен быть один. На самом деле в kwargs не так: например, вы можете и bool передать, и int — что хотите. Именно TypedDict очень подходят к этой задаче, потому что они проверяют тип значения для каждого ключа в словаре, в данном случае kwargs. И в 3.12 формализовали подход.
dataclass_transform
Но у нас есть не только дикты, ведь мы иногда хотим обращаться к полям через атрибуты. И для dataclasses в PEP 681 тоже сделали небольшое улучшение.
Теперь библиотеки могут сами сказать, что наши объекты похожи на Data Class, и не нужно каждый раз писать PEP для изменений. Теперь разработчикам будет доступна статическая типизация в библиотеках, например из известных: SQLAlchemy, FastAPI и Pydantic (всё будет проверяться из коробки).
ParamSpec
Так же есть принятый PEP 612 для особых случаев. Если вы используете много декораторов, то, начиная с версии 3.10, можно довольно простым способом пробросить типы из декорируемой функции. Раньше это было сделать невозможно и приходилось ставить заглушки для mypy.
TypeGuard
Многие знают, если в mypy запустить проверки вида if is not None
, он поймёт, что тип переменной больше не Optional. Но если вы хотите каких‑то более сложных проверок и выносите эти проверки в функции, то mypy их не сможет понять. Благодаря PEP 647 проблема решается аннотациями. Конкурент для TypeGuard — TypeIs уже на подходе в PEP 742 для Python 3.13.
final override
Для любителей ООП тоже сделали парочку улучшений. В версии 3.8 добавили декоратор final
, который запрещает переопределять методы в дочерних классах И ещё добавили декоратор override
, который говорит: если вы в родительском классе что‑то поменяете, не забудьте поменять вот этот самый метод. Без этих проверок можно потерять изменения между классами при рефакторе.
Generics
Давайте немножко пожестим. Добавили Variadic Generics, добавили Type Parameter Syntax. Я, когда первый раз посмотрел на это, вообще не понял, что это там происходит. Но на самом деле это сильно упрощает конструкции для авторов научных библиотек типа numpy/scipy.
class Array(Generic[DType, *Shape]): ...
С другой стороны, после PEP 612, глядя на объявление функции, мы сразу видим, что во всём классе используется одна и та же переменная типа. Не нужно выносить в отдельные TypeVar.
Сравните примеры слева и справа. После внесения изменений конкретные переменные используются в конкретном месте. Не смущайтесь, если теперь у вас в классах появятся квадратные скобки. C версии 3.12 можно использовать новый синтаксис вместо TypeAlias, про который я говорил выше.
Профилировщики
Про аннотации мы закончили — перейдём на уровень глубже. До этого мы просто смотрели код — это то, что мы видим. А вот о том, что происходит под капотом, многие даже не задумываются. В этом разделе сделаем обзор инструментов для сбора характеристик программы при её исполнении, а именно обзор профилировщиков.
std
В Python из коробки поставляется профилировщик cProfile. Вы можете посмотреть, какая функция сколько времени у вас занимает. Если вы хотите увидеть, что у вас происходит с памятью, вы можете воспользоваться tracemalloc.
Но дело в том, что cProfile — детерминированный профилировщик и добавляет нагрузку на каждую строчку, на каждый вызов функций. Это мешает собирать честную статистику.
Tracemalloc добавляет ещё больше нагрузки, имеет сложный вывод, и часто при его использовании ничего не понятно.
Но прогресс не стоит на месте и появляются инструменты — быстрее и удобнее.
perf
В версии 3.12 поддержали perf. Это утилита из Linux, где вы можете посмотреть сведения о процессе. Идея похожа на cProfile, но раньше она могла смотреть только в нативные методы: то, что там в интерпретаторе вызывается на C. Возможно, вы об этом знаете, если смотрели, как работает интерпретатор Python: ядро написано на С, а про функции он вообще ничего не знал, потому что это часть интерпретатора. Теперь с версией 3.12 perf будет понимать, что у вас происходит, какие методы были вызваны.
Узнать подробнее про perf, почему он полезен для Python и как им пользоваться, можно в этой статье.
scalene
Но так как Python — это интерпретируемый язык, всё же, чем в perf, удобнее профилировать инструментами, которые специально разрабатывались для Python. Одним из таких профилировщиков является Scalene, автор которого выступал на недавнем саммите.
Этот профилировщик позволяет диагностировать не только производительность CPU, но ещё и память и GPU. Поэтому если вы делаете какие‑то разработки для machine learning и запускаете код на видеокартах, то эта утилита вам обязательно поможет.
py-spy
Если вам интересно посмотреть, что у вас происходит с процессором, вы можете использовать утилиту py‑spy. В режиме диспетчера задач вы можете «подцепиться» к процессу и посмотреть, какие функции прямо сейчас у вас потребляет CPU. Это поможет быстро понять, где у вас проблема, и устранить её.
Как работает py-spy
![32c3836e9cac938df6643ab2f2ed8940.gif](https://habrastorage.org/getpro/habr/upload_files/32c/383/6e9/32c3836e9cac938df6643ab2f2ed8940.gif)
Если вы не смогли обнаружить проблему в таком режиме, то можно на время снять профиль и посмотреть его в интерфейсе через Flame Graph либо через особый формат speed scope. Он очень похож на профили, которые можно снять в браузере в JavaScript и интерактивно посмотреть. Эта идея была взята именно оттуда.
Как выглядит speed scope
![0e8827c71d8bc8073b56f4b3d5d267e0.gif](https://habrastorage.org/getpro/habr/upload_files/0e8/827/c71/0e8827c71d8bc8073b56f4b3d5d267e0.gif)
Выгода таких утилит в том, что они не дают большую нагрузку при своей работе. Они делают снимки состояния раз в несколько миллисекунд. Они не смотрят постоянно на каждую строчку, а делают статистическую выборку, которая в итоге становится статистически значимой. Это хорошо помогает в поиске слабых мест.
memray
В Python обычно не принято следить за потреблением памяти, потому что за вас делает всю грязную работу уборщица garbage collector. Но когда ваши данные утекают по памяти, к вам на помощь придёт профайлер memray. Он появился совсем недавно, в 2022 году. В тёмные времена до этого приходилось использовать tracemalloc. Очень сложно, больно. Даже в самой документации приводят пример функции для более удобного форматирования вывода.
Работа memray
![e4a25ee037447ef0f3892c4f563daf20.gif](https://habrastorage.org/getpro/habr/upload_files/e4a/25e/e03/e4a25ee037447ef0f3892c4f563daf20.gif)
Большие проекты
И ещё мне бы хотелось отметить несколько крупных проектов, релиза которых все ждут. Ну и следить за ними тоже не менее интересно.
Faster CPython
Марк Шеннон, который работал вместе с Гвидо ван Россумом, и ещё несколько разработчиков — это команда, которую нанял Microsoft, чтобы целенаправленно улучшать Python. Первым этапом их крупного проекта по ускорению языка стал Faster CPython — адаптивный интерпретатор. Его зарелизили к версии 3.11. На эту тему есть доклад Евгения Афонасьева «Адаптируйся или умри».
К версии 3.12 ребята улучшили comprehensions
. Раньше приходилось создавать под капотом специальную лямбду при вызове comprehension: когда вы создавали списочек с for
внутри, это уже было лучше, чем просто написать свой for
, а теперь стало ещё быстрее.
И этот проект оказался настолько нашумевшим, что в сети гулял мем: якобы к версии 3.14 разработчики настолько оптимизируют Python, что он обгонит C++. Просто Марк Шеннон обещал ускоряться на 25% от версии к версии.
На своих микросервисах при переходе на Python 3.11 мы заметили прирост производительности от 10%.
nogil
Общение между процессами, да и вообще создание процессов — очень дорогое удовольствие. Это большой overhead, и память у них раздельная. Поэтому приходится каждый раз заводить данные — и в первом процессе, и во втором, а ещё их нужно постоянно сериализовать и десериализовать.
В других языках эту проблему решают с помощью потоков. В Python они тоже есть, но пользы от них мало. Всё упирается в Global Interpreter Lock. Когда вы пытаетесь взаимодействовать с памятью или используете особые модули — всё забирает этот lock. И когда у вас много потоков, соответственно будет высокий и contention. Да и на самом деле у вас под капотом будет выполняться в один момент времени только один поток.
И как сделать так, чтобы всем было хорошо? Об этом уже давно спорят на форумах.
Первые успехи в этом направлении появились у Сэма Гросса. Его проект nogil на саммите 2023 года в комитете встретили позитивно. Они сказали: «Идея хорошая, наконец‑то похоже, что там что‑то работает. Давайте‑ка мы плавненько лет через пять это всё запустим».
Помогла состояться этому проекту опять Meta (признана экстремистской организацией, а её продукты, Facebook и Instagram, запрещены на территории РФ). Она выпустила из своего форка бессмертные объекты. У неё был префорк‑фреймворк, который пытался создавать все объекты заранее и при создании отдельных процессов использовать copy‑on‑write — CoW. У себя компания это сделала быстро, а вот над общим кодом пришлось сильно попотеть, потому что фреймворк меняет ядро интерпретатора. Но благодаря этому изменению проект nogil смог наконец‑то получить позитивную оценку.
Довести nogil до конца планируется к 2030 году, а пока можно посмотреть подробный разбор от Евгения Афонасьева «Зачем нужен GIL и как от него избавиться».
subinterpreters
Но, как я уже сказал, всё это будет лет через пять. А что делать уже сейчас?
Есть решение. Очень давно было такое понятие subinterpreters — первые упоминания возникли еще на версии 1.5. И суть идеи такова: давайте мы будем запускать GIL в каждом субинтерпретаторе. Не один общий на всех, а много маленьких внутри одного процесса.
Нативные модули под капотом появились в версии 3.12. А в версии 3.13 обещали сделать именно модуль в стандартной библиотеке, где вы можете сказать: import interpreters
.
Но PEP 734, пришедший на смену PEP 554, отправили в статус Deferred. Ждём обновлений. Продолжатся ли работы в версии Python 3.14? Как самому стать контрибьютором и ускорить процесс вы можете узнать в цикле публичных лекций от Никиты Соболева «Лучший курс по Python».
Итак, мы сегодня рассмотрели изменения в языке и аннотациях, а также поняли, как можно ускорить и код, и интерпретатор. Помните: чем чаще обновляетесь, тем меньше дифф.