Pattern matching. Теперь и в Python

—

от автора

Привет!

В юбилейный минор третьего питона наконец-то завезли pattern matching. Саму концепцию сложно назвать новой, она уже реализована во многих языках, причём как нового поколения (Rust, Golang), так и у тех, кому уже за 0x18 (Java).

Анонсировал pattern matching Гвидо ван Россум, автор языка программирования Python и «великодушный пожизненный диктатор»

Меня зовут Денис Кайшев, я код-ревьюер на курсе «Мидл Python-разработчик». В этом посте хочу рассказать, зачем в Python pattern matching и как с ним работать.

Синтаксически конструкция pattern matching по сути аналогична тому, как это представлено в ряде других языков:

match_expr:     | star_named_expression ',' star_named_expressions?     | named_expression match_stmt: "match" match_expr ':' NEWLINE INDENT case_block+ DEDENT case_block: "case" patterns [guard] ':' block guard: 'if' named_expression patterns: value_pattern ',' [values_pattern] | pattern pattern: walrus_pattern | or_pattern walrus_pattern: NAME ':=' or_pattern or_pattern: '|'.closed_pattern+ closed_pattern:     | capture_pattern     | literal_pattern     | constant_pattern     | group_pattern     | sequence_pattern     | mapping_pattern     | class_pattern capture_pattern: NAME !('.' | '(' | '=') literal_pattern:     | signed_number !('+' | '-')     | signed_number '+' NUMBER     | signed_number '-' NUMBER     | strings     | 'None'     | 'True'     | 'False' constant_pattern: attr !('.' | '(' | '=') group_pattern: '(' patterns ')' sequence_pattern: '[' [values_pattern] ']' | '(' ')' mapping_pattern: '{' items_pattern? '}' class_pattern:     | name_or_attr '(' ')'     | name_or_attr '(' ','.pattern+ ','? ')'     | name_or_attr '(' ','.keyword_pattern+ ','? ')'     | name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')' signed_number: NUMBER | '-' NUMBER attr: name_or_attr '.' NAME name_or_attr: attr | NAME values_pattern: ','.value_pattern+ ','? items_pattern: ','.key_value_pattern+ ','? keyword_pattern: NAME '=' or_pattern value_pattern: '*' capture_pattern | pattern key_value_pattern:     | (literal_pattern | constant_pattern) ':' or_pattern     | '**' capture_pattern 

Может показаться сложным и запутанным, но на самом деле всё сводится примерно к такому виду:

match some_expression:     case pattern_1:         ...     case pattern_2:         ... 

Это выглядит куда понятнее и приятнее глазу.

Сами шаблоны разбиты на несколько групп:

  • Literal Patterns;
  • Capture Patterns;
  • Wildcard Pattern;
  • Constant Value Patterns;
  • Sequence Patterns;
  • Mapping Patterns;
  • Class Patterns.

Расскажу немного о каждой из них.

Literal Patterns

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

Это выглядит как string == 'string', используется метод __eq__.

match number:     case 42:         print('answer')     case 43:         print('not answer') 

Capture Patterns

Шаблон захвата позволяет связать переменную с заданным в шаблоне именем и использовать это имя внутри локальной области видимости.

match greeting:     case "":         print('Hello my friend')     case name:         print(f'Hello  {name}')

Wildcard Pattern

Если вариантов сопоставления слишком много, то можно использовать _ , что является неким значением по умолчанию и будет совпадать со всеми элементами в конструкции match

 match number:     case 42:         print("Its’s forty two")     case _:         print("I don’t know, what it is")

Constant Value Patterns

При использовании констант нужно использовать dotted names, к примеру перечисления, иначе сработает паттерн захвата.

OK = 200 CONFLICT = 409  response = {'status': 409, 'msg': 'database error'} match response['status'], response['msg']:     case OK, ok_msg:         print('handler 200')     case CONFLICT, err_msg:         print('handler 409')     case _:         print('idk this status')

И ожидаемый результат будет не самым очевидным.

Sequence Patterns

Позволяет сопоставлять списки, кортежи и любые другие объекты от collections.abc.Sequence, кроме str, bytes, bytearray.

answer = [42] match answer:     case []:            print('i do not find answer')     case [x]:         print('asnwer is 42')     case [x, *_]:         print('i find more than one answers')

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

Mapping Patterns

Эта группа немного похожа на предыдущую, только здесь мы сопоставляем словари, или, если быть точным, объекты типа collections.abc.Mapping. Их можно достаточно неплохо сочетать друг с другом.

args = (1, 2) kwargs = {'kwarg': 'kwarg', 'one_more_kwarg': 'one_more_kwarg'}  def match_something(*args, **kwargs):     match (args, kwargs):         case (arg1, arg2), {'kwarg': kwarg}:             print('i find positional args and one keyword args')         case (arg1, arg2), {'kwarg': kwarg, 'one_more_kwarg': one_more_kwarg}:             print('i find a few keyword args')         case _:             print('i cannot match anything')  match_something(*args, **kwargs)

И всё бы ничего, но есть особенность. Этот паттерн гарантирует вхождение этого ключа (ключей) в словарь, но длина словаря не имеет значения. Поэтому на экране появится i find positional args and one keyword args.

Class patterns

Что касается пользовательских типов данных, то здесь используется синтаксис, схожий с инициализацией объекта.

Вот как это будет выглядеть на примере дата-классов:

from dataclasses import dataclass  @dataclass class Coordinate:     x: int     y: int     z: int  coordinate = Coordinate(1, 2, 3) match coordinate:     case Coordinate(0, 0, 0):         print('Zero point')     case _:         print('Another point') 

Также можно использовать if, или так называемый guard. Если условие ложно, то сопоставление с шаблоном продолжится. Стоит отметить, что сначала происходит сопоставление с шаблоном, и только после этого проверяется условие:

case Coordinate(x, y, z) if z == 0:     print('Point in the plane XY')

Если использовать непосредственно классы, то нужен атрибут __match_args__, в котором необходимы позиционные аргументы (для namedtuple и dataclasses __match_args__ генерируется автоматически).

class Coordinate:     __match_args__ = ['x', 'y', 'z']      def __init__(self, x, y, z):         self.x = x         self.y = y         self.z = z  сoordinate = Сoordinate(1, 2, 3) match Сoordinate:     case Сoordinate(0, 0, 0):         print('Zero Сoordinate')     case Сoordinate(x, y, z) if z == 0:         print('Сoordinate in the plane Z')     case _:         print('Another Сoordinate') 

Иначе вызовется исключение TypeError: Coordinate() accepts 0 positional sub-patterns (3 given)

Что в итоге?

Фактически это выглядит как очередной синтаксический сахар наряду с недавним walrus operator. Реализация в текущем её виде преобразует блоки оператора match в эквивалентные конструкции if/else, а именно в байт-код, который имеет такой же эффект.

Армин Ронахер, создатель веб-фреймворка Flask для Python, очень ёмко описал текущее состояние Pattern matching

Да, сложно спорить: код станет несколько чище, нежели это была бы башня из if/else на треть экрана. Но и назвать это тем, что производит вау-эффект, тоже нельзя. Неплохо, что оно вводится: местами это будет удобно использовать, но не везде. Так или иначе, главное с этой новизной не переусердствовать, не бежать быстрее обновлять все проекты на 3.10 и всё переписывать, ведь:

Now is better than never. Although never is often better than right now.

Будете использовать? Если да, то где?

ссылка на оригинал статьи https://habr.com/ru/company/yandex_praktikum/blog/547902/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *