Присваивания переменных внутри выражений Python оператором Walrus Python

Присваивания переменных внутри выражений Python оператором Walrus

Любое, даже самое гениальное изобретение со временем требует совершенствования и абсорбции того наилучшего, что имеется в подобных ему продуктах человеческого разума. Не является исключением и Python, который невзирая на изначально заслуженную популярность, с каждой новой версией интенсивно развивается, совершенствуя свой синтаксис. Одним из подтверждений этому является появление в сравнительно новой Python версии 3.8 оператора выражения присваивания – (:=), который в просторечии еще называют моржовым оператором (walrus operator) за свою схожесть с глазами и бивнями моржа.

Данный оператор по функциональности практически идентичен обычному оператору присваивания “=” за исключением того, что он может присваивать значения переменных прямо внутри выражений Python, что зачастую помогает упростить код программы, сделав его более компактным и читабельным. Вместе с тем, наряду с преимуществами моржового оператора, его применение иногда имеет ряд подводных камней и не рекомендуется к реализации. Возможно, существованием именно таких неоднозначностей при применении данного оператора, в частности, было продиктовано бурное обсуждение этого новшества, по итогам которого Гвидо ван Россум (автор Python) все-таки принял его в июле 2018 году и, с тех пор объявил, что устраняется от роли главы проекта – доброжелательного диктатора (BDFL).

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

Особенности реализации операторов присвоения в Python

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

int x = 3, y = 8;
if (x = y) {
    printf("х и у равны (x = %d, y = %d)", x, y);
}

Здесь if (x = y) будет оцениваться как true. Следовательно, фрагмент кода будет распечатан как: х и у равны (x = 8, y = 8). Это, конечно же, далеко не тот результат, который нами ожидался. Ведь мы пытались сравнить изначальные значения x и y, но как же вдруг значение x изменилось с 3 до 8?

А, объяснятся данная ситуация тем, что мы использовали оператор присваивания (=) вместо оператора сравнения (==). Ведь в языке C - x = y является выражением, в котором значение y присваивается x, следовательно, пример x = y оценивается как 8, что считается истинным в контексте if утверждения.

Но, взгляните на аналогичный пример, написанный на Python. Этот код вызывает SyntaxError:

x, y = 3, 8
if x = y:
   print(f"x and y are equal ({x = }, {y = })")

В отличие от примера, написанного на C, с целью недопущения двузначности в интерпретации кода, Python обеспечивает четкое разграничение между оператором обычного присваивания (=) и оператором присваивания внутри выражения (:=), что предотвращает возникновения таких трудно обнаруживаемых ошибок, какие были описаны выше на C. Необходимость такого разграничения между операторами присваивания вследствие вышеназванных причин прописана также и в Официальных рекомендациях по стилю программирования PEP 572*.

Разграничения между операторами (=) и (:=) в Python заключаются отнюдь не только лишь в визуальных их различиях. Прежде всего разграничения этих операторов состоят в том, что каждый из них должен применяться исходя из своего сугубо специфического предназначения. Другими словами, для обоих этих операторов не существует идентичных контекстов кода, в которых они оба были бы допустимы. Например, мы не можем выполнить простое присваивание с помощью моржового оператора:

>>> morzh := True
    File "<stdin>", line 1
    morzh := True
           ^
SyntaxError: invalid syntax

Конечно же, для моржового оператора в этом случае и, во многих других случаях с целью корректной работы программы мы можем добавить вокруг выражения присваивания круглые скобки (()) и, тем самым избежать ошибки:

# Выражение действительно, но в данном контексте не рекомендовано
>>> (morzh := True)
True

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

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

Варианты использования моржового оператора

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

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

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

Отладка программ

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

image

где, ϕ1 и ϕ2 широта, а λ1 и λ2 долгота соответственно 1-й и 2-й точек на земной поверхности, между которыми нам нужно найти расстояние.

Чтобы продемонстрировать эту формулу на практики, давайте попробуем рассчитать расстояние между Осло (59,9° с. ш., 10,8° в. д.) и Ванкувером (49,3° с. ш., 123,1° з. д.) следующим образом:

>>> from math import asin, cos, radians, sin, sqrt

>>> # Примерный радиус Земли в километрах
>>> rad = 6371

>>> # Расположение Осло и Ванкувера
>>> ϕ1, λ1 = radians(59.9), radians(10.8)
>>> ϕ2, λ2 = radians(49.3), radians(-123.1)

