Статьи
July 22

match-case в Python и почему он не так прост

Некоторые до сих пор утверждают, что Python не нуждается в синтаксисе "switch-case". Даже сам Гвидо не поддерживал его добавление. Однако почему он все же появился в версии 3.10? Причина может быть найдена в названии. Новый функционал называется "match case", а не "switch case", как в большинстве языков программирования.

Рассмотрим 7 примеров, которые продемонстрируют, насколько гибким и «питоническим» является новый синтаксис.

Базовый сценарий использования

Прежде чем приступать к каким-либо кучерявостям, давайте начнём с базового варианта использования этого нового синтаксиса. Предположим, мы пишем функцию для преобразования кода состояния HTTP в сообщения об ошибках, мы можем использовать match-case следующим образом:

def http_status(status):
    match status:
        case 400:
            return "Bad request"
        case 401:
            return "Unauthorized"
        case 403:
            return "Forbidden"
        case 404:
            return "Not found"
            
http_status(404)
# Not found

Действительно, в данном конкретном примере синтаксис match-case не даёт никаких преимуществ по сравнению с синтаксисом if-else, как показано ниже:

def http_error(status):
    if status == 400:
        return "Bad request"
    elif status == 401:
        return "Unauthorized"
    elif status == 403:
        return "Forbidden"
    elif status == 404:
        return "Not found"
    else:
        return "Unknown status code"

Если бы новая фича могла делать только это, вряд ли бы её добавили. Давайте продолжим с другими примерами.

Вариант по умолчанию

match-case должен быть похож на синтаксис switch-case, который есть в большинстве других языков, поэтому у него должен быть "вариант по умолчанию". Когда ни одна из веток не срабатывает, то будет выполнен этот самый вариант.

Python выполняет это требование в своем стиле, используя знак подчёркивания "_", который обозначает анонимную переменную. Анонимная переменная может «соответствовать» чему угодно.

Рассмотрим пример ниже.

def http_status(status):
    match status:
        case 400:
            return "Bad request"
        case 401:
            return "Unauthorized"
        case 403:
            return "Forbidden"
        case 404:
            return "Not found"
        case _:
            return "Other error"
            
http_status(999)
# Other error

В приведенном выше коде мы добавили случай по умолчанию и произошёл вывод «Other error».

Комбинирование проверок

Что если иногда у нас есть несколько проверок, которые следует объединить? То есть действия при нескольких вариантах должны быть одинаковыми.

В Python мы можем использовать оператор "|" для объединения нескольких проверок в одну. По сути это «ИЛИ».

def http_status(status):
    match status:
        case 400:
            return "Bad request"
        case 401 | 403:
            return "Authentication error"
        case 404:
            return "Not found"
        case _:
            return "Other error"
            
http_status(401)
# "Authentication error"

http_status(403)
# "Authentication error"

В приведённом выше коде мы выводим один текст при значении аргумента 401 и 403.

wildcard в списке

Теперь к более интересному. Предположим, мы пишем логику будильника, используя синтаксис match-case. В качестве аргумента функция принимает список. Первый элемент — это время суток («утро», «день» и «вечер»). Вторым элементом будет действие, о котором нам нужно напоминать.

Однако мы можем захотеть, чтобы будильник напоминал нам о необходимости сделать несколько дел одновременно. Поэтому мы хотим передать более одного действия в списке, чтобы будильник мог напомнить нам выполнить их все по очереди.

Вот код для реализации такого требования:

def alarm(item):
    match item:
        case [time, action]:
            print(f'Good {time}! It is time to {action}!')
        case [time, *actions]:
            print('Good morning!')
            for action in actions:
                print(f'It is time to {action}!')

alarm(['afternoon', 'work'])
# Good afternoon! It is time to work!

alarm(('morning', 'have breakfast', 'brush teeth', 'work'))
# Good morning!
# It is time to have breakfast!
# It is time to brush teeth!
# It is time to work!

Первый вызов не требует особых пояснений. Во втором мы ставим звездочку перед переменной actions, чтобы она могла соответствовать одному или нескольким значениям.

