Улучшите качество кода, украсив его оператором match и срезами объектов.

Python не случайно стал невероятно популярным в современной технической среде. Он, если сравнивать его с другими языками программирования, возможно, является самым удобным и доступным для новичков. И, вместе с этой доступностью, он ещё и обладает огромными возможностями. С его помощью можно решить множество задач в самых разных сферах, им пользуются веб‑разработчики, дата‑сайентисты, учёные.

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

В этом материале мы рассмотрим две таких возможности: оператор match и создание срезов строк и списков. Мы подробно разберёмся с тем, как работают эти механизмы, а так же изучим несколько примеров, которые позволят вам познакомиться с синтаксисом и семантикой соответствующих конструкций.

Оператор match

Оператором match (оператором сопоставления с шаблонами) можно пользоваться начиная с Python 3.10. Это — механизм, который позволяет проверять выполнение условий и предпринимать некие действия при выполнении того или иного условия. Если вы пришли в Python из другого языка, вроде C или JavaScript — вы, возможно, уже знакомы с идеей, лежащей в основе оператора switch.

В принципе, оператор match похож на условный оператор, но у него имеется пара полезных преимуществ. Начнём с изучения базовой структуры оператора match путём его сравнения с обычными условными конструкциями. Потом поговорим о его преимуществах перед такими конструкциями.

Возможно, вам доводилось писать код, подобный нижеприведённому, с помощью которого можно, например, проверить имя владельца банковского счёта:

name = "Yen"

if name == "Yen":
    print("This is your account.")
elif name == "Ben":
    print("This is your sister's account.")
else:
    print("Fraud attempt.")

Если переписать это с использованием match, то получится следующее:

match name:
    case "Yen":
        print("This is your account.")
    case "Ben":
        print("This is your sister's account.")
    case _:
        print("Fraud attempt.")

Разберём этот код:

  • Первая строка и в первом, и во втором варианте будет одной и той же. Это — определение переменной name.

  • В начале оператора match используется ключевое слово match.

  • Затем, при проверке отдельных условий, мы, вместо того, чтобы явным образом что‑то с чем‑то сравнивать, используем оператор case, что позволяет сравнить переменную с одним из возможных значений. В результате конструкцию вида case "Yen" можно воспринимать как проверку того, что содержимое переменной name, которую мы исследуем, равняется "Yen".

  • И наконец, в последнем блоке case, используется символ универсального сопоставления. Он выглядит как знак подчёркивания (_) и играет роль ветви else условного оператора.

Теперь у вас может появиться вопрос о том, зачем может понадобиться использовать match вместо традиционного условного оператора. У меня поначалу возникал такой же вопрос. Меня даже раздражал код, в котором вместо стандартной конструкции if‑else использовался оператор match. Но дело тут в том, что у match есть определённые преимущества перед условным оператором.

Первое преимущество заключается в том, что match — это просто более аккуратная конструкция, позволяющая достичь той же цели, что и if‑else. «Аккуратность кода» как преимущество новой конструкции может показаться чем‑то несерьёзным, но это, на самом деле, очень важно. Весь Python пронизан стремлением к возможности написания чистого, лаконичного кода (если вы мне не верите — введите в своём Python-интерпретаторе import this и нажмите Enter).

Это преимущество заметно особенно ярко при наличии большого количества проверяемых условиях, так как чтение громоздких конструкций, построенных из if и elif, может оказаться непростой задачей. Использование оператора match позволяет сделать код чище и облегчает его чтение для других программистов. Улучшение читабельности кода — это достойная цель для любого, кто пишет на Python.

Помимо этого оператор match может напрямую деконструировать определённые объекты, избавляя программиста от необходимости делать это вручную в условных конструкциях. На практике это означает две вещи:

  • Можно автоматически проверять типы объектов (устраняется необходимость в ручной проверке типов).

  • Можно автоматически обращаться к атрибутам объекта в блоках case.

Рассмотрим пример. Предположим, у нас имеется следующий код, в котором определяются два класса, описывающие разные типы автомобилей:

class Honda:
    # Ниже вы найдёте пояснения по поводу __match_args__
    __match_args__ = ("year", "model", "cost")
    def __init__(self, year, model, cost):
        self.year = year
        self.model = model
        self.cost = cost

