Примеры match/case в Python 3.10 с объяснением

Дмитро Сімагін

Релиз Python 3.10, вышедший в октябре 2021 года, предложил разработчикам несколько интересных изменений, включая pattern matching statement (оператор сопоставления с шаблонами). Как уверяли авторы PEP 622, на создание этого предложения их вдохновил схожий синтаксис в языках программирования Scala и Erlang.

Если вы еще не знакомы с pattern matching, то у вас сейчас есть хорошая возможность узнать в данной статье, что выполняет эта функция,  как происходит совпадение, о предназначении и вариантах использования этого оператора в Python. 

Pattern matching: что это такое и зачем он нужен

Согласно пояснениям, изложенным в PEP622, в коде Python иногда требуется сопоставлять данные с типами, обращаться к ним по индексу и применять проверку на тип. При этом нужно проверять не один лишь тип данных, но и их число. А это приводит к тому, что возникает большое количество ответвлений if/else, которые вызывают функции isinstance, len и обращаются к элементам по индексу, атрибуту или ключу. Чтобы избежать подобных усложнений и сократить if/else, появился оператор Pattern matching.

 До выпуска Python 3.10 в его синтаксисе отсутствовал простой и надежный способ сравнения значений с с одним из множества вероятных условий. Хотя в других языках такие фичи давно существовали. К примеру, в C и C++ есть конструкция switch/case. Нечто подобное есть и в Rust.

Pattern matching в коде Python реализуется при помощи конструкції match/case. Она позволяет сопоставлять выражение, структуру данных или тип с шаблоном. Проще говоря, Python перебирает все варианты шаблонов, пока не найдет тот шаблон, который соответствует выражению.

Важно не путать match/case и switch/case. Различия между ними в том, что pattern matching и его конструкция match/case — это не обычный оператор для сравнения какой-то переменной со значениями, а готовый механизм для сопоставления данных, их распаковки и управления потоком выполнения.

Использовать match/case можно в следующих ситуациях:

  • для упрощения сложных ветвлений (теперь не нужны многочисленные проверки if/elif);
  • для сопоставления сложных структур данных с шаблоном;
  • для проверки типа переменной и обработки разных типов данных в нескольких вариантах.

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

Пример 1: Простое сопоставление значений

Для начала посмотрим, как это работает с if/else:

day = "Monday"

if day == "Monday" or day == "Tuesday" or day == "Wednesday" or day == "Thursday" or day == "Friday":
    print("It's a workday.")
elif day == "Saturday" or day == "Sunday":
    print("It's a weekend.")
else:
    print("Unknown day.")

А теперь то же самое, но уже с match/case:

day = "Monday"

match day:
    case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
        print("It's a workday.")
    case "Saturday" | "Sunday":
        print("It's a weekend.")
    case _:
        print("Unknown day.")

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

Пример 2: Сопоставление с типами данных

Опять для начала используем if/else. Предположим, у нас есть переменная value, и мы хотим определить ее тип данных, чтобы выполнить соответствующее действие.

value = 42

if isinstance(value, int):
    print("The value is an integer.")
elif isinstance(value, float):
    print("The value is a float.")
elif isinstance(value, str):
    print("The value is a string.")
elif isinstance(value, list):
    print("The value is a list.")
else:
    print("Unknown type.")

То же самое, но уже с match/case:

value = 42

match value:
    case int():
        print("The value is an integer.")
    case float():
        print("The value is a float.")
    case str():
        print("The value is a string.")
    case list():
        print("The value is a list.")
    case _:
        print("Unknown type.")

В этих примерах код проверяет тип данных переменной value и выполняет соответствующее действие. В первой версии используется if/else с функцией isinstance, во второй — конструкция match/case, которая позволяет более лаконично сопоставлять значения с их типами.

Пример 3: Сопоставление с шаблоном в списке

Сначала применяем if/else. Тут у нас есть список из двух элементов, и мы хотим выполнить определенное действие в зависимости от значений этих элементов.

values = [1, 2]

if len(values) == 2 and values[0] == 1 and values[1] == 2:
    print("The list is [1, 2].")
elif len(values) == 2 and values[0] == 3 and values[1] == 4:
    print("The list is [3, 4].")
elif len(values) == 2 and values[0] == 5 and values[1] == 6:
    print("The list is [5, 6].")
else:
    print("Unknown list pattern.")

Тот же самый код, но с match/case:

values = [1, 2]

match values:
    case [1, 2]:
        print("The list is [1, 2].")
    case [3, 4]:
        print("The list is [3, 4].")
    case [5, 6]:
        print("The list is [5, 6].")
    case _:
        print("Unknown list pattern.")

Здесь код проверяет, соответствует ли список values определенным шаблонам. Сначала используется if/else с условиями для длины и элементов списка, а во второй — конструкция match/case, которая позволяет легко сопоставлять списки с заданными шаблонами.

Пример 4: Сопоставление с шаблоном в словаре

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

data = {"name": "Alice", "age": 30}

if "name" in data and "age" in data and data["name"] == "Alice" and data["age"] == 30:
    print("This is Alice, aged 30.")