>>> # Расстояние между Осло и Ванкувером
>>> S = 2 * rad * asin(
...     sqrt(
...         sin((ϕ2 - ϕ1) / 2) ** 2
...         + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
...     )
... )
>>> print(S)
7181.7841229421165

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

Примечание. Поскольку исходный код Python обычно сохраняется в универсальной кодировке UTF-8, то в нашем коде мы свободно можем использовать такие греческие буквы, как ϕ и λ. Это, в частности, весьма удобно при переносе математических формул в необходимый нам Python код.

Однако, следует помнить, что хотя полный UTF-8 и поддерживается, например в строках, имена переменных в Python все же используют более ограниченный набор символов. В частности, при именовании наших переменных мы не можем использовать смайлики, что с точки зрения наглядности и читабельности кода весьма обосновано.

Теперь давайте предположим, что у нас как-то не заладилось с правильным ответом и, следовательно нам требуется разложить наше выражение на несколько подвыражений с тем, чтобы по отдельности проверить их правильность. Допустим в нашем конкретном примере нам нужно просто посмотреть, насколько первый из членов гаверсинуса влияет на конечный результат. Конечно же для этого можно в отдельной строке продублировать необходимое нам подвыражение, присвоив его результат соответствующей переменной и, выведя затем ее на печать. Однако, мы можем избежать такого дублирования, просто за счет оператора (:=) дав имя соответствующему подвыражению прямо внутри общего расчета по формуле гаверсинуса:

>>> S = 2 * rad * asin(
...     sqrt(
...         (ϕ_hav := sin((ϕ2 - ϕ1) / 2) ** 2)
...         + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
...     )
... )
>>> print(S)
7181.7841229421165
>>> print(ϕ_hav)
0.008532325425222883

Преимущество использования здесь моржового оператора заключается в том, что одновременно с вычислением значения (S) для полного выражения, параллельно мы можем также отследить и значение ϕ_hav. Это в свою очередь дает нам всю необходимую информацию для отладки с минимальными издержками.

Списки и словари

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

>>> sampling = [2, 8, 0, 1, 1, 9, 7, 7]

>>> statistical_parameters = {
...     "Объем выборки": len(sampling),
...     "Сумма элементов": sum(sampling),
...     "Среднее значение": sum(sampling) / len(sampling),
... }

>>> print(statistical_parameters)
{'Объем выборки': 8, 'Сумма элементов': 35, 'Среднее значение': 4.375}

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

>>> sampling = [2, 8, 0, 1, 1, 9, 7, 7]

>>> sampling_size = len(sampling)
>>> sampling_sum = sum(sampling)

>>> statistical_parameters = {
...     "Объем выборки": sampling_size,
...     "Сумма элементов": sampling_sum,
...     "Среднее значение": sampling_sum / sampling_size,
... }

>>> print(statistical_parameters)
{'Объем выборки': 8, 'Сумма элементов': 35, 'Среднее значение': 4.375}

Но, применив моржовый оператор вместо обычного оператора присвоения, мы несомненно обеспечим нашему коду, как более компактный, так и более понятный вид.

>>> sampling = [2, 8, 0, 1, 1, 9, 7, 7]

>>> statistical_parameters = {
...     "Объем выборки": (sampling_size := len(sampling)),
...     "Сумма элементов": (sampling_sum := sum(sampling)),
...     "Среднее значение": sampling_sum / sampling_size,
... }

>>> print(statistical_parameters)
{'Объем выборки': 8, 'Сумма элементов': 35, 'Среднее значение': 4.375}

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

Примечание. Область действия переменных sampling_size и sampling_sum, как в примере с обычным оператором присвоения, так и в примере с моржовым оператором, абсолютно одинакова. Это означает, что в обоих примерах эти переменные доступны и после определения словаря statistical_parameters.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# file_statistics.py