class Subaru:
    __match_args__ = ("year", "model", "cost")
    def __init__(self, year, model, cost):
        self.year = year
        self.model = model
        self.cost = cost
        
car = Subaru(2021, "Outback", 18000)

Выше мы определили экземпляр класса Subaru. Теперь мы хотим написать код, который проверяет тип объекта, символизирующего автомобиль, и выводит некоторые атрибуты этого объекта. Если воспользоваться традиционной условной конструкцией — можно получить следующее:

if isinstance(car, Honda):
    print("Honda " + car.model)
elif isinstance(car, Subaru):
    print("Subaru " + car.model)
else:
    print("Failure :(")

В ходе выполнения этого кода при работе с переменной car будет выведено "Subaru Outback". Если переписать пример с использованием оператора match, то получится следующий код, который оказывается проще, чем предыдущий вариант программы:

match car:
    case Honda(year, model, cost):
        print("Honda " + model)
    case Subaru(year, model, cost):
        print("Subaru " + model)
    case _:
        print("Failure")

Возможности оператора match по сопоставлению шаблонов позволяют Python автоматически проверять типы объектов в блоках case, а так же позволяют напрямую обращаться к атрибутам. Обратите внимание на то, что это возможно благодаря тому, что в определение класса включена конструкция __match_args__. Здесь перечислены позиционные Python‑аргументы. В документации по Python рекомендуется, чтобы эта конструкция имитировала бы ту, что применяется в конструкторе __init__ при назначении атрибутов self.

Тот код, в котором используется match, легче и читать, и писать. В нём нет слишком громоздких конструкций. Это, на самом деле, очень маленький пример, но по мере того, как задачи, стоящие перед программистами, усложняются, условные операторы могут становиться гораздо запутаннее.

Принимая во внимание вышесказанное — помните о том, что эта возможность доступна только начиная с Python 3.10. Учитывайте это, если вы пишете код для некоей системы, приложения или проекта, где необходима совместимость с более старыми версиями Python.

Если ваш код будет работать в среде, где используется версия Python, поддерживающая match — подумайте о том, чтобы воспользоваться этой возможностью. Для того, чтобы привыкнуть к match, могут понадобиться некоторые усилия, но это, в долгосрочной перспективе, улучшит ваш код.

Срезы строк и списков

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

В простейшем виде создание срезов (slice) подразумевает использование компактных синтаксических конструкций, позволяющих извлекать части строк и списков в Python. Вот — небольшой пример:

>>> my_str = "hello"
>>> my_str[1:3]
'el'

Синтаксические правила описания срезов требуют использования квадратных скобок, содержащих начальный и конечный индексы, разделённые двоеточием. Помните о том, что в Python индексация начинается с нуля, поэтому индекс 1 в нашем примере соответствует букве 'e'. Кроме того, обратите внимание на то, что при формировании среза элемент, на который указывает индекс, находящийся справа, не включается в результат работы этого механизма. Поэтому система доходит до элемента 3, но не включает его в вывод. В результате мы и получаем 'el', а не 'ell'.

Если нужно начать формирование среза с начала строки или списка, или пройти весь объект до конца — соответствующий индекс можно оставить пустым:

>>> my_lst[:3]
['apple', 'orange', 'blackcurrant']
>>> my_lst[2:]
['blackcurrant', 'mango', 'pineapple']

А если оставить пустыми оба индекса — система выдаст копию всего объекта:

>>> my_str[:]
'hello'
>>> my_lst[:]
['apple', 'orange', 'blackcurrant', 'mango', 'pineapple']

Обратите внимание на то, что и при работе со списками, и при работе со строками их срезы — это совершенно новые объекты, отличающиеся от исходных объектов:

>>> new_lst = my_lst[2:]
>>> new_lst
['blackcurrant', 'mango', 'pineapple']
>>> my_lst
['apple', 'orange', 'blackcurrant', 'mango', 'pineapple']

