Курс по Python: https://stepik.org/course/100707
На предыдущих занятиях мы в целом увидели, как можно прописывать различные шаблоны для
проверок отдельных констант и переменных, а также упорядоченных коллекций list, tuple и более сложных
– dict, set. На этом
занятии посмотрим некоторые реальные приложения конструкции match/case, а также увидим
ограничения при использовании этих операторов.
Давайте
предположим, что нам нужно организовать подключение к БД. Для этого формируется
словарь с необходимыми данными, например, такой:
request = {'server': '127.0.0.1', 'login': 'root', 'password': '1234', 'port': 24}
Затем, с помощью
нашей функции connect_db() выполним проверку данных в запросе request и вернем
результат подключения:
result = connect_db(request)
print(result)
Саму же функцию
запишем в следующем виде:
def connect_db(connect: dict) -> str:
match connect:
case {'server': host, 'login': login, 'password': psw, 'port': port}:
return f"connection: {host}@{login}.{psw}:{port}"
case {'server': host, 'login': login, 'password': psw}:
port = 22
return f"connection: {host}@{login}.{psw}:{port}"
case _: # wildcard
return "error connection"
Здесь сразу
бросается в глаза дублирование кода в первых двух блоках case, а именно,
строчка с возвращением f-строки. Давайте это поправим. Вынесем
ее из блока match и запишем в
самом конце функции:
def connect_db(connect: dict) -> str:
match connect:
case {'server': host, 'login': login, 'password': psw, 'port': port}:
pass
case {'server': host, 'login': login, 'password': psw}:
port = 22
case _: # wildcard
return "error connection"
return f"connection: {host}@{login}.{psw}:{port}"
Смотрите, в
первом блоке case записан оператор
pass, который ничего
не делает. Но он нужен, т.к. Python не разрешает
объявление пустых блоков case, в них обязательно должен быть хотя бы
один оператор. Именно поэтому я и прописал pass. Во втором
блоке также стоит один оператор, который создает переменную port со значением
22. Если первые два блока case не сработают, то
третий отработает в любом случае и функция connect_db() вернет строку
"error connection". Значит,
если мы доходим до последней строки функции, то сработал или первый case, или второй. В
любом случае у нас будут переменные host, login, psw и port, так как все
созданные переменные внутри блока match сохраняются и
за его пределами. Поэтому формирование f-строки пройдет
без ошибок и будет возвращен требуемый результат. Так мы избавляемся от
дублирования кода при одинаковом наборе переменных.
Давайте
рассмотрим еще один пример. Предположим, у нас может информация о книгах быть
представлена в разных форматах, например:
book_1 = ('Балакирев', 'Python', 2022)
book_2 = ['Балакирев', 'Python ООП', 2022, 3432.27]
book_3 = {'author': 'Балакирев', 'title': 'Нейросети', 'year': 2020}
book_4 = {'author': 'Балакирев', 'title': 'Keras + Tensorflow', 'price': 5430, 'year': 2020}
Наша задача
написать функцию, которая бы обрабатывала все эти варианты представления книг и
выдавала кортеж в формате:
(автор,
название, год, цена)
Пусть эта
функция называется book_to_tuple() и вызывается
в программе следующим образом:
result = book_to_tuple(book_1)
print(result)
Саму функцию
удобно реализовать с помощью операторов match/case, например, так:
def book_to_tuple(data: dict | tuple | list) -> tuple | None:
match data:
case author, title, year:
price = None
case author, title, year, price, *_:
pass
case {'author': author, 'title': title, 'year': year, 'price': price}:
pass
case {'author': author, 'title': title, 'year': year}:
price = None
case _: # wildcard
return None
return author, title, year, price
Смотрите,
параметр data может быть
разных типов: словарь, кортеж или список. На выходе функция должна выдавать или
кортеж, или значение None, если входные данные не соответствуют
ожидаемым форматам. Далее, первый блок case распаковывает
кортеж или список, состоящий из трех элементов в формате: автор, название, год
издания. Если шаблон срабатывает, то формируется четвертая переменная price со значением None. Следующий
шаблон ожидает также список или кортеж, состоящий, как минимум из четырех
элементов в порядке: автор, название, год издания, цена. Далее могут идти
какие-либо другие элементы, мы их просто игнорируем. Следующие два блока
проверяют словари. Словарь может содержать или три ключа, тогда формируется
четвертая переменная price со значением None, или четыре
ключа с полным набором необходимых переменных. Если ни один из шаблонов не
срабатывает, то попадаем в последний блок case, который
возвращает значение None, то есть, несоответствие форматов. Если же
отрабатывает один из предыдущих блоков case, то переходим к
последней команде функции, где возвращается кортеж со значениями переменных author,
title, year, price.
Как видите, все
достаточно просто и очевидно. Но здесь опять же есть небольшое дублирование
кода: дважды записана команда price = None. По логике ее
можно вынести за пределы оператора match и записать
перед ним:
def book_to_tuple(data: dict | tuple | list) -> tuple | None:
price = None
match data:
case author, title, year:
pass
case author, title, year, price, *_:
pass
case {'author': author, 'title': title, 'year': year, 'price': price}:
pass
case {'author': author, 'title': title, 'year': year}:
pass
case _: # wildcard
return None
return author, title, year, price
Получим
абсолютно тот же самый результат. (Операторы pass можно не считать
дублированием кода).
Давайте немного
усложним задачу и сделаем дополнительную проверку на целочисленный тип
переменной year и ее
принадлежность диапазону значений (min_year; max_year). Если
сделать это «в лоб», то получим что то вроде:
def book_to_tuple(data: dict | tuple | list, min_year=1800, max_year=3000) -> tuple | None:
price = None
match data:
case author, title, int(year) if min_year < year < max_year:
pass
case author, title, int(year), price, *_ if min_year < year < max_year:
pass
case {'author': author, 'title': title, 'year': int(year), 'price': price} if min_year < year < max_year:
pass
case {'author': author, 'title': title, 'year': int(year)} if min_year < year < max_year:
pass
case _: # wildcard
return None
return author, title, year, price
Видите, здесь
идет явное дублирование кода в шаблонах. Оставить так или поправить, конечно,
решать разработчику, но, на мой взгляд, здесь целесообразно скомбинировать
оператор match с оператором if следующим
образом:
def book_to_tuple(data: dict | tuple | list, min_year=1800, max_year=3000) -> tuple | None:
price = None
match data:
case author, title, int(year):
pass
case author, title, int(year), price, *_:
pass
case {'author': author, 'title': title, 'year': int(year), 'price': price}:
pass
case {'author': author, 'title': title, 'year': int(year)}:
pass
case _: # wildcard
return None
if not (min_year < year < max_year):
return None
return author, title, year, price
То есть, мы
буквально всю проверку вынесли в одну строчку оператора if. Этот пример
показывает, что нас никто не ограничивает в различных комбинациях операторов match и if для написания
более удобного и читабельного текста программы.
В заключение
этого занятия покажу еще одну особенность конструкции match/case. Для простоты
предположим, что у нас имеется некая целочисленная переменная:
и в операторе match хотели бы
выделить две ситуации со значениями 3 и 5. Очевидно, сделать это можно
следующим образом:
match cmd:
case 3:
print("3")
case 5:
print("5")
Но опытный
разработчик знает, что явно прописывать константы в программе не лучший ход.
Правильнее было бы сначала задать константы с числами 3 и 5, а уже потом их
использовать в операторах case. Давайте так и сделаем:
CMD_3 = 3
CMD_5 = 5
cmd = 3
match cmd:
case CMD_3:
print("3")
case CMD_5:
print("5")
Однако, при
запуске обнаруживается синтаксическая ошибка. Почему? Дело в том, что в Python нет констант как
таковых. Здесь объявлены две переменные CMD_3 и CMD_5, которые
указываются в операторах case. В действительности, это аналогично
тому, что мы в case пропишем произвольную переменную,
например, такую:
match cmd:
case _:
print("3")
case CMD_5:
print("5")
Да,
принципиально мы ничего не изменили, просто написали другое имя переменной. Но
вы помните, что она означает – любые данные, т.е. срабатывает всегда, поэтому
второй блок case здесь приводит
к ошибке. Если мы его уберем, то программа всегда будет выводить число 3:
match cmd:
case _:
print("3")
Но это не то,
что нам нужно. Все же, как можно использовать переменные как константы в
операторах case? Здесь есть, по
крайней мере, два варианта. Если сами константы объявлены в текущем модуле и
менять мы этого не хотим, то можно пойти на следующую хитрость:
match cmd:
case int(cmd) as x if x == CMD_3:
print("3")
case int(cmd) as x if x == CMD_5:
print("5")
Однако, выглядит
это слишком громоздко. Тут проще было бы воспользоваться операторами if/elif/else. Во втором
случае разработчики разрешают нам использовать переменные как константы, если переменная
указана через точку. Но что это значит? Например, мы можем переменные CMD_3 и CMD_5 вынести в
отдельный файл (модуль), а потом подключить его через import в текущем
модуле:
import consts
cmd = 3
match cmd:
case consts.CMD_3:
print("3")
case consts.CMD_5:
print("5")
Тогда никаких
проблем не возникнет и программа будет работать, как задумано.
Или, можно пойти
на еще одну хитрость и определить константы внутри какого-либо класса,
например, так:
class Consts:
CMD_3 = 3
CMD_5 = 5
cmd = 3
match cmd:
case Consts.CMD_3:
print("3")
case Consts.CMD_5:
print("5")
В этом случае
тоже все отработает как надо без ошибок.
Вот такие приемы
и особенности есть у конструкции match/case, которые мне
удалось выяснить. Я думаю, что материал этих занятий по операторам match/case позволит вам
достаточно уверенно и легко использовать их в своих программах, там, где они
действительно позволят упростить программный код и сделают его более читаемым и
удобным для редактирования.
Курс по Python: https://stepik.org/course/100707