import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    counts = (
        path.read_text().count("\n"),   # Число строк
        len(path.read_text().split()),  # Число слов
        len(path.read_text()),          # Число символов
    )
    print(*counts, path)

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

  • Строка 6 перебирает в массиве sys.argv каждое имя файла, которое может указываться пользователями в командной строке для нашей утилиты file_statistics. Этот массив представляет собой список аргументов, предоставленных пользователями в командной строке для соответствующей утилиты, включая и имя этой утилиты, которое всегда указывается в качестве первого (нулевого) элемента массива sys.argv. Таким образом, у нас в строке 6 перебираются все возможные аргументы (имена файлов) в командной строке за исключением имени самой нашей утилиты.
  • В строке 7 каждое передаваемое в командной строке для нашей утилиты имя текстового файла преобразуется в pathlib.Path объект, что обеспечивает возможность для удобного чтения таких файлов в последующих строках кода нашей утилиты.
  • Строки с 8 по 12 создают кортеж счетчиков для представления количества строк, слов и символов для одного текстового файла.
  • Строка 9 читает текстовый файл и вычисляет количество строк в нем путем подсчета символов перевода каретки "\n".
  • Строка 10 читает файл и вычисляет количество слов в нем за счет разбиения текста в этом файле по пробелам.
  • Строка 11 читает текстовый файл и вычисляет количество символов в нем находя длину строки с содержанием всего этого файла.
  • Строка 13 выводит на консоль значения всех трех счетчиков, наряду с именем анализируемого файла. Синтаксис кортежа - *counts, указываемый в этой строке при распечатке, на самом деле необходим для распаковки данного кортежа в виде эквивалентном print(count[0], counts[1], counts[2], path).

Давайте попробуем нашу утилиту в действии запустив ее для определения параметров файла, содержащим ее же Python код. При этом, предположим, что сам файл file_statistics.py размещен на диске D: нашего компьютера.

$ python d:\file_statistics.py d:\file_statistics.py
13 30 353 d:\file_statistics.py

Таким образом мы выяснили, что файл file_statistics.py состоит из 13 строк, 30 слов и 353 символов.

Если мы внимательно проанализируем программный код нашей утилиты, то заметим, что он далек от идеала. В частности, вызов path.read_text() у нас повторяется аж три раза, и именно столько же раз нам требуется читать каждый текстовой файл, записываемый в командной строке нашей утилиты в качестве аргументов. Но, давайте посмотрим, как избежать этих повторений используя моржовый оператор:

# file_statistics.py

import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    counts = [
        (text := path.read_text()).count("\n"),  # Число строк
        len(text.split()),  # Число слов
        len(text),  # Число символов
    ]
    print(*counts, path)

В этом примере содержимое файла из командной строки сначала присваивается переменной text, а уже потом значение этой переменной повторно используется в следующих двух вычислениях. Утилита по-прежнему у нас работает так же:

$ python d:\file_statistics.py d:\file_statistics.py
13 33 336 d:\file_statistics.py

Но наглядность (читабельность) кода из предыдущего примера весьма и весьма пострадала. Поэтому, в данном случае, лучшим вариантом будет остановиться на старом добром операторе присваивания и определить с помощью него переменную text в отдельной строке перед определением кортежа counts:

# file_statistics.py

import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    text = path.read_text()
    counts = [
        text.count("\n"),  # Число строк
        len(text.split()),  # Число слов
        len(text),  # Число символов
    ]
    print(*counts, path)

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

Генераторы списков

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

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

sampling = [7, 6, 1, 4, 1, 8, 0, 6]

results = [braked(element) for element in sampling if braked(element) > 0]

В этом примере, полученный от функции braked() результат обработки каждого элемента выборки sampling мы сначала фильтруем, оставляя лишь положительные результаты, а затем из этих положительных результатов формируем новый список results. Проблема с данным кодом состоит в том, что в нем наша ресурсоемкая медлительна функция braked() вызывается дважды.

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

results = []
for element in sampling:
    value = braked(element)
    if value > 0:
        results.append(value)

Действительно, благодаря такому подходу функция braked() вызывается лишь один раз. Но теперь, к сожалению, код у нас разросся и стал менее понятным. Ведь конструкция генератора списка в предыдущем примере ясно сигнализировала нам о создании нового списка, в то время как сейчас процесс формирования списка замаскирован внутри явного цикла for, где данный процесс распылен на несколько строк кода внутри тела данного цикла. Кроме того, сама конструкция генератора списка выполняется быстрее, чем повторные вызовы функции .append() внутри тела цикла.

Существуют и другие варианты решения вышеописанной проблемы. Так, например, мы можем попытаться оптимизировать наш код используя функцию filter() вместе с lambda оператором или же сконструировать генератор списков на основе применения так называемого двойного списка:

# Использование фильтра совместно с lambda
results = filter(lambda value: value > 0, (braked(element)
                                           for element in sampling))

# Использование генератора двойного списка
results = [value for element in sampling for value in
           [braked(element)] if value > 0]

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

