Разработчики представили новую версию языка программирования Python 3.10 — она будет поддерживаться не менее следующих пяти лет. Мы вместе с преподавателем Хекслета Валентином Хомутенко рассказываем, что нового в версии Python 3.10 и почему ее так долго ждали разработчики.
Python 3.10
— В модуль typing был добавлен оператор Concatenate вместе с переменной ParamSpec, это позволит передавать дополнительную информацию для статической проверки типов при использовании Callable. Модуль typing теперь также имеет специальные значения TypeGuard для аннотирования функций защиты типов и TypeAlias для явного определения псевдонима типа.
StrCache: TypeAlias = 'Cache[str]' # a type alias
Кроме того, в модуль typing также добавлен новый оператор, позволяющий использовать синтаксис "X | Y" для выбора одного из типов (тип X или тип Y).
def square(number: int | float) -> int | float:
return number ** 2
эквивалентно ранее поддерживаемой конструкции:
def square(number: Union[int, float]) -> Union[int, float]:
return number ** 2
— В Python 3.10 появилась возможность использовать круглые скобки в операторе with для разнесения на несколько строк определения коллекции контекстных менеджеров. Тут же можно добавить, что теперь разрешается оставлять запятую после работы с финальным контекстным менеджером в группе:
with (
CtxManager1() as example1,
CtxManager2() as example2,
CtxManager3() as example3,
):
...
— Достаточно сильно улучшена история с информированием разработчика об ошибках в коде. Например, теперь при незакрытой фигурной скобки вместо обычной ошибки синтаксиса будет выводиться подсветка открывающей скобки и указание, что не получается найти закрывающую скобку.
Тут же можно отметить, что для отладочных инструментов теперь событие будет указываться точно вплоть до строк выполненного кода.
— Функция zip()
теперь имеет необязательный флаг "strict", при указании которого проводится проверка на одинаковую длину перебираемых аргументов.
>>> list(zip(('a', 'b', 'c'), (1, 2, 3), strict=True))
[('a', 1), ('b', 2), ('c', 3)]
>>> list(zip(range(3), ['fee', 'fi', 'fo', 'fum'], strict=True))
Traceback (most recent call last):
...
ValueError: zip() argument 2 is longer than argument 1
Среди других обновлений в функциях — появились встроенные функции aiter() и anext() с реализацией асинхронных аналогов функциям iter()
и next()
. Кроме того, работа конструкторов str()
, bytes()
и bytearray()
была увеличена на 40% при работе с мелкими объектами.
Читайте также: Современный PHP сформирует хорошее мышление в стиле ООП: интервью с Максимом Шамаевым
Pattern matching как главное в новом релизе
По словам преподавателя Хекслета Валентина Хомутенко, главное обновление этого релиза — функция pattern matching. Вы могли где-то слышать, что «в Python теперь есть switch case», но это не совсем так. Pattern matching — намного круче, чем обычный switch в других языках.
Предположим, что нам нужно написать функцию, которая читает содержимое текстовых файлов. При этом если файл имеет расширение .md
, то она должна попробовать прочитать его как markdown-документ. Если расширение .docx
, то как документ Microsoft Office. Во всех остальных случаях надо прочитать его как обычный текстовый файл.
Такую логику можно реализовать помощью обычного if
.
import pathlib
def read_document(file_path):
with open(file_path) as file:
extension = pathlib.Path(file_path).suffix.lower():
if extension == '.md':
return parse_md(file)
if extension == '.docx':
return parse_docx(file)
return parse_plain(file)
А вот как такая же задача решается с помощью pattern matching.
import pathlib
def read_document(file_path):
with open(file_path) as file:
match pathlib.Path(file_path).suffix.lower():
case '.md':
return parse_md(file)
case '.docx':
return parse_docx(file)
case _:
return parse_plain(file)
Окей, стало чуть компактнее, но кажется не критично.
Также полезно: С чего начать изучение Python и как написать на нем первый код
После этого мы поняли, что пользователи часто передают пути к картинкам и другим бинарным файлам, которые нельзя прочитать как обычный текст. Давайте модифицируем функцию так, чтобы как обычный текст читались только файлы с расширениями .txt
или .log
.
import pathlib
def read_document(file_path):
with open(file_path) as file:
extension = pathlib.Path(file_path).suffix.lower():
if extension == '.md':
return parse_md(file)
if extension == '.docx':
return parse_docx(file)
if extension in ['.txt', '.log']:
return parse_plain(file)
raise ValueError('unsupported extension {}'.format(extension))
И вариант с pattern matching:
import pathlib
def read_document(file_path):
with open(file_path) as file:
match pathlib.Path(file_path).suffix.lower():
case '.md':
return parse_md(file)
case '.docx':
return parse_docx(file)
case '.txt' | '.log':
return parse_plain(file)
case _ as extension:
raise ValueError('unsupported extension {}'.format(extension))
Обратите внимание, что мы обработали сразу два варианта .txt
и .log
в одной ветке case
, используя оператор |
. Уже выглядит получше, особенно если сравнить с обычными if
.
Помимо проверки на конкретное значение, теперь мы можем совершать и более сложные штуки. Например, мы разрабатываем утилиту для работы с файлами. Нам нужно обработать одну из команд пользователя: «скопировать файл» или «удалить файл». Команда копирования выглядит как copy old_path new_path
, а удаления — delete path
.
Код с обычными if выглядит уже не очень лаконично.
def handle_command(command):
command_parts = command.split():
if len(command_parts) == 3 and command_parts[0] == 'copy':
copy(command_parts[1], command_parts[2])
elif len(command_parts) == 2 and command_parts[0] == 'delete':
delete(path)
case _:
raise ValueError('unsupported command')
А вот так выглядит решение с pattern matching:
def handle_command(command):
# делим строку по пробелам, чтобы получить составные части
match command.split():
case ['copy', old_path, new_path]:
copy(old_path, new_path)
case ['delete', path]:
delete(path)
case _:
raise ValueError('unsupported command')
Pattern matching позволяет здесь проверять сразу 2 вещи: количество элементов списка и конкретное значение первого элемента. И все это в рамках одной ветки case без лишних вложенностей или соединения условий через or
/ and
.
Кроме списков, мы можем так просто работать и со словарями. Пусть команда из предыдущего примера теперь передается не строкой, а словарём. Команда копирования будет выглядеть так:
{
"action": "copy",
"args": ["/old/path", "new_path"],
}
А команда удаление так:
{
"action": "delete",
"args": ["/old/path"],
}
Код обработки команды не стал сильно сложнее, если мы используем pattern matching.
def handle_command(command):
match command:
case {"action": "copy", "args": [old_path, new_path]}:
copy(old_path, new_path)
case {"action": "delete", "args": [path]}:
delete(path)
case _:
raise ValueError('unsupported command')
И опять же pattern matching позволяет нам сделать сразу много проверок в одном выражении:
- Существование ключа
action
- Значение ключа action —
copy
илиdelete
- Существование ключа
args
- Тип значения у ключа
args
— значение должно быть массивом. - Количество элементов внутри
args
.
И все это в одном выражении case :tada:
. Если вы хотите подробнее разобраться в этом и прочувствовать всю магию этой функции, то попробуйте в качестве упражнения переписать этот пример без использования pattern matching.
Больше примеров использования можно увидеть в PEP636, а в PEP634 можно посмотреть все детали нового синтаксиса.
Никогда не останавливайтесь: В программировании говорят, что нужно постоянно учиться даже для того, чтобы просто находиться на месте. Развивайтесь с нами — на Хекслете есть сотни курсов по разработке на разных языках и технологиях