Сегодня вышла шестая альфа-версия 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 — Сопоставление структурных шаблонов: Учебное пособие
* Запрос на извлечение, содержащий изменения