Ведь согласитесь, что для того, чтобы вникнуть в смысл кода особенно для генератора двойного списка требуется немало усилий. Кроме этого, довольно большой кусок кода в этом примере со вторым for оператором используется только лишь для инициализации переменной value значением, возвращаемым braked(element). Удивительно, но применяя к этому сверхсложному и запутанному выражению генератора двойного списка наш моржовый оператор, мы можем кардинально упростить имеющейся у нас код.

Давайте же попробуем это сделать и, перепишем приведенный выше генератор двойного списка применив в нем оператор присваивания внутри выражения:

results = [value for element in sampling if (value := braked(element)) > 0]

В этом примере следует лишь обратить внимание, что круглые скобки вокруг выражения value := braked(element) являются обязательными. В тоже время, вполне очевидно то, что код последнего примера несомненно эффективен, удобочитаем и нагляден в восприятии своего назначения.

Примечание. При формировании генераторов списка с использованием моржового оператора, Вам следует добавлять данный оператор только лишь в подвыражение if. Иначе, если вы попытаетесь с помощью (:=) инициализировать переменную value вызовом функции braked() где ни будь в другом месте конструкции генератора списка, то это не сработает:

>>> results = [(value := braked(element)) for element in sampling if value > 0]
NameError: name 'value' is not defined

Подобное, неправильное место использования моржового оператора вызывает исключение NameError, поскольку записанное в конце подвыражение для оператора if в генераторе списка всегда рассчитывается первым.

While циклы

В Python может использоваться, как while цикл, так и цикл for. Как правило, for используется в случаях, когда нам либо известно число итераций в цикле, либо когда нам нужно перебрать последовательность с четко ограниченным числом элементов. Цикл же while используется тогда, когда мы заранее не знаем, сколько раз нам понадобится выполнять данный цикл.

С целью исключения неоднозначных ситуаций, в отличии от других языков программирования, условия выполнения while циклов в Python определяются лишь в верхней их части. Это иногда приводит к определенным неудобствам в процессе кодинга, когда нам приходится выполнять некоторые настройки перед выполнением соответствующих проверок. Для примера рассмотрим фрагмент программы, реализующей проведение викторины с несколькими вариантами ответов, часть из которых является правильными:

vopros = "Будете ли вы использовать моржовый оператор?"
korrektnye_otvety = {"да", "Да", "y", "Y", "нет", "Нет", "n", "N"}

user_otvet = input(f"\n{vopros} ")
while user_otvet not in korrektnye_otvety:
    print(f"Пожалуйста, ответьте путем ввода одного из следующих значений:"
          f" {', '.join(korrektnye_otvety)}")
    user_otvet = input(f"\n{vopros} ")

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

Для упрощения вышеприведённого кода можно прибегнуть к довольно популярному использованию while True цикла, когда проверка условия выполняемости цикла происходит не в его начале, а позже где-то в теле цикла совместно с применением оператора break:

while True:
    user_otvet = input(f"\n{vopros} ")
    if user_otvet in korrektnye_otvety:
        break
    print(f"Пожалуйста, ответьте путем ввода одного из следующих значений:"
          f" {', '.join(korrektnye_otvety)}")

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

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

while (user_otvet := input(f"\n{vopros} ")) not in korrektnye_otvety:
    print(f"Пожалуйста, ответьте путем ввода одного из следующих значений:"
          f" {', '.join(korrektnye_otvety)}")

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

Репрезентативные и отверженные элементы в структурах данных

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

Суть этого варианта заключается в хитром приеме совместного использования оператора (:=) с функцией any(), при котором в таких структурах данных, как списки, кортежи или словари нам предоставляется возможность для поиска их репрезентативных элементов. Репрезентативные элементы, в данном контексте – это элементы той или иной структуры данных, которые после прохождения проверки через функцию any() возвращают True.

Аналогичным образом можно использовать моржовый оператор и для поиска отверженных элементов в структурах данных через функцию all(). Отверженные элементы в данном контексте – это элементы, которые не проходят проверку на то или иное условие, вследствие чего функция all() возвращает False.

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

>>> cities = ["Ванкувер", "Осло", "Хьюстон", "Варшава", "Прага", "Гамбург"]

Применим к определенному нами выше списку городов функции any() и all() для ответа на ряд следующих вопросов:

>>> # Есть ли в списке города, название которых начинается с "О"?
>>> result1 = any(city.startswith("О") for city in cities)
>>> print(result1)
True

