ru:https://highload.today/blogs/structural-pattern-matching-chto-python-3-10-nam-gotovit-chast-ii/ ua:https://highload.today/uk/blogs/structural-pattern-matching-chto-python-3-10-nam-gotovit-chast-ii/
logo
Теория      17/03/2021

Structural pattern matching: что Python 3.10 нам готовит (часть II)

Павел Дмитриев BLOG

full-stack iOS developer компании Postindustria

В 6-ю альфа-версию Python 3.10, вышедшую в марте, включено одно из самых больших нововведений в истории языка: новая синтаксическая конструкция structural pattern matching (SPM).

Это продолжение рассказа о SPM, начатого этим текстом.

Словари и встроенные типы данных

Отдел маркетинга решил что строчка “Web scale” будет отлично смотреться на сайте нашего KVS, и попросил добавить в него HTTP API. Продакт, ненадолго отвлекшись от экспериментов с новой кофеваркой, постановил: “Отправлять и принимать строки — это какой-то прошлый век”. Согласовав вопрос с CTO, он потребовал сделать альтернативный API, принимающий на вход JSON.

Сперва — традиционно добавляем метод для работы с новым форматом команд.

def api_command(self, command: Dict):
    print(f"< {dumps(command)}")
    match command:

Как вы уже понимаете, Python умеет матчить не только простые типы данных и списки, но и словари. Начинаем добавлять команды. Сохранение и получение значений удалось скинуть на джуна, строго-настрого наказав ему не забыть про тесты; нам же достался более сложный фрагмент бизнес-логики — удаление.

case {"action": "delete", "id": int(id)} if not self._protected:
        print(f"> delete {id}")

При сопоставлении словарей проверяются только те ключи, которые указаны в case, остальные игнорируются. Как и для других значений, можно потребовать строгого соответствия одному из нескольких вариантов, как мы делаем с параметром action,  либо же “захватить” значение ключа в локальную переменную, как сделано для id. Тут показана еще одна продвинутая возможность match: мы можем проверять шаблоны на соответствие базовым типам Python; в данном случае ветка будет работать только если id без ошибок приведется к целому числу. Да, пока мы были в отгуле, сервер успели переписать для использования целых чисел в роли ключей.

Конечно же, мы не забыли добавить проверку на включение защиты, но во время демонстрации текущей сборки потенциальным инвесторам CTO забыл об этом и не смог удалить значение. Вернувшись, он потребовал “срочно добавить power users необходимые возможности”. Выдернув из медитации продакта, вы совместно решили, что это означает поддержку удаления с параметром force.

К счастью, с SPM сделать это не сложно:

case {"action": "delete", "id": int(id), "force": True}:
    print(f"> force delete {id}")

Смотрим, как это работает:

server.string_command("protection off")
# > Protection state is False

server.api_command({"action": "delete", "id": "oldkey"})
# > Invalid command

server.api_command({"action": "delete", "id": 1})
# > delete 1

server.string_command("protection on")
# > force delete 1

server.api_command({"action": "delete", "id": 1, "force": True})
# > Protection state is True

Как видите, все действует так, как мы ожидаем. Команда с нечисловым id не распознается, а дополнительный ключ включает именно “силовое” удаление. Не забудьте, что “force”-версия — более частный случай и должна идти раньше “обычного” удаления.

Курс-професія "Junior Data Analyst" від robot_dreams.
Комплексний курc для всіх, хто хоче опанувати нову професію з нуля.На прикладі реальних датасетів ви розберете кожен етап аналізу даних.
Програма курсу і реєстрація

Как быть, если нам нужно “словить” какой-то параметр, не укладывающийся в рамки базовых типов данных? Вот один из вариантов решения:

case {"kill_nodes": args} if self._is_int_list(args):
    print(f"> killing {args!r}")

Метод, используемый в защитном условии выглядит как-то так:

@staticmethod
def _is_int_list(param):
        return isinstance(param, list) and all(isinstance(item, int) for item in param)

Дописав этот код, мы поскорее пушаем его и закрываем тикет, пока руководству не пришло в голову сделать какой-то флаг для защиты от удаления с force.

Лирическое отступление: на самом деле, наличие защитного блока в сочетании с матчингом простых типов позволяет довольно сильно “абюзить” новый оператор, создавая код вроде такого:

match "bar str":
    case str(s) if s.startswith("foo"):
        print(f"pass1 {s}")
    case str(s) if s.startswith("bar"):
        print(f"pass2 {s}")

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

Классы

Пока вы обдумывали, на кого бы свалить написание Flask-кода для сетевого слоя API, позвонил продакт и, как бы невзначай поигрывая новым умным браслетом с оксиметром, сказал что надо сделать интеграцию с библиотекой Gata, используемой для популярной облачной платформы Baboon Wide Services. Поэтому наш сервер должен уметь обрабатывать команды в Gata-вском формате. Радует в этом лишь то, что команды Gata — это простые dataclass, а SPM умеет с этим работать.

Начнем с интеграции самой базовой команды библиотеки, ее класс выглядит так:

