OlegTalks
.
Статьи
Тренажеры
PythonTalk
Написать мне
Написать мне
Статьи тест
Copy: Тест
2026-03-07 12:49
Каждый новый релиз Python — это не просто циферка в версии. Это эволюция, которая либо делает нашу жизнь проще, либо подкидывает новые инструменты для решения старых проблем. Python 3.15, судя по первой [альфа-версии](https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-pep782), не стремится устроить революцию, но несет в себе несколько изменений, которые можно без преувеличения назвать фундаментальными. Этот релиз делает ставку на три основных улучшения: 1. **Производительность на старте:** Решение проблемы, о которой все знали, но с которой мирились. 2. **Новый профилировщик**, который можно безболезненно использовать в продакшене. 3. **Исправление исторических ошибок:** Прощаемся с зависимостью от локали и приветствуем UTF-8 по умолчанию. Давайте разберемся с каждым пунктом.
## 🚀 [PEP 810: Lazy Imports](https://peps.python.org/pep-0810/) — ленивые импорты, которые мы заслужили Это главная фича релиза. Идея ленивых импортов витала в воздухе годами. Многие, включая IT-гигантов, делали свои реализации. Почему? Потому что медленный старт приложений, особенно больших CLI-утилит и фреймворков, — это боль. **Проблема:** Когда вы запускаете скрипт, Python начинает каскадно подгружать все импорты. Даже если для выполнения команды `my_tool --help` нужна одна функция, вы все равно ждете, пока загрузятся `numpy`, `pandas` и `tensorflow`, просто потому что они импортированы где-то на верхнем уровне. **Решение:** PEP 810 вводит новый синтаксис — `lazy import`. ```python # Старый, "жадный" импорт. Модуль загружается СРАЗУ. import json # Новый, ленивый импорт. Модуль будет загружен только ПРИ ПЕРВОМ ОБРАЩЕНИИ. lazy import json lazy from json import dumps ``` ### Как это работает под капотом? Никакой магии. Когда вы делаете `lazy import json`, в глобальном неймспейсе создается не сам объект модуля, а специальный прокси-объект `types.LazyImportType`. Этот объект — просто заглушка, обещание того, что модуль будет загружен позже. Настоящая загрузка (импорт) происходит только в тот момент, когда вы впервые обращаетесь к атрибуту этого объекта. ```python import sys # Модуль еще не в памяти lazy import json print('json' in sys.modules) # False # Первое обращение -> происходит реальный импорт # Этот процесс называется "реификация" (reification) data = json.dumps({"hello": "world"}) # Теперь модуль загружен и находится в sys.modules print('json' in sys.modules) # True ``` После первой "реификации" прокси-объект заменяется настоящим модулем, и все последующие обращения работают с той же скоростью, что и при обычном импорте. Адаптивный интерпретатор CPython со временем и вовсе соптимизирует этот доступ, убрав любые проверки. ### Что это меняет для нас? 1. **Скорость запуска:** CLI-инструменты, тесты, тяжелые приложения — все они будут стартовать в разы быстрее, потому что загружается только тот код, который реально выполняется. 2. **Чистый код для аннотаций:** Больше не нужны уродливые блоки `if TYPE_CHECKING:`. Можно просто делать импорты для аннотаций ленивыми, и они не будут создавать никакой нагрузки в рантайме. ```python # Было from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Sequence, Mapping def process(items: "Sequence[str]") -> "Mapping[str, int]": ... # Стало lazy from collections.abc import Sequence, Mapping def process(items: Sequence[str]) -> Mapping[str, int]: ... ``` Намного чище, не правда ли? 3. **Экономия памяти:** Модули, которые так и не были использованы за время жизни процесса, никогда не будут загружены в память. Для долгоживущих приложений с кучей редко используемых фич это может быть серьезным выигрышем. > [!WARNING] > **Ложка дегтя: сайд-эффекты и порядок импортов.** > Если ваш код полагается на сайд-эффекты, происходящие в момент импорта (например, регистрация плагинов, monkey-patching), то с `lazy import` эти эффекты произойдут не на старте, а в непредсказуемый момент первого использования. Это может сломать логику. > > **Вердикт:** `lazy import` — мощнейший инструмент для оптимизации, но его нужно применять с умом, понимая, что он меняет не только *когда*, но и *если* модуль будет загружен. Для обратной совместимости и плавного перехода ввели глобальную переменную `__lazy_modules__`, позволяющую объявить список модулей для ленивой загрузки без изменения синтаксиса `import`. Также есть глобальный флаг `-X lazy_imports=all` для экспериментов. ## 📊 PEP 799: Профилировщик для продакшена, а не для "Hello, World" `cProfile`. Он прост и понятен. Но у него есть фатальный недостаток: колоссальный оверхед. Он инструментирует каждый вызов функции, замедляя код в десятки раз. Запускать его на живом, нагруженном продакшен-сервере — это самоубийство. PEP 799 вводит в стандартную библиотеку новый модуль `profiling` со статистическим семплирующим профилировщиком. **В чем разница?** Вместо того чтобы отслеживать *каждый* вызов, семплер периодически (с очень высокой частотой, до 1,000,000 раз в секунду) делает "снимки" стека вызовов запущенного Python-процесса. Затем он анализирует тысячи этих снимков и статистически определяет, в каких функциях код проводит больше всего времени. **Ключевые фичи:** * **Нулевой оверхед:** Его можно подключить к *любому* запущенному Python-процессу, не влияя на его производительность. * **Без модификации кода:** Не нужно ничего менять в приложении и перезапускать его. * **Гибкость:** Можно профилировать все потоки или только главный, выводить результаты в разных форматах, включая данные для flamegraph. ### Как им пользоваться? Элементарно. Узнаем PID нужного процесса и запускаем профилировщик из командной строки. ```bash # Профилировать процесс с PID 1234 в течение 10 секунд python -m profiling.sampling 1234 # Профилировать 30 секунд с интервалом 50 микросекунд и сохранить в файл python -m profiling.sampling -i 50 -d 30 -o profile.stats 1234 # Сгенерировать "схлопнутые" стеки для flamegraph python -m profiling.sampling --collapsed 1234 ``` Вывод профилировщика — это подробная таблица, которая сразу подсвечивает "горячие" точки в коде. ``` Captured 498841 samples in 5.00 seconds Sample rate: 99768.04 samples/sec Error rate: 0.72% Profile Stats: nsamples sample% tottime (s) cumul% cumtime (s) filename:lineno(function) 43/418858 0.0 0.000 87.9 4.189 case.py:667(TestCase.run) 3293/418812 0.7 0.033 87.9 4.188 case.py:613(TestCase._callTestMethod) 158562/158562 33.3 1.586 33.3 1.586 test_compile.py:725(check_limit) 129553/129553 27.2 1.296 27.2 1.296 ast.py:46(parse) ``` **Вердикт:** Это просто пушка. 🔥 Наличие такого инструмента в стандартной библиотеке выводит Python на новый уровень в плане отладки производительности в реальных условиях. Это тот инструмент, которого не хватало DevOps и SRE-инженерам, работающим с Python. ## 📜 PEP 686: UTF-8 по умолчанию — прощай, зоопарк кодировок Вы точно сталкивались с `UnicodeDecodeError`. Особенно если вы работаете на Windows, а ваши коллеги — на Linux или macOS. Файл с кириллицей, созданный в одной ОС, отказывался читаться в другой без явного указания кодировки. Это приводило к золотому правилу: **всегда пиши `open(..., encoding='utf-8')`**. Так вот, эту эпоху можно считать законченной. > [!NOTE] > Исторически Python полагался на системную локаль для определения кодировки по умолчанию. На Linux это чаще всего была UTF-8, а вот на Windows — cp1251 для русского языка. Этот разнобой и был источником бесконечных проблем при переносе кода и данных. С версии 3.15 Python перестает оглядываться на операционную систему. **UTF-8 становится кодировкой по умолчанию для всех файловых операций**, где она не указана явно. ```python # Раньше (и до 3.15) этот код мог использовать cp1251 на Windows with open('my_file.txt', 'w') as f: f.write('Привет, мир!') # Теперь этот код ВСЕГДА будет использовать UTF-8 with open('my_file.txt', 'w') as f: f.write('Привет, мир!') ``` ### Что это значит для нас? * **Надежность:** Код становится более предсказуемым и переносимым. Скрипт, написанный на macOS, без проблем заработает на Windows-сервере, и наоборот. * **Меньше бойлерплейта:** Можно реже писать `encoding='utf-8'`, хотя для обратной совместимости и явности это все еще хорошая практика. Если вам по какой-то причине нужно старое поведение (например, для работы с легаси-системами), есть "аварийные выходы": * Переменная окружения `PYTHONUTF8=0`. * Флаг командной строки `-X utf8=0`. * Явное указание `encoding='locale'` для использования системной кодировки (доступно с Python 3.10). **Вердикт:** Это маленькое изменение, которое исправляет давнюю головную боль. Python становится еще более дружелюбным к вебу и кроссплатформенной разработке. ## 🧠 Улучшенные сообщения об ошибках: Python стал умнее Мелочь, а приятно. Интерпретатор продолжает становиться умнее и старается не просто констатировать факт ошибки, а подсказать возможное решение. ### AttributeError с подсказками по вложенным объектам Представьте, у вас есть класс-контейнер, который хранит внутри другой объект. И вы по ошибке пытаетесь обратиться к атрибуту внутреннего объекта напрямую. ```python from dataclasses import dataclass from math import pi @dataclass class Circle: radius: float @property def area(self) -> float: return pi * self.radius**2 class Container: def __init__(self, inner: Circle) -> None: self.inner = inner circle = Circle(radius=4.0) container = Container(circle) print(container.area) # Ошибка! ``` **Раньше** мы бы получили сухое: `AttributeError: 'Container' object has no attribute 'area'`. И пошли бы дебажить. **Теперь в Python 3.15:** ``` Traceback (most recent call last): ... AttributeError: 'Container' object has no attribute 'area'. Did you mean: 'inner.area'? ``` Интерпретатор заглянул внутрь `container.inner` и понял, что мы, скорее всего, хотели обратиться именно туда. Экономия времени налицо. ### Подсказки для `delattr()` То же самое теперь работает и для `delattr()`. Если вы опечатались в имени атрибута при удалении, Python предложит правильный вариант. ```python class A: pass a = A() a.abcde = 1 delattr(a, 'abcdf') # Опечатка в последней букве ``` **Результат в 3.15:** ``` Traceback (most recent call last): ... AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? ``` Эти улучшения, которые делают процесс разработки чуть менее болезненным, особенно для новичков. ## 📦 Что еще интересного: россыпь полезных мелочей Помимо трех китов, в Python 3.15 есть масса других улучшений в стандартной библиотеке. Вот самые заметные: ### `collections` * Для `Counter` добавили операции `__xor__` и `__ixor__` (операторы `^` и `^=`), которые вычисляют симметрическую разность (элементы, которые есть в одном счетчике или в другом, но не в обоих). ### `sqlite3` * Командный интерфейс (`python -m sqlite3`) стал гораздо удобнее: * **Автодополнение SQL-ключевых слов** по Tab. * Цветной вывод для промптов, ошибок и помощи. * Автодополнение имен таблиц, индексов, колонок и т.д. ### `math` * Появился новый модуль `math.integer` с математическими функциями специально для целых чисел (PEP 791). * Добавлены функции `math.isnormal()`, `math.issubnormal()`, `math.fmax()`, `math.fmin()` и `math.signbit()`, знакомые тем, кто работает с C. ### `argparse` * Параметр `suggest_on_error` у `ArgumentParser` теперь по умолчанию `True`. Это значит, что если пользователь опечатался в имени аргумента, `argparse` по умолчанию предложит ему правильный вариант.
3. **Экономия памяти:** Модули, которые так и не были использованы за время жизни процесса, никогда не будут загружены в память. Для долгоживущих приложений с кучей редко используемых фич это может быть серьезным выигрышем. > [!WARNING] > **Ложка дегтя: сайд-эффекты и порядок импортов.** > Если ваш код полагается на сайд-эффекты, происходящие в момент импорта (например, регистрация плагинов, monkey-patching), то с `lazy import` эти эффекты произойдут не на старте, а в непредсказуемый момент первого использования. Это может сломать логику. > > **Вердикт:** `lazy import` — мощнейший инструмент для оптимизации, но его нужно применять с умом, понимая, что он меняет не только *когда*, но и *если* модуль будет загружен. Для обратной совместимости и плавного перехода ввели глобальную переменную `__lazy_modules__`, позволяющую объявить список модулей для ленивой загрузки без изменения синтаксиса `import`. Также есть глобальный флаг `-X lazy_imports=all` для экспериментов. ## 📊 PEP 799: Профилировщик для продакшена, а не для "Hello, World" `cProfile`. Он прост и понятен. Но у него есть фатальный недостаток: колоссальный оверхед. Он инструментирует каждый вызов функции, замедляя код в десятки раз. Запускать его на живом, нагруженном продакшен-сервере — это самоубийство. PEP 799 вводит в стандартную библиотеку новый модуль `profiling` со статистическим семплирующим профилировщиком. **В чем разница?** Вместо того чтобы отслеживать *каждый* вызов, семплер периодически (с очень высокой частотой, до 1,000,000 раз в секунду) делает "снимки" стека вызовов запущенного Python-процесса. Затем он анализирует тысячи этих снимков и статистически определяет, в каких функциях код проводит больше всего времени. **Ключевые фичи:** * **Нулевой оверхед:** Его можно подключить к *любому* запущенному Python-процессу, не влияя на его производительность. * **Без модификации кода:** Не нужно ничего менять в приложении и перезапускать его. * **Гибкость:** Можно профилировать все потоки или только главный, выводить результаты в разных форматах, включая данные для flamegraph. ### Как им пользоваться? Элементарно. Узнаем PID нужного процесса и запускаем профилировщик из командной строки. ```bash # Профилировать процесс с PID 1234 в течение 10 секунд python -m profiling.sampling 1234 # Профилировать 30 секунд с интервалом 50 микросекунд и сохранить в файл python -m profiling.sampling -i 50 -d 30 -o profile.stats 1234 # Сгенерировать "схлопнутые" стеки для flamegraph python -m profiling.sampling --collapsed 1234 ``` Вывод профилировщика — это подробная таблица, которая сразу подсвечивает "горячие" точки в коде. ``` Captured 498841 samples in 5.00 seconds Sample rate: 99768.04 samples/sec Error rate: 0.72% Profile Stats: nsamples sample% tottime (s) cumul% cumtime (s) filename:lineno(function) 43/418858 0.0 0.000 87.9 4.189 case.py:667(TestCase.run) 3293/418812 0.7 0.033 87.9 4.188 case.py:613(TestCase._callTestMethod) 158562/158562 33.3 1.586 33.3 1.586 test_compile.py:725(check_limit) 129553/129553 27.2 1.296 27.2 1.296 ast.py:46(parse) ``` **Вердикт:** Это просто пушка. 🔥 Наличие такого инструмента в стандартной библиотеке выводит Python на новый уровень в плане отладки производительности в реальных условиях. Это тот инструмент, которого не хватало DevOps и SRE-инженерам, работающим с Python. ## 📜 PEP 686: UTF-8 по умолчанию — прощай, зоопарк кодировок Вы точно сталкивались с `UnicodeDecodeError`. Особенно если вы работаете на Windows, а ваши коллеги — на Linux или macOS. Файл с кириллицей, созданный в одной ОС, отказывался читаться в другой без явного указания кодировки. Это приводило к золотому правилу: **всегда пиши `open(..., encoding='utf-8')`**. Так вот, эту эпоху можно считать законченной. > [!NOTE] > Исторически Python полагался на системную локаль для определения кодировки по умолчанию. На Linux это чаще всего была UTF-8, а вот на Windows — cp1251 для русского языка. Этот разнобой и был источником бесконечных проблем при переносе кода и данных. С версии 3.15 Python перестает оглядываться на операционную систему. **UTF-8 становится кодировкой по умолчанию для всех файловых операций**, где она не указана явно. ```python # Раньше (и до 3.15) этот код мог использовать cp1251 на Windows with open('my_file.txt', 'w') as f: f.write('Привет, мир!') # Теперь этот код ВСЕГДА будет использовать UTF-8 with open('my_file.txt', 'w') as f: f.write('Привет, мир!') ``` ### Что это значит для нас? * **Надежность:** Код становится более предсказуемым и переносимым. Скрипт, написанный на macOS, без проблем заработает на Windows-сервере, и наоборот. * **Меньше бойлерплейта:** Можно реже писать `encoding='utf-8'`, хотя для обратной совместимости и явности это все еще хорошая практика. Если вам по какой-то причине нужно старое поведение (например, для работы с легаси-системами), есть "аварийные выходы": * Переменная окружения `PYTHONUTF8=0`. * Флаг командной строки `-X utf8=0`. * Явное указание `encoding='locale'` для использования системной кодировки (доступно с Python 3.10). **Вердикт:** Это маленькое изменение, которое исправляет давнюю головную боль. Python становится еще более дружелюбным к вебу и кроссплатформенной разработке. ## 🧠 Улучшенные сообщения об ошибках: Python стал умнее Мелочь, а приятно. Интерпретатор продолжает становиться умнее и старается не просто констатировать факт ошибки, а подсказать возможное решение. ### AttributeError с подсказками по вложенным объектам Представьте, у вас есть класс-контейнер, который хранит внутри другой объект. И вы по ошибке пытаетесь обратиться к атрибуту внутреннего объекта напрямую. ```python from dataclasses import dataclass from math import pi @dataclass class Circle: radius: float @property def area(self) -> float: return pi * self.radius**2 class Container: def __init__(self, inner: Circle) -> None: self.inner = inner circle = Circle(radius=4.0) container = Container(circle) print(container.area) # Ошибка! ``` **Раньше** мы бы получили сухое: `AttributeError: 'Container' object has no attribute 'area'`. И пошли бы дебажить. **Теперь в Python 3.15:** ``` Traceback (most recent call last): ... AttributeError: 'Container' object has no attribute 'area'. Did you mean: 'inner.area'? ``` Интерпретатор заглянул внутрь `container.inner` и понял, что мы, скорее всего, хотели обратиться именно туда. Экономия времени налицо. ### Подсказки для `delattr()` То же самое теперь работает и для `delattr()`. Если вы опечатались в имени атрибута при удалении, Python предложит правильный вариант. ```python class A: pass a = A() a.abcde = 1 delattr(a, 'abcdf') # Опечатка в последней букве ``` **Результат в 3.15:** ``` Traceback (most recent call last): ... AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? ``` Эти улучшения, которые делают процесс разработки чуть менее болезненным, особенно для новичков. ## 📦 Что еще интересного: россыпь полезных мелочей Помимо трех китов, в Python 3.15 есть масса других улучшений в стандартной библиотеке. Вот самые заметные: ### `collections` * Для `Counter` добавили операции `__xor__` и `__ixor__` (операторы `^` и `^=`), которые вычисляют симметрическую разность (элементы, которые есть в одном счетчике или в другом, но не в обоих). ### `sqlite3` * Командный интерфейс (`python -m sqlite3`) стал гораздо удобнее: * **Автодополнение SQL-ключевых слов** по Tab. * Цветной вывод для промптов, ошибок и помощи. * Автодополнение имен таблиц, индексов, колонок и т.д. ### `math` * Появился новый модуль `math.integer` с математическими функциями специально для целых чисел (PEP 791). * Добавлены функции `math.isnormal()`, `math.issubnormal()`, `math.fmax()`, `math.fmin()` и `math.signbit()`, знакомые тем, кто работает с C. ### `argparse` * Параметр `suggest_on_error` у `ArgumentParser` теперь по умолчанию `True`. Это значит, что если пользователь опечатался в имени аргумента, `argparse` по умолчанию предложит ему правильный вариант. ### `difflib` * `difflib.unified_diff()` теперь умеет выводить цветной дифф, как в `git diff`. * HTML-страницы, генерируемые `HtmlDiff`, стали современнее (HTML5) и поддерживают темную тему. И это лишь верхушка айсберга. Обновления коснулись `os`, `ssl`, `hashlib`, `unittest` и многих других модулей. Также было удалено много старого, ранее помеченного как deprecated, кода. ## 🚮 Deprecations и удаления: время прощаться Как и в любом релизе, происходит чистка. Python 3.15 убирает много старых и неиспользуемых вещей и помечает новые для будущего удаления. * **Удалено:** `CGIHTTPRequestHandler` (да, кто-то им еще пользовался?), `platform.java_ver()`, `pathlib.PurePath.is_reserved()`, старый синтаксис для `NamedTuple` и `TypedDict`, и многое другое. * **Помечено как устаревшее:** * Опции командной строки `-b` и `-bb` (проверка сравнения `bytes` и `str`). Они были актуальны при переходе с Python 2, сейчас их пользу видят в основном тайп-чекеры. * Атрибут `__version__` во многих модулях стандартной библиотеки (`json`, `csv`, `re` и др.). Вместо него следует использовать `sys.version_info`. * Старая система политик `asyncio` (`get_event_loop_policy`). Полный список огромен, и если вы поддерживаете старый код, стоит заглянуть в [официальный чейнджлог](https://docs.python.org/3.15/whatsnew/changelog.html#changelog). ## 🎯 Вердикт: Обновляться или нет? Python 3.15 — это релиз про качество жизни. Он не ломает язык, но исправляет исторические недочеты. **Однозначно стоит обновляться, если:** 1. **Вы пишете CLI-утилиты или большие приложения**, где скорость запуска имеет значение. `lazy import` — это ваш новый лучший друг. 2. **Вы занимаетесь поддержкой высоконагруженных систем.** Новый семплирующий профилировщик позволит находить узкие места в проде без риска что-либо уронить. 3. **Вы устали от проблем с кодировками.** UTF-8 по умолчанию — это просто бальзам на душу. Даже если эти пункты вас не затрагивают напрямую, улучшения в сообщениях об ошибках и стандартной библиотеке делают новую версию привлекательной для всех. Первая альфа-версия уже вышла, а значит, стабильный релиз не за горами.