Конструкция match/case со словарями и множествами

Курс по Python: https://stepik.org/course/100707

На предыдущих занятиях мы увидели, как можно строить шаблоны проверок для отдельных переменных и упорядоченных коллекций типа list и tuple. Теперь пришло время обработки других коллекций типа dict (словарь) и set (множество). Начнем со словарей.

Давайте предположим, что на вход блока обработки поступает запрос в виде следующего словаря:

request = {'url': 'https://proproprogs.ru/', 'method': 'GET', 'timeout': 1000}

Мы хотим выбрать из него данные по ключам «url» и «method». Шаблон проверки в этом случае будет выглядеть так:

match request:
    case {'url': url, 'method': method}:
        print(f"Запрос: url: {url}, method: {method}")
    case _:  # wildcard
        print("неверный запрос")

Обратите внимание, мы здесь использовали фигурные скобки. Как только мы их прописываем, шаблон сразу ожидает данные в формате «ключ-значение». Далее, мы говорим, что в словаре request должны быть два ключа «url» и «method» с какими то произвольными значениями. Если эти ключи действительно присутствуют, то создаются переменные url и method, которые ссылаются на соответствующие значения ключей. Затем, с помощью print(), мы выводим в консоль строку с этими значениями переменных.

То есть, данный шаблон будет срабатывать всякий раз, как только на вход поступает словарь с ключами «url» и «method». При этом наличие или отсутствие других ключей не играет никакой роли. Например, в словаре request есть еще один ключ «timeout», но, тем не менее, шаблон срабатывает. Это отличает проверку словарей от проверки кортежей и списков, когда мы должны были указывать ровно то число элементов, которые в них ожидаются. Для словарей же достаточно лишь наличие указанных ключей и не более того.

Далее, если мы дополнительно хотим сделать проверку на тип данных в значениях словаря, то это можно сделать следующим образом:

case {'url': str(url), 'method': int() as method}:

Я здесь специально проверку типа записал двумя разными способами, чтобы напомнить о них. В первом ключе мы требуем, чтобы была строка, а во втором – чтобы было целое число. Конечно, сейчас шаблон не сработает, т.к. method является строкой, но если мы его изменим на число:

request = {'url': 'https://proproprogs.ru/', 'method': 1, 'timeout': 1000}

то он среагирует на этот словарь.

Записывая после ключей имена переменных, мы, тем самым, определяем шаблон для произвольных значений заданного типа (или любого типа, если этих проверок нет). Но можно записать ключ с конкретным значением, например, так:

case {'url': url, 'method': method, 'timeout': 1000}:

Тогда оператор case отработает только если ключ 'timeout' принимает значение 1000 и будет игнорировать все другие значения. Например, если:

request = {'url': 'https://proproprogs.ru/', 'method': 'GET', 'timeout': 2000}

то перейдем в последний блок case (отбойник). Но, если мы скомбинируем эти два варианта в одном шаблоне:

case {'url': url, 'method': method, 'timeout': 1000} | {'url': url, 'method': method, 'timeout': 2000}:

то сможем отлавливать и значение 1000 и значение 2000 для ключа 'timeout'. То есть, здесь оператор ‘|’ работает так же, как и с упорядоченными коллекциями.

Также мы, при необходимости, можем прописывать guard (защитника) при обработке словарей. Например, если в запросе request ожидается не более трех ключей, то такой шаблон можно записать в виде:

case {'url': url, 'method': method} if len(request) <= 3:

И так далее, здесь допустимо использовать все те же самые конструкции, которые мы рассматривали на предыдущих занятиях.

Давайте теперь предположим, что нам нужно построить шаблон проверки словаря, в котором есть ключи «url» и «method» и дополнительно может быть еще не более двух ключей. Как это сделать? Для этого следует воспользоваться оператором упаковки ** всех остальных данных словаря, кроме указанных ключей, например, так:

match request:
    case {'url': url, 'method': method, **kwargs} if len(kwargs) <= 2:
        print(f"Запрос: url: {url}, method: {method}")
    case _:  # wildcard
        print("неверный запрос")