>>> # Есть ли в списке города, названия которых состоят из не менее чем 10 символов?
>>> result2 = any(len(city) >= 10 for city in cities)
>>> print(result2)
False

>>> # Все ли названия городов содержат "а" или "о"?
>>> result3 = all(set(city) & set("ао") for city in cities)
>>> print(result3)
True

>>> # Все ли названия городов начинаются с "В"?
>>> result4 = all(city.startswith("В") for city in cities)
>>> print(result4)
False

Для каждого из четырех вышеприведенных вопросов функции any() или all() дают нам однозначные ответы: True или False. Но, что, если нам необходимо увидеть, какой именно город по отношению к тому или иному вопросу влечет за собой возвращения True или False после вызова соответствующей функции. Другими словами, на наши вопросы нам нужно получит четкие ответы следующим образом:

  • Есть ли в списке города, названия которых начинаются с "О"? Да, потому что "Осло" начинается с "О".
  • Все ли названия городов начинаются с "В"? Нет, потому что "Прага" не начинается с "В".

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

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

>>> representative = [city for city in cities if city.startswith("О")]

>>> if representative:
...     print(f"{representative[0]} начинается с «О»")
... else:
...     print("Города, начинающиеся с «О» в данном списке отсутствуют")
...
Осло начинается с «О»

В этом примере мы сначала фиксируем все названия городов, начинающиеся с «О», а затем, если такие названия у нас все-таки есть, мы выводим на консоль первый, найденный нами город, начинающийся с искомой нами буквы. По сути здесь дедовскими методами мы выполняем работу, аналогичную работе функции any().

Теперь, попробуем переписать предыдущий код с использованием функции any() и моржового оператора (:=) следующим образом:

>>> if any((representative := city).startswith("О") for city in cities):
        print(f"{representative} начинается с «О»")
... else:
...     print("Города, начинающиеся с «О» в данном списке отсутствуют")
...
Осло начинается с «О»

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

Примечание. В случае, если вы хотите проверить, все ли названия городов начинаются с буквы «О», лучше всего, основываясь на поиске отверженных элементов, заменить any() на all() и обновить функцию print() таким образом, чтобы с ее помощью сообщать о первом элементе, который не прошел проверку.

Давайте более подробно рассмотрим что же все-таки происходит внутри функции any(), использовав при ее вызове дополнительно определенную нами пользовательскую функцию starts_with_h(), которая будет распечатывать все элементы, проверяемые any() до прекращения ее работы. При этом давайте в any() через нашу пользовательскую функцию и выражение .startswith('Х') зададим условие для поиска тех городов, названия которых начинаются на букву «Х».

>>> def starts_with_h(name):
...     print(f"Проверка - {name}: {name.startswith('Х')}")
...     return name.startswith("Х")

>>> result1 = any(starts_with_h(city) for city in cities)
>>> print(result1)
Проверка - Ванкувер: False
Проверка - Осло: False
Проверка - Хьюстон: True
True

Из результатов этого примера видно, что any() проверяет элементы из списка cities только до тех пор, пока не найдет тот, который удовлетворял бы условию. В данном случае из шести городов, находящихся в нашем списке any() остановила свою работу всего лишь на третьем городе по порядку, так как название именно этого города начиналось на «Х». Другими словами, комбинация оператора (:=) и функции any() работает посредством итеративного присвоения каждого проверяемого элемента списка переменной representative. Однако, в этой переменной сохраняется только тот элемент списка, который последним проверялся any() и, является либо элементом, удовлетворяющим заданному условию, либо последним элементом в списке.

Таким образом, если даже any() возвратить False, то переменная representative все равно будет инициализирована:

>>> result1 = any(len(representative := city) >= 10 for city in cities)
>>> print(result1, representative)
False Гамбург

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

Особенности синтаксиса моржового оператора

Прежде чем разобраться в закономерностях синтаксиса оператора (:=), нам следует запомнить одно, уже упоминавшееся раннее и весьма простое правило, заключающееся в том, что моржовый оператор (:=) и обычный оператор присвоения (=) никогда не могут быть использованы для прямой взаимозамены друг на друга. Именно это правило в Python предотвращает достаточно большое количество ошибок с моржовым оператором, которые характерны для других языков программирования в отношении использования там аналогичных операторов.

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

>>> lat = lon := 0
SyntaxError: invalid syntax