elif "name" in data and "age" in data and data["name"] == "Bob" and data["age"] == 25:
    print("This is Bob, aged 25.")
elif "name" in data and "age" in data and data["name"] == "Charlie" and data["age"] == 40:
    print("This is Charlie, aged 40.")
else:
    print("Unknown person.")

Тот же пример с использованием match/case:

data = {"name": "Alice", "age": 30}

match data:
    case {"name": "Alice", "age": 30}:
        print("This is Alice, aged 30.")
    case {"name": "Bob", "age": 25}:
        print("This is Bob, aged 25.")
    case {"name": "Charlie", "age": 40}:
        print("This is Charlie, aged 40.")
    case _:
        print("Unknown person.")

Сначала код проверяет, соответствует ли словарь data определенным шаблонам. В первой версии используется if/else с проверкой наличия ключей и значений, а во второй — конструкция match/case, которая позволяет легко сопоставлять словари с заданными шаблонами.

Пример 5: Сопоставление вложенных структур данных

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

data = {
    "user": {
        "name": "Alice",
        "details": {
            "age": 30,
            "hobbies": ["reading", "hiking"]
        }
    }
}

if (
    "user" in data and
    "name" in data["user"] and data["user"]["name"] == "Alice" and
    "details" in data["user"] and
    "age" in data["user"]["details"] and data["user"]["details"]["age"] == 30 and
    "hobbies" in data["user"]["details"] and data["user"]["details"]["hobbies"] == ["reading", "hiking"]
):
    print("This is Alice, aged 30, who likes reading and hiking.")
else:
    print("Unknown user or different structure.")

А теперь пример с использованием match/case:

data = {
    "user": {
        "name": "Alice",
        "details": {
            "age": 30,
            "hobbies": ["reading", "hiking"]
        }
    }
}

match data:
    case {
        "user": {
            "name": "Alice",
            "details": {
                "age": 30,
                "hobbies": ["reading", "hiking"]
            }
        }
    }:
        print("This is Alice, aged 30, who likes reading and hiking.")
    case _:
        print("Unknown user or different structure.")

В этих примерах идет проверка, соответствует ли вложенная структура данных data определенному шаблону. В первом случае используется конструкция if/else с проверками на наличие ключей и значений на каждом уровне вложенности. Во втором случае используется конструкция match/case, которая позволяет намного проще сопоставлять сложные вложенные структуры данных с заданными шаблонами.

 

Сложный пример кода с использованием match/case в Python

А теперь давайте перейдем от простых примеров кода к более сложным вариантам применения match/case в Python. Фрагмент, который вы сейчас увидите, демонстрирует анализ сложной структуры данных и включает сравнение вложенных структур данных, использование условных проверок (guards) и извлечение значений.

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

def process_message(message):
    match message:
        case {"type": "user_action", "action": action, "details": {"user_id": user_id, "timestamp": timestamp}}:
            return f"User {user_id} performed {action} at {timestamp}"
        
        case {"type": "system_event", "event": event, "metadata": {"severity": severity}} if severity > 3:
            return f"Critical system event: {event} with severity {severity}"
        
        case {"type": "notification", "content": {"title": title, "message": msg}, "recipients": recipients}:
            if "admin" in recipients:
                return f"Admin notification: {title} - {msg}"
            else:
                return f"User notification: {title} - {msg}"
        
        case {"type": "batch", "messages": messages}:
            results = [process_message(msg) for msg in messages]
            return f"Batch processed with results: {results}"
        
        case _:
            return "Unknown message format"
 
# Пример сообщений для обработки
message1 = {
    "type": "user_action",
    "action": "login",
    "details": {"user_id": 42, "timestamp": "2024-08-22T14:00:00Z"}
}
 
message2 = {
    "type": "system_event",
    "event": "disk_failure",
    "metadata": {"severity": 5}
}
 
message3 = {
    "type": "notification",
    "content": {"title": "Maintenance", "message": "Scheduled maintenance at midnight"},
    "recipients": ["admin", "user_123"]
}
 
message4 = {
    "type": "batch",
    "messages": [message1, message2, message3]
}
 
# Обработка сообщений
print(process_message(message1))  # Output: User 42 performed login at 2024-08-22T14:00:00Z
print(process_message(message2))  # Output: Critical system event: disk_failure with severity 5
print(process_message(message3))  # Output: Admin notification: Maintenance - Scheduled maintenance at midnight
print(process_message(message4))  # Output: Batch processed with results: [...]

 

Давайте разберем, что мы видим в этом коде. 

  • case 1: Выполняет сопоставление сообщения типа “user_action” с вложенной структурой данных. Извлекаются значения action, user_id и timestamp. Затем они используются в строке результата.
  • case 2: Выполняет сопоставление сообщения типа “system_event”, но только если значение severity превышает 3 (условие, обозначенное с помощью if severity > 3).
  • case 3: Выполняет сопоставление сообщения типа “notification”. В зависимости от наличия “admin” в списке получателей (recipients), формируется разное сообщение.
  • case 4: Обрабатывает сообщения типа “batch”, содержащие список сообщений. Для каждого сообщения внутри списка рекурсивно вызывается функция process_message, и результаты собираются в список.
  • case _: Собирает все сообщения, не подходящие ни под один из предыдущих шаблонов, и возвращает сообщение об ошибке.

 