А теперь — самое интересное. Используя механизм формирования срезов можно применять и отрицательные индексы. Если вы с таким приёмом не знакомы — знайте, что он позволяет начинать отсчёт элементов не от начала, а от конца списка или строки. Последняя буква строки имеет индекс -1, предпоследняя — -2 и так далее.

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

>>> my_str[:-1]
'hell'

И наконец — одна из самых недооценённых возможностей формирования срезов. Она заключается в том, что в квадратных скобках можно указывать не два, а три числа. Третье число описывает нечто вроде «шага», совершаемого при выдаче среза. Это легче всего объяснить на примере:

>>> my_long_lst = ['apple', 'orange', 'blackcurrant', 'mango', 'pineapple', 'grapes', 'kiwi', 'papaya', 'coconut']
>>> my_long_lst[1:-1:2]
['orange', 'mango', 'grapes', 'papaya']

Разберём то, что здесь происходит:

  • Тут, чтобы было понятнее, мы объявляем список, в котором содержится больше элементов, чем в списках, с которыми мы экспериментировали ранее.

  • В выражении формирования среза первые два числа — это 1 и -1. Как мы уже видели — такая конструкция описывает первый и последний элементы обрабатываемого объекта, которым в данном случае является список my_long_list.

  • И наконец — после дополнительного двоеточия, в конце, мы записываем число 2. Всё это сообщает Python о том, что мы хотим взять элементы списка, начиная с первого индекса и заканчивая последним, но при этом нам нужен лишь каждый второй элемент. Если тут воспользоваться числом 3 — будет взят каждый третий элемент, если написать 4 — будет выдан каждый четвёртый элемент и так далее.

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

>>> my_long_lst[-1:1:-2]
['coconut', 'kiwi', 'pineapple', 'blackcurrant']

# Для того чтобы успешно осуществить обход списка в обратном порядке, значение "шага" должно быть отрицательным
# В противном случае получится пустой список
>>> my_long_lst[-1:1:2]
[]

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

>>> my_lst
['apple', 'orange', 'blackcurrant', 'mango', 'pineapple']
>>> my_lst[::-1]
['pineapple', 'mango', 'blackcurrant', 'orange', 'apple']

Поняли — что тут происходит? В качестве упражнения вы можете, вспомнив всё то, что узнали о формировании срезов, самостоятельно проанализировать этот код. Вот вам подсказка: взгляните на описание того, что происходит в том случае, если индексы первого и последнего элемента оставляют пустыми.

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

Какая от всего этого польза дата-сайентисту?

Если говорить о программировании в целом, то надо отметить, что при написании Python‑кода важно учитывать его читабельность и чистоту. Использование вышеописанных возможностей во многом этому способствует. Как мы уже говорили, оператор match, в плане читабельности и чистоты кода, имеет несколько важных преимуществ перед условными конструкциями. А если коснуться срезов списков, то их применение позволяет создавать гораздо более чистый код, чем тот, что получился бы при попытке достичь того же эффекта, применяя какие‑нибудь сложные циклы.

А теперь отойдём от этих соображений, справедливых для любого Python‑программиста, и поговорим о том, что всё это значит для дата‑сайентистов.

С практической точки зрения, если вы работаете дата‑сайентистом, то весьма высока вероятность того, что вы получали формальное образование в сфере, отличной от информатики. Это, скорее всего, была сфера статистики, математики, или даже сфера data science, науки о данных (если вам очень повезло найти соответствующую учебную программу). В ходе освоения подобных учебных программ информационные технологии обычно изучают лишь как инструменты для достижения неких целей.

Основное внимание уделяется изучению базовых принципов программирования, освоению того, что достаточно для обработки данных, выполнения их анализа, создания моделей разных масштабов. В результате не так много времени остаётся для изучения тем наподобие «полезные синтаксические конструкции Python». Да что там говорить — такие темы часто отсутствуют даже там, где учат программистов.

Но применение подобных возможностей языка может помочь вам вывести качество своего кода на новый уровень, может помочь вам выделиться среди своих замечательных коллег и выдать своим клиентам более качественные, чем прежде, результаты. Оператор match и формирование срезов — это два ярких примера полезных возможностей Python, но в этом языке есть ещё очень много всего интересного. Советую вам со всем этим ознакомиться.

