Сегодня вышла шестая альфа-версия Python 3.10 (финальный релиз запланирован на октябрь). Он представляет (с PEP 634) новую замечательную функцию — сопоставление с образцом. Посмотрим, как это работает!
Как переключатель
Синтаксис немного напоминает оператор switch
, известный из других языков программирования:
from random import randint day_of_week = randint(1, 7) match day_of_week: case 1: print(“So we’re starting…”) case 5: print(“Yeah, friday!”) case _: print(f”{day_of_week}. day of the week”)
Итак, мы проверяем значения сверху вниз, есть что-то вроде метки default
(символ _
wildcard). Другими словами, мы можем легко заменить многие структуры if…elif…else
более простым синтаксисом.
…но гораздо лучше
К счастью, сопоставление с образцом имеет гораздо больший потенциал! Я почти уверен, что разработчики программного обеспечения, использующие функциональные языки программирования (такие как Haskell, Scala или OCaml), уже понимают, что я имею в виду.
Теперь мы можем использовать Python для сопоставления еще нескольких интересных шаблонов:
match mysterious: case 1, y: doSomething() case (_, y): doSomethingElse() case str(pair) if len(pair.split(“,”)) == 2: processString() case Pair(10, _) as pair: usePair(pair)
Работа с разными данными
Сопоставление с образцом можно использовать с различными типами данных, начиная со списков, через словари и заканчивая типами, определенными пользователем. В следующих абзацах я покажу вам несколько полезных примеров.
Списки
Когда мы не знаем длину списка, с которым имеем дело, на помощь приходит сопоставление с образцом.
Код стал более читабельным, так как нам не нужно явно вызывать метод len()
(разумеется, этот метод используется «под капотом»).
match some_list: case []: print(“Empy list”) case [x]: print(f”Single-element list: {x}”) case [x, y]: print(f”List containing only two elements: {x} and {y}”) case [x, y, z]: print(f”List z three elements: {x}, {y} and {z}”) case [x, y, *tail]: print(f”List with more than three elements. Here are the first two: {x} and {y}”)
Дикты
Синтаксис для dicts также имеет тенденцию быть довольно интуитивным. Мы можем сопоставить определенные ключи и значения, как в примере ниже:
match employee_record: case {“age”: age, **personal_data}: employee.set_year_of_birth(age=age) employee.update_personal_data(personal_data) case {“position”: “engineer”, “salary”: salary}: update_salary(employee, salary) case dict(x) if not x: raise Exception(“no data to process”)
Обратите внимание, что случай {“key”: value}
не означает, что будет найден только одноэлементный словарь. Он будет соответствовать любому словарю, содержащему ”key”
, но длина этого словаря не имеет значения.
Вот почему мы используем регистр dict(x) if not x
для сопоставления с пустым словарем. Мы могли бы также использовать, например, case {} as x if not x
, но не case {}
. Последний будет соответствовать любому диктору, а не только пустому.
Более интересным является использование нескольких вложенных подшаблонов:
x = [1, {“foo”: “bar”, “foo2”: (10, 20)}, 3, 4] match x: case [1, {“foo”: val} as d, *_]: print(f”Foo value: {val}, whole dict: {d}”) case _: print(“Not matched”)
Представьте, как вам пришлось бы реализовать извлечение значения ”foo”
без сопоставления с образцом. Держу пари, что это не будет более читаемым.
Пользовательские типы
Сопоставление классов данных — самый простой случай среди определяемых пользователем типов. Чтобы выполнить объектную деконструкцию, мы используем синтаксис, который намеренно подобен синтаксису объектной конструкции:
from dataclasses import dataclass @dataclass class Pair: first: int second: int pair = Pair(10, 20) match pair: case Pair(0, x): print(“Case #1”) case Pair(x, y) if x == y: print(“Case #2”) case Pair(first=x, second=20): print(“Case #3”) case Pair as p: print(“Case #4”)
Как видите, мы можем использовать как ключевые слова, так и позиционные аргументы. Ситуация меняется, когда дело доходит до пользовательских типов, которые не используют декоратор @dataclass
:
class Pair: def __init__(self, first: int, second: int): self.first = first self.second = second
В таком случае допускаются только аргументы ключевого слова. Таким образом, вы можете написать случай Pair(first=x, second=y)
или case Pair as p
, но case Pair(x, y)
вызовет следующее исключение:
TypeError: Pair() accepts 0 positional sub-patterns (2 given)
Что мы можем с этим сделать? Используйте атрибут __match_args__
! Он определяет позиционные параметры и их порядок, поэтому для класса ниже мы можем использовать те же шаблоны, что и для dataclass
.
class Pair: __match_args__ = [“first”, “second”] def __init__(self, first: int, second: int): self.first = first self.second = second
Предостережения
В большинстве случаев поведение сопоставления с образцом в Python довольно интуитивно понятно. Однако есть несколько вещей, о которых вам нужно знать, чтобы избежать ошибок в ваших приложениях.
Подстановочные знаки
Как мы знаем из предыдущих примеров, разрешены следующие две синтаксические конструкции:
* case {“age”: age, **personal_data}
* case [x, *_]
В обоих случаях мы сопоставляем остальные элементы контейнера. Переменная personal_data
будет словарем, содержащим все элементы dict, кроме ”age”
, а подстановочный знак *_
означает, что мы ожидаем список длиной ›= 1.
Однако синтаксис case {“age”: age, **_}
запрещен. Почему? Это было бы лишним, так как тот же case {“age”: age}
означает «словарь, содержащий минимум один элемент — age
».
Константы
Вторая проблема — использование констант в шаблонах. Посмотрите на пример ниже и попробуйте угадать, что получится в результате его выполнения:
HTTP_404 = 404 HTTP_500 = 500 response = (500, “error”) match response: case HTTP_404, content: print(“Not found”) case HTTP_500, content: print(“Server error”)
Будет напечатано сообщение «Не найдено»! Более того, переменная HTTP_404
будет содержать значение 500
. Удивлен? Вот как работает связывание переменных в сопоставлении с образцом Python.
Если вы хотите использовать константы внутри шаблона, вам нужно использовать так называемые «точечные константы». Это может быть член enum
или любого другого класса, например:
class Error: HTTP_404 = 404 HTTP_500 = 500 response = (500, “error”) match response: case Error.HTTP_404, content: print(“Not found”) case Error.HTTP_500, content: print(“Server error”)
Теперь, наконец, программа напечатает «Ошибка сервера», и обе наши константы не изменят значения.
Попробуй сам
Если вы хотите поиграться с сопоставлением с образцом до финального релиза Python 3.10, то можете скачать версию 3.10 alpha 6 с официального сайта.
В качестве альтернативы вы можете использовать веб-сайт mybinder.org, который позволяет использовать интерактивные блокноты Python. Нажмите здесь, чтобы открыть файл, подготовленный Гвидо ван Россумом и использующий экспериментальную версию Python 3.10.
Резюме
Сопоставление с образцом — это функция, специфичная для языков функционального программирования, таких как Haskell, OCaml или Scala, но она также присутствует во многих мультипарадигмальных языках — C#, Ruby или Rust.
Лично я очень рад, что Python присоединяется к этому ряду и становится еще более функциональным. И мне не терпится избавиться от ifs
в пользу сопоставления с образцом. ;)
Ссылки
* Python 3.10.0a6 — скачать
* Учебник Гвидо ван Россума
* PEP 634 — Сопоставление структурных шаблонов: Спецификация
* PEP 635 — Сопоставление структурных шаблонов: Мотивация и обоснование
* PEP 636 — Сопоставление структурных шаблонов: Учебное пособие
* Запрос на извлечение, содержащий изменения