@dataclass
class APICommand:
    action: str
    node: int
    comment: str
    force: bool = False

    def __str__(self):
        return f"{self.action}({self.node}, force={self.force})"

Знакомая уже заглушка нового метода:

def thirdparty_command(self, command: ServerCommands):
    print(f"< {command!s}")

    match command:
        case _:
            print("> Invalid command")

По традиции, можно начать с команд удаления:

Онлайн-курс "Business English for Marketers" від Laba.
Опануйте професійну англійську для маркетингу.Розширте карʼєрні можливості для роботи з іноземними колегами: від розробки нових продуктів до презентації стратегії бренду.
Детальніше про курс
case APICommand(("delete" | "rm"), node, force=True):
    print(f"> force delete {node}")
case APICommand(("delete" | "rm"), node) if not self._protected:
    print(f"> delete {node}")

Синтаксис выглядит достаточно очевидно, датакласс “матчится”, если совпадают его поля. При этом, как и раньше, можно использовать | и захватывать переменные. Поля, которые опущены, игнорируются.

В первом варианте case мы принудительно указали имя поля force, если бы этого не было сделано, с True сравнивалось бы поле comment, поскольку оно идет в APICommand третьим. В качестве альтернативной меры можно пропускать “ненужное” поле с помощью _,тогда строка шаблона выглядела бы так: case APICommand(("delete" | "rm"), node, _, True).

Но на самом деле комментарии нам совсем не нужны, и было бы здорово вообще игнорировать это поле при сопоставлении с шаблоном. Это не сложно сделать, указав список полей, которые участвуют в сопоставлении с помощью нового атрибута (разработчики Gata пообещали добавить его в свой класс).

__match_args__ = ["action", "node", "force"]

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

Теперь добавлять новые паттерны стало проще.

case APICommand("erase", node, True):
    print(f"> erasing {node}")

Последнее значение True как раз соответствует полю force.

Разумеется, один match-блок может обрабатывать разные датаклассы. Нам это пригодится, потому что в Gata нашлись дополнительные классы команд: административные и “суперпользовательские”.  Последние просто оборачивают обычные команды, повышая их привилегии:

@dataclass
class AdminCommand:
    action: str
    password: str


@dataclass
class RootCommand:
    command: APICommand

Добавляем поддержку пары команд:

case AdminCommand("shutdown", self._password):
    print("> shutting down")
case RootCommand(APICommand(("delete" | "rm"), node)):
    print(f"> root delete {node}")

Как видите, оператор match прекрасно справляется с разными классами и даже умеет сопоставлять вложенные датаклассы.

Курс English For IT: Communication від Enlgish4IT.
Почни легко працювати та спілкуватися з мультикультурними командами та міжнародними клієнтами. Отримайте знижку 10% за промокодом ITCENG.
Інформація про курс

Посмотрим, как это работает:

server.thirdparty_command(APICommand("delete", 1, "Do it?"))
# > Invalid command

server.thirdparty_command(APICommand("delete", 1, "Do it!", force=True))
# > force delete 1

server.thirdparty_command(APICommand("erase", 12, "Not needed", force=True))
# > erasing 12

server.thirdparty_command(AdminCommand("shutdown", "test123"))
# > shutting down

server.thirdparty_command(RootCommand(APICommand("delete", 1, "sudo!")))
# > root delete 1

На этом моменте CTO вернулся с совещания с инвесторами и сказал, что финансирования не будет и проект свернут.

Заключение

Надеюсь, я сумел вас убедить в том что structural pattern matching — это одно из самых больших нововведений в истории Python. Инструмент получился настолько мощным, что были даже призывы не допустить его добавления, аргументированные тем, что он сильно усложняет язык, снижая “фирменную” простоту. Впрочем, консенсус большинства (включая Гвидо ван Россума) был прост: у тех, для кого эта возможность слишком сложна, остается прекрасная возможность — ее не использовать. Стоит отметить, что обсуждение на этот раз вышло короче, чем при добавлении :=, — видимо, желание получить в свои руки новую игрушку все же победило.

Несколько векторов потенциального расширения SPM видны уже сейчас, например, было бы здорово иметь какой-то оператор вроде continue, заставляющий интерпретатор продолжить сопоставление, даже если он уже нашел подходящую ветку. Еще можно было бы сделать возможность “кастомного матчинга” классов, добавив в них служебный метод-предикат, возвращающий булевый флаг. Впрочем, требовать этого от первого релиза действительно было бы “слишком жирно”.

Теперь нам остается ждать повсеместного внедрения новой версии Python, чтоб писать более эффектный код. А скрасить ожидание поможет чтение обсуждения одного из следующих “больших изменений”: сокращенного синтаксиса объявления анонимных функций.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Онлайн курс UI/UX Design Pro від Ithillel.
Навчіться проєктувати інтерфейси з урахуванням поведінки користувачів, розв'язувати їх проблеми через Customer Journey Mapping, створювати дизайн-системи і проводити дослідження юзабіліті, включаючи проєктування мобільних додатків для Android та iOS і розробку UX/UI на основі даних!
Дізнатися більше

Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: