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) - оба захватываются.
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 ближе к академическим языкам функционального семейства, где такие конструкции исторически развивались десятилетиями.
Мини-задание
- Используй 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
- Сопоставление списков:
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]))
- Парсинг команды через словарный образец:
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.