Рубріки: Теория

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

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

В 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”-версия — более частный случай и должна идти раньше “обычного” удаления.

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

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")

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

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 прекрасно справляется с разными классами и даже умеет сопоставлять вложенные датаклассы.

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

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.

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

Токсичные коллеги. Как не стать одним из них и прекратить ныть

В благословенные офисные времена, когда не было большой войны и коронавируса, люди гораздо больше общались…

07.12.2023

Делать что-то впервые всегда очень трудно. Две истории о начале карьеры PM

Вот две истории из собственного опыта, с тех пор, когда только начинал делать свою карьеру…

04.12.2023

«Тыжпрограммист». Как люди не из ІТ-отрасли обесценивают профессию

«Ты же программист». За свою жизнь я много раз слышал эту фразу. От всех. Кто…

15.11.2023

Почему чат GitHub Copilot лучше для разработчиков, чем ChatGPT

Отличные новости! Если вы пропустили, GitHub Copilot — это уже не отдельный продукт, а набор…

13.11.2023

Как мы используем ИИ и Low-Code технологии для разработки IT-продукта

Несколько месяцев назад мы с командой Promodo (агентство инвестировало в продукт более $100 000) запустили…

07.11.2023

Университет или курсы. Что лучше для получения IT-образования

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

19.10.2023