>>> angle(phi = lat := 59.9)
SyntaxError: invalid syntax

>>> def distance(phi = lat := 0, lam = lon := 0):
SyntaxError: invalid syntax

Во всех этих примерах для предотвращения ошибок вместо моржового оператора нам лучше было бы использовать обычное (=). Вместе с тем, можно избежать ошибок и для вышеприведенных примеров, переписав их следующим образом:

# Приемлемые, но не рекомендуемые варианты совместного использования (:=) и (=)

>>> lat = (lon := 0)

>>> angle(phi = (lat := 59.9))

>>> def distance(phi = (lat := 0), lam = (lon := 0)):
...     pass
...

Однако, применение моржового оператора во всех трех вышеприведенных примерах отнюдь не сокращает и не улучшает их код.

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

  • Присваивание значений для элементов и атрибутов: оператор (:=) допускает присваивание значений только для простых (цельных) переменных без использования индексов и точек:
>>> (smaylik["serdtse"] := "♥")
SyntaxError: cannot use assignment expressions with subscript

>>> (nomer.otveta := 42)
SyntaxError: cannot use assignment expressions with attribute

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

  • Итерируемая распаковка: моржовый оператор не может использоваться при распаковки любых последовательностей данных типа кортежей, списков или наборов:
>>> lat, lon := 59.9, 10.8
SyntaxError: invalid syntax

При этом, если мы добавим круглые скобки вокруг всего нашего выражения выше, то оно будет интерпретировано как кортеж, содержащий три следующие элементы: lat, 59.9 и 10.8.

  • Расширенное присваивание: оператор (:=) не может быть использован в сочетании с операторами расширенного присваивания типа (+=). Такое сочетание приводит к SyntaxError:
>>> count +:= 1
SyntaxError: invalid syntax

Наиболее простым выходом из этой ситуации может быть оформления приведенного выше выражения в форму явного увеличения типа (count := count + 1).

В большинстве случаев моржовый оператор будет вести себя аналогично нашему традиционному оператору присваивания. Так, например, области действия переменных, назначаемых как благодаря использованию (:=), так и за счет применения обычного присваивания, являются абсолютно одинаковыми. Как правило, (:=) используется для назначения локальных переменных, однако, если переменная предварительно была объявлена как global или как nonlocal, то это также учитывается в моржовом операторе.

Связь между элементами выражения у моржового оператора гораздо менее тесная (за исключение запятой), чем у других операторов. Поэтому, в соответствующих выражениях нам может потребоваться применение круглых скобок для обобщения необходимых участков выражения. Рассмотрим, что произойдет в случае, когда мы можем «полениться» использовать круглые скобки:

>>> number = 3
>>> if square := number ** 2 > 5:
...     print(square)
...
True

Здесь переменная square связана со всем выражением number ** 2 > 5. Другими словами, square вместо того, чтобы получить значение number ** 2 (как и было изначально задумано), реально получает значение True. Для устранения данной проблемы мы можем разделить соответствующее выражение скобками следующим образом:

>>> number = 3
>>> if (square := number ** 2) > 5:
...     print(square)
...
9

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

И, наконец, последней проблемой, с которой мы можем столкнуться применяя (:=), является присваивание содержимого кортежей, при котором нам всегда нужно использовать круглые скобки вокруг этих кортежей. Рассмотрим следующие примеры:

>>> morzh = 3.7, False
>>> morzh
(3.7, False)

>>> (morzh := 3.8, True)
(3.8, True)
>>> morzh
3.8

>>> (morzh := (3.8, True))
(3.8, True)
>>> morzh
(3.8, True)

Обратите внимание, что во втором примере переменная morzh принимает значение лишь первого элемента 3.8, а не всего содержимого кортежа в целом. Это происходит потому, что оператор (:=) связывает элементы в выражении более сильно, нежели запятая. Данная закономерность на первый взгляд может показаться неоправданной, однако, если бы запятая имела бы перед моржовым оператором более высокий приоритет, то (:=) невозможно было бы использовать при вызовах функций с более чем одним аргументом.

Рекомендации по стилю для моржового оператора в основном являются такими же, как и для обычного оператора присваивания (=). Здесь, во-первых, всегда в коде следует добавлять пробелы вокруг (:=), а во-вторых, круглые скобки вокруг выражений нужно использовать лишь по мере необходимости, т.е. необходимо всегда избегать добавления лишних (ненужных) круглых скобок.

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

Выводы

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

Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

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

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09