Обработка данных в системе умного дома с помощью match/case в Python

Предположим, нам поручили разработать систему управления умным домом. Система получает данные от датчиков, установленных в разных помещениях. Они передают информацию о температуре, влажности, уровне освещения и другие данные. Затем все эти сообщения поступают в устройства, которые, скорее всего, имеют разную структуру. Наша задача состоит в том, чтобы эффективно обработать сообщения от датчиков, используя конструкцию match/case в Python. 

Согласно условиям задачи для каждого типа датчика нужно: 

  • Извлечь из устройства данные.
  • Изменить работу устройства при необходимости (повысить или понизить температуру, включить освещение, отправить сообщение).
  • Реализация должна быть выполнена с помощью match/case в Python 3.10.
def process_sensor_data(sensor_data):
    match sensor_data:
        # Сообщение от датчика температуры
        case {"type": "temperature", "value": temp, "unit": "C"} if temp > 30:
            return f"Warning: High temperature detected: {temp}°C. Turning on the AC."
        
        case {"type": "temperature", "value": temp, "unit": "C"}:
            return f"Temperature is normal: {temp}°C."
        
        # Сообщение от датчика влажности
        case {"type": "humidity", "value": humidity, "unit": "%"} if humidity > 70:
            return f"Warning: High humidity detected: {humidity}%. Activating dehumidifier."
        
        case {"type": "humidity", "value": humidity, "unit": "%"}:
            return f"Humidity is normal: {humidity}%."
        
        # Сообщение от датчика движения
        case {"type": "motion", "detected": True, "location": location}:
            return f"Motion detected in {location}. Turning on the lights."
        
        case {"type": "motion", "detected": False}:
            return "No motion detected."
        
        # Сообщение от датчика освещения
        case {"type": "light", "value": level, "unit": "lux"} if level < 50:
            return f"Low light level detected: {level} lux. Turning on the lights."
        
        case {"type": "light", "value": level, "unit": "lux"}:
            return f"Light level is sufficient: {level} lux."
        
        # Обработка неизвестных типов сообщений
        case _:
            return "Unknown sensor data received."
 
# Примеры входных данных
sensor_data1 = {"type": "temperature", "value": 32, "unit": "C"}
sensor_data2 = {"type": "humidity", "value": 75, "unit": "%"}
sensor_data3 = {"type": "motion", "detected": True, "location": "living room"}
sensor_data4 = {"type": "light", "value": 30, "unit": "lux"}
 
# Обработка данных
print(process_sensor_data(sensor_data1))  # Output: Warning: High temperature detected: 32°C. Turning on the AC.
print(process_sensor_data(sensor_data2))  # Output: Warning: High humidity detected: 75%. Activating dehumidifier.
print(process_sensor_data(sensor_data3))  # Output: Motion detected in living room. Turning on the lights.
print(process_sensor_data(sensor_data4))  # Output: Low light level detected: 30 lux. Turning on the lights.

 

Нужно пояснить, что мы видим в данном коде. Сначала происходит обработка сообщений, полученных от датчиков температуры:

  • Если температура превышает 30°C, система умного дома создает предупреждение и включает кондиционер.
  • Если температура соответствует норме, появляется сообщение об этом.

Обработка сообщений от датчиков влажности:

  • При превышении показателя влажности 70% нужно включить осушитель и создать об этом сообщение.
  • Если влажность соответствует норме, появляется сообщение об этом.

Обработка сообщений от датчиков движения:

  • При обнаружении движения в помещении, система включает освещение в указанном месте.
  • Если движение не фиксируется, выводится сообщение об отсутствии движения.

Обработка данных от датчиков света:

  • При уровне освещенности ниже 50 люкс, система должна включить свет.
  • Если уровень освещенности приемлемый, выводится сообщение.

Обработка неизвестных типов сообщений:

  • Если сообщение не соответствует ни одному из известных типов, выводится сообщение об ошибке.

Преимущества использования match/case в данном примере: 

  • Чистота и читабельность кода: благодаря конструкции match/case можно выразить логику обработки сообщений структурно и лаконично.
  • Гибкость: используя guards (if), можно изменять как структуру данных, так и их значения.
  • Расширяемость: добавление новых типов датчиков и новых условий обработки происходит без существенных изменений кода.

Заключение

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

Останні статті

Что такое прокси-сервер: пояснение простыми словами, зачем нужны прокси

Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…

21.11.2024

Что такое PWA приложение? Зачем необходимо прогрессивное веб-приложение

Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…

19.11.2024

Как создать игру на телефоне: программирование с помощью конструктора

Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…

17.11.2024

Google Bard: эффективный аналог ChatGPT

В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…

14.11.2024

Скрипт и программирование: что это такое простыми словами

Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…

12.11.2024

Дедлайн в разработке: что это такое простыми словами

Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…

11.11.2024