Разбираем мощь match/case в Python: от базового синтаксиса до распаковки JSON и эмуляции в старых версиях.
Долгожданный switch пришёл. Но это не совсем switch
Если вы, как и я, пришли в Python из других языков вроде C++ или Java, то наверняка недоумевали: «Как в таком красивом языке до сих пор нет нормального оператора switch?». 20 лет мы жили с конструкциями if-elif-elif-else и словарями-диспетчеризаторами.
И вот в Python 3.10 (октябрь 2021) появился долгожданный оператор match. Но это оказался не просто аналог switch — это структурное сопоставление (pattern matching), мощный инструмент, который меняет подход к написанию читаемого кода для обработки сложных структур данных.
В этой статье я покажу:
Базовый синтаксис для тех, кто пропустил анонс
Реальную мощь паттерн-матчинга, о которой мало говорят
Как эмулировать похожее поведение в Python 3.9 и ниже
Практические кейсы из реальных проектов
Часть 1: Базовый синтаксис — тот самый «switch»
Для начала разберёмся с основами. Допустим, у нас есть статус заказа:
def handle_order_status(status):
match status:
case "pending":
print("Заказ ожидает обработки")
case "shipped":
print("Заказ в пути")
case "delivered":
print("Заказ доставлен")
case _:
print("Неизвестный статус")
# Использование
handle_order_status("shipped") # Вывод: "Заказ в пути"Пока всё знакомо, правда? case _ — это аналог default в других языках.
Но на этом сходство с обычным switch заканчивается. Давайте копать глубже.
Часть 2: Паттерн-матчинг — где начинается магия
2.1. Распаковка структур прямо в case
Представьте, что вы работаете с географическими координатами:
def process_coordinates(point):
match point:
case (0, 0):
print("Точка в начале координат")
case (x, 0):
print(f"Точка на оси X: {x}")
case (0, y):
print(f"Точка на оси Y: {y}")
case (x, y):
print(f"Точка в ({x}, {y})")
case _:
print("Не координаты")
process_coordinates((5, 0)) # "Точка на оси X: 5"
process_coordinates((3, 4)) # "Точка в (3, 4)"Обратите внимание: в case (x, y) переменные x и y связываются со значениями! Это не сравнение, а распаковка с сопоставлением.
2.2. Работа со списками любой длины
def process_items(items):
match items:
case []:
print("Пустой список")
case [first]:
print(f"Один элемент: {first}")
case [first, second]:
print(f"Два элемента: {first} и {second}")
case [first, *rest]:
print(f"Первый: {first}, остальные: {rest}")
process_items([1, 2, 3, 4]) # Первый: 1, остальные: [2, 3, 4]Звёздочка (*rest) — это тот же оператор распаковки, но внутри паттерна!
2.3. Условия внутри case (Guards)
Иногда одного паттерна недостаточно. Добавим условия:
def check_point(point):
match point:
case (x, y) if x == y:
print(f"Точка на диагонали: ({x}, {y})")
case (x, y) if x > 0 and y > 0:
print(f"Точка в первом квадранте: ({x}, {y})")
case (x, y):
print(f"Просто точка: ({x}, {y})")
check_point((5, 5)) # "Точка на диагонали: (5, 5)"
check_point((3, 4)) # "Точка в первом квадранте: (3, 4)"Ключевое слово if после паттерна — это guard (охранное выражение). Если паттерн совпал, но guard вернул False, Python проверяет следующий case.
2.4. Сопоставление по типу
def process_value(value):
match value:
case int():
print(f"Целое число: {value}")
case float():
print(f"Дробное число: {value}")
case str() as text if len(text) > 10:
print(f"Длинная строка: {text[:10]}...")
case str() as text:
print(f"Строка: {text}")
case list() | tuple() as sequence:
print(f"Последовательность длиной {len(sequence)}")
case _:
print("Что-то другое")
process_value(42) # "Целое число: 42"
process_value([1, 2, 3]) # "Последовательность длиной 3"Обратите внимание на конструкции:
int()— проверка типаstr() as text— проверка типа + привязка к переменнойlist() | tuple()— ИЛИ-паттерн (совпадение с любым из)
2.5. Работа с классами и dataclass
Вот где match раскрывается полностью:
from dataclasses import dataclass
from typing import Literal
@dataclass
class User:
name: str
role: Literal["admin", "editor", "viewer"]
active: bool
def handle_user(user: User):
match user:
case User(name="admin", role="admin"):
print("Супер-администратор")
case User(name=name, role="admin"):
print(f"Администратор {name}")
case User(role="editor", active=True):
print("Активный редактор")
case User(role="viewer", active=False):
print("Неактивный зритель")
case User(name=name, active=True):
print(f"Активный пользователь: {name}")
case _:
print("Неизвестный пользователь")
# Тестируем
admin = User(name="alice", role="admin", active=True)
editor = User(name="bob", role="editor", active=False)
handle_user(admin) # "Администратор alice"
handle_user(editor) # "Активный пользователь: bob"Часть 3: Практические кейсы из реальной жизни
Кейс 1: Обработка JSON-ответов API
Представьте, что вы работаете с JSON-ответом от какого-то API:
def handle_api_response(response):
match response:
case {"status": "success", "data": list(data)}:
print(f"Успех! Получено {len(data)} элементов")
return process_data(data)
case {"status": "error", "code": 404, "message": msg}:
print(f"Не найдено: {msg}")
return None
case {"status": "error", "code": int(code), "message": msg}:
print(f"Ошибка {code}: {msg}")
return None
case {"status": "success", "data": dict(data)}:
print(f"Успех! Получен объект")
return process_object(data)
case _:
print(f"Неизвестный формат ответа: {response}")
return NoneКод стал значительно читабельнее, чем вложенные if с проверками "status" in response and response["status"] == ....
Кейс 2: Парсинг AST (Abstract Syntax Tree)
Если вы когда-нибудь писали кодогенераторы или линтеры, то оцените:
import ast
def analyze_code(node):
match node:
# Если это присваивание
case ast.Assign(targets=[ast.Name(id=name)], value=value):
print(f"Присваивание переменной {name}")
analyze_code(value)
# Если это вызов функции
case ast.Call(func=ast.Name(id=func_name), args=args):
print(f"Вызов функции {func_name} с {len(args)} аргументами")
for arg in args:
analyze_code(arg)
# Если это бинарная операция
case ast.BinOp(left=left, op=op, right=right):
print(f"Бинарная операция {type(op).__name__}")
analyze_code(left)
analyze_code(right)
# Числовые литералы
case ast.Constant(value=int(value)):
print(f"Целое число: {value}")
case ast.Constant(value=str(value)):
print(f"Строка: '{value}'")
case _:
print(f"Другой узел: {type(node).__name__}")
# Пример использования
code = "result = calculate(10 + 20, 'test')"
tree = ast.parse(code)
analyze_code(tree.body[0])Кейс 3: Обработка команд CLI
def handle_command(command_line):
match command_line.split():
case ["exit"]:
print("Выход из программы")
return False
case ["help"]:
print("Доступные команды: help, exit, copy, move")
return True
case ["copy", src, dest]:
print(f"Копируем {src} в {dest}")
return True
case ["move", src, dest] if src != dest:
print(f"Перемещаем {src} в {dest}")
return True
case ["move", path, path]:
print(f"Ошибка: исходный и целевой пути одинаковы")
return True
case ["search", *terms]:
print(f"Поиск по терминам: {terms}")
return True
case _:
print(f"Неизвестная команда: {command_line}")
return TrueЧасть 4: А что делать, если у вас Python < 3.10?
Если вы застряли на старой версии Python, не отчаивайтесь! Можно эмулировать похожее поведение.
Способ 1: Словарь-диспетчер (для простых случаев)
def handle_status_simple(status):
handlers = {
"pending": lambda: print("Заказ ожидает обработки"),
"shipped": lambda: print("Заказ в пути"),
"delivered": lambda: print("Заказ доставлен"),
}
handler = handlers.get(status, lambda: print("Неизвестный статус"))
handler()Способ 2: Классы с визитором (для сложных случаев)
Этот подход используется в компиляторах и сложных парсерах:
class OrderVisitor:
def visit(self, order):
method_name = f'visit_{order["status"]}'
method = getattr(self, method_name, self.visit_unknown)
return method(order)
def visit_pending(self, order):
return "Заказ ожидает обработки"
def visit_shipped(self, order):
return "Заказ в пути"
def visit_delivered(self, order):
return "Заказ доставлен"
def visit_unknown(self, order):
return "Неизвестный статус"
# Использование
visitor = OrderVisitor()
order = {"status": "shipped", "id": 123}
print(visitor.visit(order)) # "Заказ в пути"Способ 3: Цепочка условий с распаковкой
def process_point_legacy(point):
# Эмуляция match point: case (x, y) if x == y:
if isinstance(point, tuple) and len(point) == 2:
x, y = point
if x == y:
return f"Точка на диагонали: ({x}, {y})"
if x > 0 and y > 0:
return f"Точка в первом квадранте: ({x}, {y})"
return f"Просто точка: ({x}, {y})"
return "Не координаты"Часть 5: Подводные камни и лучшие практики
1. Порядок имеет значение
Как и в if-elif, case проверяются по порядку:
match value:
case int():
print("Это int") # Сработает для value=42
case str() | int(): # Этот case никогда не сработает для int!
print("Это str или int")2. Изменяемые значения в паттернах
Паттерны не работают с произвольными изменяемыми объектами:
match [1, 2, 3]:
case [1, 2, 3]: # Работает
print("Совпадение")
case list(): # Тоже работает
print("Это список")
# Но так нельзя:
pattern = [1, 2, 3] # Переменная
match [1, 2, 3]:
case pattern: # Это не сравнение с [1,2,3], а присваивание pattern=[1,2,3]!
print("Не сработает как ожидается")3. Производительность
match обычно немного медленнее, чем простой if-elif для примитивных типов. Но разница незначительна, а читаемость выигрывает. Для сложных структур данных match может быть даже быстрее за счёт оптимизаций.
4. Когда НЕ стоит использовать match:
Простые проверки одного значения:
match status: case "A": case "B":— иногда проще черезif.Когда нужна проверка только по типу:
isinstance(x, int)читабельнее.В очень performance-critical коде (но сначала измерьте!).
Заключение
match/case в Python — это не просто замена switch. Это мощный инструмент декомпозиции данных, который:
Делает код читабельнее при работе со сложными структурами
Позволяет объединять проверку типа, распаковку и дополнительные условия
Отлично подходит для обработки JSON, AST, команд и т.д.
Может быть эмулирован в старых версиях Python
Совет от автора: Начните с малого. Сначала замените один сложный if-elif на match. Потом попробуйте обработать им вложенный словарь. Постепенно вы найдёте больше мест, где этот инструмент сделает ваш код элегантнее.
А вы уже используете match/case в своих проектах? Делитесь интересными кейсами применения в комментариях!
Статья написана для Python 3.10+. Примеры проверены на Python 3.11.