Пусть код всегда пребывает на вашей стороне. До новых встреч, друзья.

О, а приходите к нам работать? ???? ????

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Комментарии (26)


  1. vadimr
    20.11.2023 09:59
    -7

    Срезы строк и списков

    Правильный перевод – сечения.


    1. Hungryee
      20.11.2023 09:59
      +6

      С каких это пор?

      Как минимум, сечение - это место разреза, то есть в нашем случае индекс.

      Срез / слайс - это те части, что остаются при сечении, то есть то, в чем мы заинтересованы

      Не нужно знать программирование, достаточно стереометрии


      1. vadimr
        20.11.2023 09:59
        -3

        Именно в программировании этот термин переводится на русский язык словом “сечение” с 1966 года, когда эта синтаксическая возможность впервые появилась в языке PL/I. Далее в APL, Фортране и т.д. Сечения не в Питоне придумали так-то.

        Что касается стереометрии, то речь буквально идёт о сечении одного n-мерного параллелепипеда другим n-мерным параллелепипедом. А вы, вероятно, представляете себе сечение плоскостью, которым обычно ограничиваются в школе.


        1. Hungryee
          20.11.2023 09:59
          +1

          И часто вы в жизни пересекаете произвольный n-мерный параллелепипед другим? Или, быть может, n-мерные параллелепипеды имеют прямое отношение к массивам?

          Загуглите «сечение массива» и посчитайте, сколько ссылок будет релевантны в 2023 году.

          Вылезайте из эпохи прологов и алголов


          1. vadimr
            20.11.2023 09:59

            n-мерный параллелепипед имеет самое прямое отношение к массивам, это и есть n-мерный массив.

            Загуглите «сечение массива» и посчитайте, сколько ссылок будет релевантны в 2023 году.

            Что вы хотите этим сказать?


            1. Hungryee
              20.11.2023 09:59

              Так вы загуглили? Вместо того чтоб минус ставить в карму


              1. vadimr
                20.11.2023 09:59

                Я не знаю, на что вы намекаете, а сами вы не рассказываете. Но если вы загуглите "сечение массива", то увидите материалы вузов и научных институтов, а если "срез массива" - то преимущественно популярную литературу лёгкого жанра.


  1. economist75
    20.11.2023 09:59
    +21

    Не слышать о срезах в Python (см. КДПВ) мог только мертвый.

    А match может здорово навредить, если забыть о том что в бизнесе от 5 до 20% хостов трудятся под Win7 (и Python 3.8, где match нет)/ Эстетики match-case добавляет совсем немного.


    1. vadimr
      20.11.2023 09:59
      +2

      В компилируемых языках case/select обычно отличается от if тем, что изначально траслируется в таблицу переходов, а не в последовательные проверки (и по этой причине управляется выражением перечислимого типа). Но для Питона разница, действительно, чисто эстетическая.


      1. leshabirukov
        20.11.2023 09:59

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

        area :: Figure -> Int
        area circle r = Pi * r*r
        area rect w h = w * h

        В Питоне, конечно, можно не всякое из Хаскеля, но польза от дополнительной функциональщины будет.


      1. ptr128
        20.11.2023 09:59

        Gcc транслирует switch в jump-table только при наличии свыше пяти case. До этого - разницы никакой.


        1. domix32
          20.11.2023 09:59

          Хмм, шланг делал, а вот гцц я так и не смог уличить в jump table.


          1. ptr128
            20.11.2023 09:59

            А просили?

            -fcase-values-threshold, -fjump-table-max-growth-ratio-for-size, -fjump-table-max-growth-ratio-for-speed


            1. domix32
              20.11.2023 09:59

              Прям отдельно - нет, не просил. Только всякие уровни оптимизации дёргал. У шланга оно видимо автоматом в один из уровней попадало.


      1. sami777
        20.11.2023 09:59

        Это в самом лучшем случае. А по факту он разворачивается в ветвление из кучи ifов. По крайней мере так в GCC. Как то приходилось сравнивать время выполнения кучи ифов с свитчем. Оказалось, что выполняются они за одно и тоже время..


    1. magiavr
      20.11.2023 09:59

      Не только в win. В отечественной astra Linux также не стремятся к новому


  1. Andrey_Solomatin
    20.11.2023 09:59
    +3

    Я посмотрел это видео https://www.youtube.com/watch?v=ZTvwxXL37XI и подумал, что могу подить пока и без матчинга. Буду по старинке словрики пользовать на пару с полиморфизмом.


  1. firehacker
    20.11.2023 09:59
    -2

    И наконец, в последнем блоке case, используется символ универсального сопоставления. Он выглядит как знак подчёркивания (_) и играет роль ветви else условного оператора.

    Ну гениально же! Лучшего выбора и сделать было нельзя. Но мне кажется создатели этого языка, подарка судьбы для продавцов курсов, недостаточно преисполнились в своем дзене

    Вместо if нужно было использовать символ ?, а вместо else — символ /.

    Возможно мы увидим это в каком-нибудь ином новорожденном языке, например, языке Udaff. Курсы по которому будут рекламировать из каждой дырки.


    1. baldr
      20.11.2023 09:59
      +1

      На самом деле там в статье символ подчеркивания просто для красоты, поскольку он не используется далее. В реальности это просто имя переменной, в которую будет подставлено значение. Далее эту переменную можно использовать внутри этой ветви.

      На хабре pattern matching уже несколько раз описывался, и гораздо подробнее. Немного обсуждения было тут.

      Лично моё мнение - там внутри него столько всего наворотили, что упоминать "явное лучше неявного" уж лучше постеснялись бы после этого. Happy debugging, bitches.


    1. Hungryee
      20.11.2023 09:59

      В статье логическая ошибка

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


  1. magiavr
    20.11.2023 09:59
    +1

    Меня лично при знакомстве с python удивило отсутствие оператора инкрементации (++) и switch. А после появления match удивление возрасло: чем им не понравилась реализация через switch?


    1. baldr
      20.11.2023 09:59

      Пока Гвидо прокладывал курс - все двигалось более-менее последовательно. Ну решил он что ++ - это синтаксический сахар. Особенно со всеми этими неопределенностями с `i++ + ++i - ++i` и прочими..

      А потом он решил устроить демократию и разрешить парочку PEP добавить контрибьюторам..

      Так и появился моржовый оператор и match..


  1. Igelko
    20.11.2023 09:59
    +2

    Небольшая ремарка - поведение isinstance и match совсем разное.
    Первый проходится по иерархии и проверяет классы всех предков на совпадение.

    >>> isinstance(car, object)
    True
    

    Второй - потрошит объект, доставая из него match_args, позволяя делать ещё и так:

    >>> def match_car(car):
    ...   match car:
    ...     case Subaru(2020, model, cost):
    ...         print("Subaru 2020 " + model)
    ...     case Subaru(2021, model, cost):
    ...         print("Subaru 2021 " + model)
    ...     case _:
    ...         print("Failure")
    >>> match_car(car)
    Subaru 2021 Outback
    

    И важно не перепутать, когда что нужно.

    И я сначала грешным делом подумал, что он на этих строках инстанцирует объект, но нет, там сделали отдельный опкод виртуальной машины MATCH_CLASS для проверки класса.

      5          68 COPY                     1
                 70 LOAD_GLOBAL              4 (Subaru)
                 82 LOAD_CONST               1 (())
                 84 MATCH_CLASS              3
                 86 COPY                     1
                 88 POP_JUMP_FORWARD_IF_NONE    31 (to 152)
                 90 UNPACK_SEQUENCE          3
                 94 LOAD_CONST               3 (2020)
                 96 COMPARE_OP               2 (==)
                102 POP_JUMP_FORWARD_IF_FALSE    23 (to 150)
                104 STORE_FAST               1 (model)
                106 STORE_FAST               2 (cost)
                108 POP_TOP
    


  1. domix32
    20.11.2023 09:59
    +1

    о которых вы не слышали

    match-то ладно, не у всех есть возможность на свежем питоне работать. Но отрицательные индексы? Cерьёзно? Это едва ли не в каждой первой книжке по питону встречается, тот же Dive into Python.


  1. dmproger
    20.11.2023 09:59

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


    1. baldr
      20.11.2023 09:59
      +1

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