Оператор match в Python
Структурное сопоставление шаблонов в Python: match/case, литералы, последовательности, словари, классы, охранники и wildcard — с примерами.
В Python 3.10 появилось структурное сопоставление шаблонов через оператор match — мощный способ ветвления по форме и содержимому данных, а не только по равенству. В этой главе рассматривается всё: от базового синтаксиса match/case до сложных шаблонов — разбиения последовательностей, шаблонов словарей, шаблонов классов, охранников и реальных примеров использования.
Перед чтением этой главы рекомендуется быть знакомым с конструкцией if/else в Python, функциями Python и базовыми структурами данных (списками, кортежами, словарями).
Что такое структурное сопоставление шаблонов?
Структурное сопоставление шаблонов позволяет анализировать структуру объекта — его тип, значения полей, форму последовательности — и выполнять разный код в зависимости от того, какой шаблон подходит. Это выходит далеко за рамки простой проверки if x == y.
Рассмотрим маршрутизацию по HTTP-коду статуса. С цепочками if/elif код выглядит так:
if status == 200:
print("OK")
elif status == 404:
print("Not Found")
elif status == 500:
print("Internal Server Error")
else:
print("Unknown status")С match намерение выражается чище:
match status:
case 200:
print("OK")
case 404:
print("Not Found")
case 500:
print("Internal Server Error")
case _:
print("Unknown status")Настоящее преимущество проявляется, когда объект сложный — кортеж, словарь или датакласс — и его нужно деструктурировать в процессе сопоставления.
Базовый синтаксис
match subject:
case pattern1:
# runs if subject matches pattern1
case pattern2:
# runs if subject matches pattern2
case _:
# wildcard — runs if nothing else matchedПравила, которые нужно помнить:
matchиcase— мягкие ключевые слова: они являются ключевыми только в данном контексте и в остальных частях кода могут использоваться как имена переменных.- Каждый блок
caseпроверяется по порядку; первое совпадение выигрывает, остальные пропускаются. - Блок
case _:— это wildcard: он всегда совпадает и служит вариантом по умолчанию. - Требуется Python 3.10+. Запуск на Python 3.9 или более ранних версиях вызывает
SyntaxError.
Шаблоны литералов
Простейший шаблон сопоставляет конкретное значение: число, строку, True, False или None.
def http_status(status):
match status:
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return "Unknown status"
print(http_status(200)) # OK
print(http_status(404)) # Not Found
print(http_status(999)) # Unknown statusШаблоны ИЛИ (|)
Используйте | внутри case, чтобы сопоставить любой из нескольких литералов:
def is_vowel(letter):
match letter.lower():
case "a" | "e" | "i" | "o" | "u":
return True
case _:
return False
print(is_vowel("a")) # True
print(is_vowel("b")) # False
print(is_vowel("E")) # TrueШаблоны ИЛИ также работают с числами, None и другими типами литералов.
Шаблоны захвата
Шаблон захвата — это простое имя (не строковый литерал, не составное имя), которое совпадает с чем угодно и привязывает сопоставленное значение к этому имени для использования в теле блока:
def greet(name):
match name:
case "Alice":
return "Hello, Alice!"
case other: # captures whatever was passed
return f"Hello, {other}!"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob")) # Hello, Bob!other здесь — шаблон захвата: он привязывает сопоставленное значение к локальной переменной other. Это похоже на wildcard _, но _ отбрасывает значение, тогда как именованный захват его сохраняет.
Простое имя в case — это всегда захват, но не сравнение. Чтобы сравнить со значением константы, определённой в другом месте, используйте составное имя — например, Status.OK — или охранник (case x if x == my_constant:).
Шаблоны последовательностей
Шаблон последовательности сопоставляется со списками, кортежами или любыми последовательностями и одновременно может деструктурировать элементы в переменные.
def process_point(point):
match point:
case (0, 0):
return "Origin"
case (x, 0):
return f"On x-axis at {x}"
case (0, y):
return f"On y-axis at {y}"
case (x, y):
return f"Point at ({x}, {y})"
print(process_point((0, 0))) # Origin
print(process_point((5, 0))) # On x-axis at 5
print(process_point((0, 3))) # On y-axis at 3
print(process_point((2, 4))) # Point at (2, 4)Использование * для захвата остатка
*name внутри шаблона последовательности собирает оставшиеся элементы — так же, как при распаковке итерируемых объектов:
def describe_list(items):
match items:
case []:
return "empty list"
case [single]:
return f"one item: {single}"
case [first, *rest]:
return f"starts with {first!r}, then {len(rest)} more item(s)"
print(describe_list([])) # empty list
print(describe_list([42])) # one item: 42
print(describe_list([1, 2, 3, 4])) # starts with 1, then 3 more item(s)Используйте [first, *_], если хотите захватить только первый элемент и отбросить остальные.
Шаблоны словарей
Шаблон словаря сопоставляется со словарями (или любым Mapping). Вы указываете только интересующие вас ключи — лишние ключи в объекте игнорируются.
def process_event(event):
match event:
case {"type": "click", "button": button}:
return f"Mouse click: button {button}"
case {"type": "keypress", "key": key}:
return f"Key pressed: {key!r}"
case {"type": action}:
return f"Other event: {action}"
case _:
return "Unknown event"
print(process_event({"type": "click", "button": 1}))
# Mouse click: button 1
print(process_event({"type": "keypress", "key": "Enter"}))
# Key pressed: 'Enter'
print(process_event({"type": "resize", "width": 800}))
# Other event: resize
print(process_event({}))
# Unknown eventКлючевой момент: шаблон словаря не вызывает ошибку из-за лишних ключей в объекте. {"type": "click", "button": button} совпадёт, даже если событие также содержит координаты "x" и "y".
Чтобы захватить оставшиеся пары ключ/значение, используйте **rest:
match event:
case {"type": "click", **rest}:
print(f"Click event with extra data: {rest}")Шаблоны классов
Шаблон класса сопоставляется с экземпляром определённого класса и извлекает его атрибуты. Это особенно удобно с датаклассами, поскольку их атрибуты автоматически доступны по имени.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
def describe_shape(shape):
match shape:
case Point(x=0, y=0):
return "Point at origin"
case Point(x=x, y=y):
return f"Point at ({x}, {y})"
case Circle(center=Point(x=cx, y=cy), radius=r):
return f"Circle centered at ({cx}, {cy}) with radius {r}"
case _:
return "Unknown shape"
print(describe_shape(Point(0, 0))) # Point at origin
print(describe_shape(Point(3, 4))) # Point at (3, 4)
print(describe_shape(Circle(Point(1, 2), 5)))# Circle centered at (1, 2) with radius 5Обратите внимание на вложенный шаблон класса в ветке Circle: Point(x=cx, y=cy) сопоставляется внутри шаблона Circle. Шаблоны можно комбинировать на произвольную глубину.
Для встроенных типов int, str, float и bool можно использовать позиционные шаблоны с одним аргументом:
def handle_input(value):
match value:
case (int() | float()) as number:
return f"Got a number: {number}"
case str() as text:
return f"Got text: {text!r}"
case _:
return "Unknown type"
print(handle_input(3.14)) # Got a number: 3.14
print(handle_input("hello")) # Got text: 'hello'
print(handle_input([1, 2])) # Unknown typeКлючевое слово as (шаблон AS) привязывает всё сопоставленное значение к имени даже после проверки типа.
Охранники
Охранник — это условие if, добавляемое после шаблона. Ветка case совпадает только тогда, когда шаблон подходит и охранник возвращает True.
def classify_number(n):
match n:
case 0:
return "zero"
case x if x < 0:
return f"{x} is negative"
case x if x % 2 == 0:
return f"{x} is positive and even"
case x:
return f"{x} is positive and odd"
print(classify_number(0)) # zero
print(classify_number(-5)) # -5 is negative
print(classify_number(4)) # 4 is positive and even
print(classify_number(7)) # 7 is positive and oddОхранники вычисляются после того, как структурный шаблон совпал, поэтому захваченные переменные доступны внутри них. Если охранник не срабатывает, последующие ветки case продолжают проверяться.
Wildcard-шаблон _
_ — универсальный перехватчик. Он совпадает с любым значением и ничего не привязывает (значение отбрасывается). Он должен быть последним case в блоке match. Без него match, не нашедший ни одного совпадения, просто ничего не делает — ошибка не возникает.
def describe(value):
match value:
case 0:
return "zero"
case _:
return f"something else: {value!r}"
print(describe(0)) # zero
print(describe(99)) # something else: 99
print(describe("hi")) # something else: 'hi'_ может встречаться и внутри шаблона, чтобы игнорировать отдельные части:
match point:
case (_, 0):
print("On the x-axis (x value doesn't matter)")
case (0, _):
print("On the y-axis (y value doesn't matter)")Комбинирование шаблонов: реальный пример
Шаблоны, описанные выше, можно комбинировать. Вот парсер команд текстового квеста, в котором сочетаются шаблоны последовательностей, охранники и wildcard:
def run_command(command):
match command.split():
case ["quit"]:
return "Quitting"
case ["go", direction] if direction in ("north", "south", "east", "west"):
return f"Going {direction}"
case ["go", direction]:
return f"Cannot go {direction!r} — try north, south, east, or west"
case ["get", item]:
return f"Picking up {item}"
case ["drop", item]:
return f"Dropping {item}"
case ["inventory"]:
return "Checking inventory"
case [verb, *args]:
return f"Unknown command {verb!r} with args {args}"
case []:
return "No command entered"
print(run_command("go north")) # Going north
print(run_command("go up")) # Cannot go 'up' — try north, south, east, or west
print(run_command("get sword")) # Picking up sword
print(run_command("drop torch")) # Dropping torch
print(run_command("quit")) # Quitting
print(run_command("")) # No command enteredЧитая код сверху вниз, можно сразу понять каждую поддерживаемую команду — для достижения той же ясности с if/elif потребовалось бы значительно больше строк.
match vs. if/elif — когда что выбирать
| Сценарий | Лучший выбор |
|---|---|
| Простое равенство с несколькими константами | Любой; match немного чище |
| Сопоставление по структуре данных / форме | match — значительно чище |
| Деструктуризация значений при сопоставлении | match — с if это невозможно |
| Логика только с вычисляемыми условиями | if/elif |
| Python 3.9 или более ранние версии | if/elif (нет match) |
| Выражение таблицы решений наглядно | match |
match — не замена каждой цепочки if. Когда все ветки проверяют вычисляемые boolean-условия (например, if x > 10 and y < 5), цепочка if/elif более естественна. match показывает себя лучше, когда условие касается формы данных.
Типичные ошибки
Имена констант не сопоставляются по значению
Простое имя в case — это всегда захват, но не поиск значения:
STATUS_OK = 200
match response_code:
case STATUS_OK: # WRONG — this captures into STATUS_OK, not compares!
print("Success")Чтобы сравнить с именованной константой, используйте составное имя (http.HTTPStatus.OK) или охранник:
match response_code:
case x if x == STATUS_OK:
print("Success")match не является исчерпывающим по умолчанию
В отличие от switch в некоторых других языках, match без совпадающей ветки просто ничего не делает. Добавьте case _:, если нужен гарантированный обработчик.
match требует Python 3.10+
Запуск блока match на Python 3.9 или более ранних версиях вызывает SyntaxError: invalid syntax. Проверьте версию с помощью python3 --version. Если нужно настроить современное окружение Python, см. руководство по началу работы с Python.
Шаблоны — не boolean-выражения
Нельзя писать case x > 5: — это охранник, а не шаблон. Структурная часть (case x) должна идти первой, за ней следует необязательный if guard_expression.
Итоги
| Тип шаблона | Пример синтаксиса | Что сопоставляет |
|---|---|---|
| Литерал | case 42: | Точное значение |
| ИЛИ | case "yes" | "y": | Любой из вариантов |
| Wildcard | case _: | Что угодно (значение отбрасывается) |
| Захват | case x: | Что угодно, привязывает к x |
| Последовательность | case [a, b, *rest]: | Последовательность не менее чем из 2 элементов |
| Словарь | case {"key": val}: | Словарь, содержащий указанные ключи |
| Класс | case Point(x=0, y=y): | Экземпляр с совпадающими атрибутами |
| AS | case int() as n: | Совпадает и привязывает всё значение |
| Охранник | case x if x > 0: | Шаблон + дополнительное boolean-условие |
Практика
Теперь, когда вы умеете ветвиться по структуре данных, изучите циклы for в Python для итерации по последовательностям или перечисления enum в Python, чтобы определять типизированные константы, которые хорошо работают с шаблонами классов.