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

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

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

Для простоты определим кортеж из трех элементов, содержащий информацию о книге, например, такой:

cmd = ("Балакирев С.М.", "Python", 2000.78)

Здесь первые две строки – это автор и название книги, а последнее число – цена книги. Если сейчас прописать в операторе case одну переменную с проверкой на тип данных, например, так:

match cmd:
    case tuple() as book:
        print(f"кортеж: {book}")
    case _:  # wildcard
        print("непонятный формат данных")

то у нас отобразится в консоли строка:

кортеж: ('Балакирев С.М.', 'Python', 2000.78)

То есть, пока все работает ровно так, как на предыдущем занятии: формируется новая переменная book, которая ссылается на кортеж с данными о книге. Но, так как кортеж относится к упорядоченным типам данных (Sequence Types), то его можно распаковывать непосредственно в шаблонах следующим образом:

match cmd:
    case author, title, price:
        print(f"Книга: {author}, {title}, {price}")
    case _:  # wildcard
        print("непонятный формат данных")

Фактически, здесь используется уже знакомая нам конструкция распаковки последовательностей вида:

author, title, price = cmd

Причем, этот шаблон сработает только в том случае, если кортеж содержит ровно три элемента. Давайте ради интереса добавим четвертое значение – год издания:

cmd = ("Балакирев С.М.", "Python", 2000.78, 2022)

После запуска программы увидим строчку:

непонятный формат данных

то есть, шаблон не сработал. В нем мы явно прописали три элемента, а в переданном оказалось четыре. Поэтому перешли в блок default (отбойник).

Но как можно было бы сделать так, чтобы вне зависимости от числа элементов кортежа брать только первые три элемента? Очень просто! Снова вспоминаем правила распаковки последовательностей, где мы с вами использовали оператор * для чтения всех остальных элементов, если они есть:

match cmd:
    case author, title, price, *_:
        print(f"Книга: {author}, {title}, {price}")
    case _:  # wildcard
        print("непонятный формат данных")

То есть в *_ будут попадать все элементы, начиная с четвертого. Если же в кортеже всего три элемента, то шаблон также сработает, только в *_ не будет ни одного элемента.

Сразу отмечу, что все то же самое будет работать и со списками. Если вместо кортежа указать список:

cmd = ["Балакирев С.М.", "Python", 2000.78, 2022]

то на работе рассматриваемых шаблонов это никак не отразится, т.к. это все та же упорядоченная коллекция (Sequence Types).

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

match cmd:
    case author, title, price, *_ if len(cmd) < 6:
        print(f"Книга: {author}, {title}, {price}")
    case _:  # wildcard
        print("непонятный формат данных")

Но чтобы шаблон выглядел в более читаемом виде, допустимо использовать круглые или квадратные скобки:

case (author, title, price, *_) if len(cmd) < 6:

или

case [author, title, price, *_] if len(cmd) < 6:

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

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

case [str() as author, str() as title, float() as price, *_] if len(cmd) < 6:

Или сделать это только у некоторых переменных, например, у цены:

case [author, title, float() as price, *_] if len(cmd) < 6:

Мало того, если переменная может принимать несколько разных типов, то их можно перечислить через оператор ‘|’ следующим образом:

case [author, title, float() | int() as price, *_] if len(cmd) < 6:

Если нам нужно указать какие-либо ограничения на длину строк автора и заголовка, то это делается в guard, например, так:

case [str(author), str(title), price, *_] if len(cmd) < 6 and len(author) < 50 and len(title) < 100:

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

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

cmd = ["Балакирев С.М.", "Python", 2000.78]

или

cmd = [1, "Балакирев С.М.", "Python", 2000.78, 2022]

И нам нужно в обоих случаях выделять автора, заголовок и цену. Все остальное не имеет значения. Как сформировать обработку таких данных? Конечно, здесь мы могли бы прописать два отдельных шаблона, но это привело бы к некоторому дублированию кода, что не хорошо. Лучше определить один шаблон, объединяющий эти два варианта. Сделать это можно следующим образом:

match cmd:
    case (author, title, price) | (_, author, title, price, _):
        print(f"Книга: {author}, {title}, {price}")
    case _:  # wildcard
        print("непонятный формат данных")

Смотрите, мы используем группирующие скобки и указываем, как могут быть организованы данные внутри коллекции. На местах тех элементов, что нам не нужны, стоит символ подчеркивания. А оператор ‘|’ означает «или». То есть, может отработать или первый вариант формата, или второй. Причем, обратите внимание, число переменных в группах должно быть одинаковым и их имена совпадать. Это строго обязательно. То есть, мы не можем, например, во второй группе дополнительно выделить год издания:

case (author, title, price) | (_, author, title, price, year):

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

match cmd:
    case (author, title, price):
        print(f"Книга: {author}, {title}, {price}")
    case (_, author, title, price, year):
        print(f"Книга: {author}, {title}, {price}, {year}")
    case _:  # wildcard
        print("непонятный формат данных")

И для практики. Если мы бы здесь не хотели обрабатывать данные в виде кортежей, то в первом операторе case следовало бы сделать проверку на этот тип данных, например, так:

match cmd:
    case tuple():
        print("формат кортежа недопустим")
    case (author, title, price):
        print(f"Книга: {author}, {title}, {price}")
    case (_, author, title, price, year):
        print(f"Книга: {author}, {title}, {price}, {year}")
    case _:  # wildcard
        print("непонятный формат данных")

Причем, обратите внимание, проверка на тип tuple должна идти вначале перед распаковкой коллекции. Я, думаю, вы понимаете почему? Тогда если переменная cmd принимает тип tuple, то сразу отработает первый оператор case и все другие будут проигнорированы.

Вот, в целом, такие вариации построения шаблонов проверок возможны при обработке упорядоченных коллекций в конструкции match/case. В заключение лишь отмечу, что обычная строка языка Python не относится к типу Sequence Types, поэтому делать ее распаковку внутри шаблонов не получится. С ней можно только как с единым целым – со строкой целиком.

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

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

Видео по теме