match-case: structural pattern matching

С Python 3.10 в язык добавлена конструкция match-case (PEP 634). Это не просто аналог switch - это полноценный pattern matching, заимствованный из языков семейства ML. Он умеет сопоставлять не только значения, но и структуру: типы, литералы, классы, последовательности, словари. В этом уроке разберём базовое и продвинутое использование.

Зачем нужен ещё один способ ветвления

Цепочка elif для проверки разных вариантов работает, но многословна:

def describe(value):
    if isinstance(value, int) and value > 0:
        return "положительное число"
    elif isinstance(value, int) and value == 0:
        return "ноль"
    elif isinstance(value, int):
        return "отрицательное число"
    elif isinstance(value, str):
        return "строка"
    elif isinstance(value, list):
        return "список"
    else:
        return "неизвестно"

С match:

def describe(value):
    match value:
        case int() if value > 0:
            return "положительное число"
        case 0:
            return "ноль"
        case int():
            return "отрицательное число"
        case str():
            return "строка"
        case list():
            return "список"
        case _:
            return "неизвестно"

Короче и декларативнее.

Базовый синтаксис

match expression:
    case pattern1:
        # код
    case pattern2:
        # код
    case _:
        # default (wildcard)

_ - wildcard pattern, ловит что угодно (аналог default).

Литералы

match status_code:
    case 200:
        return "OK"
    case 404:
        return "Not Found"
    case 500:
        return "Server Error"
    case _:
        return "Unknown"

Можно объединять через |:

match status_code:
    case 200 | 201 | 204:
        return "Success"
    case 400 | 401 | 403 | 404:
        return "Client Error"
    case 500 | 502 | 503:
        return "Server Error"

Связывание переменных (capture)

В образце можно захватить значение в переменную:

match point:
    case (0, 0):
        return "В начале координат"
    case (x, 0):
        return f"На оси X в {x}"
    case (0, y):
        return f"На оси Y в {y}"
    case (x, y):
        return f"В точке ({x}, {y})"

В case (x, 0) - литерал 0 должен совпасть, x захватит первое значение. В case (x, y) - оба захватываются.

```python status = 200 NOT_FOUND = 404

match status: case NOT_FOUND: # ВНИМАНИЕ: это capture, не сравнение print("404") case 200: print("OK")


Здесь `case NOT_FOUND:` - не «сравнить с константой 404», а **захватить значение в переменную NOT_FOUND**! Этот case поймает любое значение и затмит константу. Для сравнения с константой используй точечную нотацию: `case constants.NOT_FOUND:` или сделай [Enum](./31-dataclass-and-enum.md).
</InfoCard>

## Guard - дополнительное условие

После образца можно добавить `if`:

```python
match age:
    case n if n < 0:
        return "Возраст не может быть отрицательным"
    case n if n < 18:
        return "Несовершеннолетний"
    case n if n < 65:
        return "Взрослый"
    case _:
        return "Пенсионер"

Guard проверяется после сопоставления образца.

Структурные образцы: последовательности

match command:
    case []:
        print("Пустая команда")
    case [single]:
        print(f"Одна: {single}")
    case [first, second]:
        print(f"Две: {first}, {second}")
    case [first, *rest]:
        print(f"Первый: {first}, остальные: {rest}")

*rest захватывает остаток списка. Это похоже на распаковку.

Структурные образцы: словари

match request:
    case {"action": "create", "data": data}:
        create(data)
    case {"action": "delete", "id": id}:
        delete(id)
    case {"action": "list"}:
        list_all()
    case _:
        raise ValueError("Unknown action")

Образец словаря частичный: проверяет наличие указанных ключей, другие ключи допустимы. Если хочешь точного совпадения - проверяй длину или используй явное сравнение.

Образцы классов

Это самая мощная часть match. Сопоставляем по типу и атрибутам:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

@dataclass
class Square:
    top_left: Point
    side: float

def area(shape):
    match shape:
        case Circle(radius=r):
            return 3.14 * r ** 2
        case Square(side=s):
            return s * s
        case Point():
            return 0
        case _:
            raise TypeError("Unknown shape")

В образце Circle(radius=r) мы говорим: «должен быть Circle, и атрибут radius захвати в переменную r».

Для классов с __match_args__ (это автоматически у dataclass, см. также урок про dunder-методы) можно использовать позиционный синтаксис:

match shape:
    case Point(0, 0):
        print("Начало координат")
    case Point(x, y):
        print(f"({x}, {y})")

Вложенные образцы

Образцы можно вкладывать произвольно:

match command:
    case {"type": "move", "to": Point(x, y)}:
        move_to(x, y)
    case {"type": "scale", "factor": float(f) | int(f)}:
        scale(f)
    case [Circle(_, r1), Circle(_, r2)]:
        compare_circles(r1, r2)

Когда использовать match-case

Хорошо подходит для:

  • Парсинг JSON / API-ответов с разной структурой
  • AST-обходы (компиляторы, линтеры)
  • Стейт-машины
  • Discriminated unions (тип + данные)
  • Структурированных команд в CLI

Когда обычный if/elif лучше:

  • Простое сравнение значения с константами (читать привычнее)
  • Однократные проверки
  • Когда нужна совместимость с Python <3.10

Антипаттерны

1. Использование как простой switch:

# Можно и через match, но обычный elif короче
match level:
    case "debug":
        log_debug()
    case "info":
        log_info()
    case _:
        log_unknown()

# Или ещё короче - dict-маппинг
loggers = {"debug": log_debug, "info": log_info}
loggers.get(level, log_unknown)()

2. Захват вместо сравнения:

SUCCESS = 200

match status:
    case SUCCESS:   # это capture - поймает ВСЁ
        print("OK")

Правильно:

from enum import IntEnum

class Status(IntEnum):
    SUCCESS = 200

match status:
    case Status.SUCCESS:   # точечная нотация - сравнение
        print("OK")

Совместимость

match-case доступен только с Python 3.10+. Для старых версий используй elif или dict-маппинг.

Если проект должен работать на Python 3.9, лучше не использовать match - получишь SyntaxError на парсинге. Это не runtime-ошибка, и старый интерпретатор даже не сможет загрузить файл.

Сравнение с Go

В Go есть switch, мощнее чем в C: можно сравнивать любые типы, использовать выражения в case, проверять тип через type switch. Но это всё ещё именно switch - сравнение значений и типов. Structural pattern matching как в Python (деструктуризация списков, словарей, классов) в Go нет. Для похожих задач в Go обычно используют type assertions и тяжёлые цепочки if.

Pattern matching в Python ближе к академическим языкам функционального семейства, где такие конструкции исторически развивались десятилетиями.

Мини-задание

  1. Используй match для классификации:
def classify(value):
    match value:
        case 0:
            return "zero"
        case n if n > 0:
            return "positive"
        case n if n < 0:
            return "negative"

print(classify(5))    # positive
print(classify(-3))   # negative
print(classify(0))    # zero
  1. Сопоставление списков:
def describe_list(lst):
    match lst:
        case []:
            return "empty"
        case [x]:
            return f"one: {x}"
        case [x, y]:
            return f"two: {x}, {y}"
        case [x, *rest]:
            return f"first={x}, rest={rest}"

print(describe_list([]))
print(describe_list([1]))
print(describe_list([1, 2]))
print(describe_list([1, 2, 3, 4]))
  1. Парсинг команды через словарный образец:
def execute(command):
    match command:
        case {"action": "create", "name": name}:
            return f"Создаю {name}"
        case {"action": "delete", "id": int(id)}:
            return f"Удаляю id={id}"
        case _:
            return "Неизвестная команда"

print(execute({"action": "create", "name": "post"}))
print(execute({"action": "delete", "id": 42}))
print(execute({"action": "unknown"}))

Что дальше

Освоили pattern matching. В следующем уроке - comprehensions: компактный синтаксис для создания списков, словарей, множеств и генераторов из других iterable.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.