Разбираем мощь match/case в Python: от базового синтаксиса до распаковки JSON и эмуляции в старых версиях.

Долгожданный switch пришёл. Но это не совсем switch

Если вы, как и я, пришли в Python из других языков вроде C++ или Java, то наверняка недоумевали: «Как в таком красивом языке до сих пор нет нормального оператора switch?». 20 лет мы жили с конструкциями if-elif-elif-else и словарями-диспетчеризаторами.

И вот в Python 3.10 (октябрь 2021) появился долгожданный оператор match. Но это оказался не просто аналог switch — это структурное сопоставление (pattern matching), мощный инструмент, который меняет подход к написанию читаемого кода для обработки сложных структур данных.

В этой статье я покажу:

  1. Базовый синтаксис для тех, кто пропустил анонс

  2. Реальную мощь паттерн-матчинга, о которой мало говорят

  3. Как эмулировать похожее поведение в Python 3.9 и ниже

  4. Практические кейсы из реальных проектов

Часть 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. Это мощный инструмент декомпозиции данных, который:

  1. Делает код читабельнее при работе со сложными структурами

  2. Позволяет объединять проверку типа, распаковку и дополнительные условия

  3. Отлично подходит для обработки JSON, AST, команд и т.д.

  4. Может быть эмулирован в старых версиях Python

Совет от автора: Начните с малого. Сначала замените один сложный if-elif на match. Потом попробуйте обработать им вложенный словарь. Постепенно вы найдёте больше мест, где этот инструмент сделает ваш код элегантнее.

А вы уже используете match/case в своих проектах? Делитесь интересными кейсами применения в комментариях!

Статья написана для Python 3.10+. Примеры проверены на Python 3.11.