В результате в переменной kwargs будут находиться все остальные ключи с их значениями, кроме ключей «url» и «method». А далее, мы просто проверяем, чтобы словарь kwargs содержал не более двух дополнительных ключей.

Теперь, например, для запроса (словаря):

request = {'id': 2, 'url': 'https://proproprogs.ru/', 'method': 'GET', 'timeout': 2000}

шаблон сработает, а для словаря:

request = {'id': 2, 'url': 'https://proproprogs.ru/', 'method': 'GET', 'timeout': 2000, 'date': 100}

не сработает, т.к. дополнительно имеем уже три ключа. Причем расположение ключей в словаре не имеет никакого значения при работе шаблона. Главное, чтобы существовали ключи «url» и «method» и кроме них было не более двух дополнительных ключей.

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

case {'url': url, 'method': method, **kwargs} if not kwargs:

Мы здесь в защитнике проверяем, чтобы словарь kwargs был пустым.

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

json_data = {'id': 2, 'type': 'list', 'data': [1, 2, 3], 'access': True, 'date': '01.01.2023'}

Здесь множество ключей, причем, один из них (data) может содержать список, если ключ «type» принимает значение «list». Шаблон обработки можно записать в виде:

match json_data:
    case {'type': 'list', 'data': lst}:
        print(f"JSON-данные: type-list: {lst}")
    case _:  # wildcard
        print("неверный запрос")

Первый блок case сработает только в том случае, если ключ «type» имеет значение «list» и имеется еще один ключ «data». Ожидается, что переменная lst в этом случае будет ссылаться на список. Конечно, для надежности, мы можем сделать дополнительную проверку типа данных:

case {'type': 'list', 'data': list() as lst}:

Тогда гарантированно, при срабатывании блока case, переменная lst будет принимать тип list. Видите, как легко, просто и элегантно мы сделали обработку нужного нам формата данных с помощью операторов match/case. Гораздо проще, чем если бы использовали условные операторы if/elif/else. Именно в этом преимущество конструкции match/case перед условными операторами.

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

json_data = {'id': 2, 'access': True, 'info': ['01.01.2023', {'login': '123', 'email': 'email@m.ru'}, True, 1000]}

Нам нужно проверить наличие ключей 'access' и 'info' и выделить значение ключа 'access' и email из ключа 'info'. Сделать это можно следующим образом:

match json_data:
    case {'access': access, 'info': [_, {'email': email}, _, _]}:
        print(f"JSON: access: {access}, email: {email}")
    case _:  # wildcard
        print("неверный запрос")

Смотрите, как красиво все выглядит! Мы буквально берем значение ключа 'access' и нужное значение для email из списка ключа 'info', попутно проверяя наличие этих ключей и списка в словаре json_data. Причем список обязательно должен содержать четыре элемента и во втором храниться словарь с ключом email. Попробуйте сделать то же самое через операторы if/elif/else и вы увидите, насколько сложнее будет выглядеть эта же программа! Тогда как конструкция match/case с легкостью решает поставленную задачу.

Обработка множеств

Вот в целом так строятся проверки для словарей. Помимо них есть еще одна похожая коллекция – множество (set). Предположим, мы бы хотели выбирать все множества, состоящие из трех элементов, например:

primary_keys = {1, 2, 3}

Как это можно сделать? Если прописать по аналогии со словарем:

match primary_keys:
    case {a, b, c}:
        print(f"Primary Keys: {a}, {b}, {c}")
    case _:  # wildcard
        print("неверный запрос")

то получим синтаксическую ошибку. Для множеств так записывать шаблоны нельзя. Если попробовать убрать фигурные скобки, то будут ожидаться на входе кортежи или списки. Как же быть с множествами? Вариант, фактически, один. Прописать явно тип set и присвоить некоторой переменной ссылку на множество:

match primary_keys:
    case set() as keys:
        print(f"Primary Keys: {keys}")
    case _:  # wildcard
        print("неверный запрос")

А проверку, чтобы множество содержало ровно три элемента, можно сделать с помощью guard:

case set() as keys if len(keys) == 3:

По сути, только так мы можем отбирать множества и через guard записывать некоторые проверки.

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

Курс по Python: https://stepik.org/course/100707

Видео по теме