Подшаблоны

Иногда мы можем захотеть иметь шаблоны в шаблоне. То есть, мы хотим, чтобы match-case делал сопоставление по двум значением, но одно из них должно быть вариантивно.

Ранее мы определили, что время дня должно быть либо «утро», либо «день», либо «вечер». Давайте добавим это ограничение в код match-case. Если время не совпадает ни с одним из этих трех, выводим пользователю, что время неверное.

def alarm(item):
    match item:
        case [('morning' | 'afternoon' | 'evening'), action]:
            print(f'Good (?)! It is time to {action}!')
        case _:
            print('The time is invalid.')
            
alarm('something', 'work')
# The time is invalid.

alarm('afternoon', 'work')
# Good (?)! It is time to work!

Почему здесь стоит знак вопроса? К ак нам легко добавить в строку именно нужное значение? Вспоминаем f-строки!

def alarm(item):
    match item:
        case [('morning' | 'afternoon' | 'evening') as time, action]:
            print(f'Good {time}! It is time to {action}!')
        case _:
            print('The time is invalid.')

alarm('afternoon', 'work')
# Good afternoon! It is time to work!

Условия в сопоставлениях

Нам нужно сделать этот будильник умнее. Когда наступит вечер, выведем несколько сообщений, чтобы поблагодарить пользователя за то, что он закончил рабочий день.

def alarm(item):
    match item:
        case ['evening', action]:
            print(f'You almost finished the day! Now {action}!')
        case [time, action]:
            print(f'Good {time}! It is time to {action}!')
        case _:
            print('The time is invalid.')

alarm(['evening', 'play video games'])
# You almost finished the day! Now play video games!

Давайте сделаем этот будильник еще умнее, чтобы поощрять пользователя полноценно отдыхать. Таким образом, когда пользователь захочет поработать или поучиться вечером, будильник порекомендует ему немного отдохнуть (всем бы такой будильник).

def alarm(item):
    match item:
        case ['evening', action] if action not in ['work', 'study']:
            print(f'You almost finished the day! Now {action}!')
        case ['evening', _]:
            print('Come on, you deserve some rest!')
        case [time, action]:
            print(f'Good {time}! It is time to {action}!')
        case _:
            print('The time is invalid.')

alarm(['evening', 'study'])
# Come on, you deserve some rest!

Для этого мы добавили еще один случай с условием. Когда вечером стоит "работа" или "учёба", то будильник рекомендует отдохнуть.

Сопоставление объектов

Теперь вы убедились, что это match-case, а не switch-case, потому что в шаблонах сопоставления у нас намного больше гибкости.

Возьмём еще более сложный пример — экземпляр класса. Давайте используем match-case для сопоставление с атрибутом объекта. Класс под названием Direction создаётся для хранения направления по горизонтальной (восток или запад) и вертикальной (север или юг) оси.

class Direction:
    def __init__(self, horizontal=None, vertical=None):
        self.horizontal = horizontal
        self.vertical = vertical

Теперь используем синтаксис match-case для вывода сообщения на основе атрибута:

def direction(loc):
    match loc:
        case Direction(horizontal='east', vertical='north'):
            print('You towards northeast')
        case Direction(horizontal='east', vertical='south'):
            print('You towards southeast')
        case Direction(horizontal='west', vertical='north'):
            print('You towards northwest')
        case Direction(horizontal='west', vertical='south'):
            print('You towards southwest')
        case Direction(horizontal=None):
            print(f'You towards {loc.vertical}')
        case Direction(vertical=None):
            print(f'You towards {loc.horizontal}')
        case _:
            print('Invalid Direction')

Создадим несколько объектов из класса для тестирования.

d1 = Direction('east', 'south')
d2 = Direction(vertical='north')
d3 = Direction('centre', 'centre')

И всё работает!

# You towards southeast
# You towards north
# Invalid Direction

Заключение

Новый синтаксис match-case, который появился в Python 3.10, это всё же не совсемswitch-case, но он очень гибкий и мощный (как питон!).

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶

Источник: ProgrammeSought
Перевод: Екатерина Прохорова