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

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

PEP 572

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

В 2018 году Гвидо объявил об уходе с этой позиции. Причиной такого решения стал документ PEP 572, который вводит в язык выражения присваивания — моржовый оператор. Этот документ вызвал ожесточенные споры в сообществе Python. Многие программисты считали, что идеи, представленные в нем, противоречат философии языка и отражают, скорее, личное мнение Гвидо ван Россума, чем передовые практики в отрасли. Например, некоторым разработчикам не нравился сложный и неочевидный синтаксис оператора :=. Однако Гвидо все равно утвердил PEP 572, и в Python 3.8 появился моржовый оператор.

После публикации документа пользователи Python писали Гвидо ван Россуму множество негативных отзывов. Он был ошеломлен количеством комментариев, которые получил в ответ на принятие PEP 572. В конце концов Гвидо решил покинуть свой пост. Он отправил своим коллегам письмо, в котором написал: "Я больше не хочу когда-либо сражаться за PEP и видеть, как множество людей презирают мои решения". Полный текст письма Гвидо доступен по ссылке.

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

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

Оператор :=

Итак, моржовый оператор появился в Python 3.8 и дает возможность решить сразу две задачи (выполнить два действия):

  1. присвоить значение переменной

  2. вернуть это значение

Базовый синтаксис использования оператора := следующий:

variable := expression

Сначала выполняется выражение expression, а затем знач��ние, полученное в результате выполнения этого выражения, присваивается переменной variable, после чего это значение будет возвращено.

Кстати, оператор := часто называют моржовым, потому что он похож на глаза и бивни моржа.

Отличие оператора := от оператора =

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

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

num = 7
print(num)

Однако при использовании оператора := данный код можно сократить до одной строчки:

print(num := 7)

Значение 7 присваивается переменной num, а затем возвращается и становится аргументом для функции print().

Если мы попытаемся сделать то же самое с помощью обычного оператора присваивания, то получим ошибку типа, поскольку num = 7 ничего не возвращает и воспринимается как именованный аргумент num, которого у функции print() нет.

Приведенный ниже код:

print(num = 7)

приводит к возбуждению исключения:

TypeError: 'num' is an invalid keyword argument for print()

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

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

Пример 1. Необходимо вывести информацию о ключевых словах Python, длина которых больше пяти символов.

Приведенный ниже код:

from keyword import kwlist

for word in kwlist:
    if len(word) > 5:
        print(f'В ключевом слове {word} всего {len(word)} символов.')

выводит:

В ключевом слове assert всего 6 символов.
В ключевом слове continue всего 8 символов.
В ключевом слове except всего 6 символов.
В ключевом слове finally всего 7 символов.
В ключевом слове global всего 6 символов.
В ключевом слове import всего 6 символов.
В ключевом слове lambda всего 6 символов.
В ключевом слове nonlocal всего 8 символов.
В ключевом слове return всего 6 символов.

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

from keyword import kwlist

for word in kwlist:
    n = len(word)
    if n > 5:
        print(f'В ключевом слове {word} всего {n} символов.')

или с помощью оператора :=:

from keyword import kwlist

for word in kwlist:
    if (n := len(word)) > 5:
        print(f'В ключевом слове {word} всего {n} символов.')

Обратите внимание на то, что в данном случае выражение (n := len(word)) нужно обязательно заключать в скобки.

Приведенный ниже код:

from keyword import kwlist

for word in kwlist:
    if n := len(word) > 5:
        print(f'В ключевом слове {word} всего {n} символов.')

выводит:

В ключевом слове assert всего True символов.
В ключевом слове continue всего True символов.
В ключевом слове except всего True символов.
В ключевом слове finally всего True символов.
В ключевом слове global всего True символов.
В ключевом слове import всего True символов.
В ключевом слове lambda всего True символов.
В ключевом слове nonlocal всего True символов.
В ключевом слове return всего True символов.

Оператор :=, как и оператор =, имеет наименьший приоритет перед всеми остальными встроенными операторами, поэтому выражение n := len(word) > 5 равнозначно n := (len(word) > 5), что в контексте истинного условия равнозначно n := True.

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

words = []
word = input()

while word != 'stop':
    words.append(word)
    print(f'Значение {word!r} добавлено в список.')
    word = input()

Проблема этого кода заключается в том, что нам приходится объявлять переменную word перед циклом для первой итерации, тем самым дублируя строку кода word = input().

С использованием оператора := приведенный выше код можно записать в виде:

words = []
while (word := input()) != 'stop':
    words.append(word)
    print(f'Значение {word!r} добавлено в список.')

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

Приведенный ниже код:

with open('input.txt', 'r') as file:
    line = file.readline().rstrip()
    while line:
        print(line)
        line = file.readline().rstrip()

с использованием оператора := можно записать в виде:

with open('input.txt', 'r') as file:
    while line := file.readline().rstrip():
        print(line)

Пример 3. Нам д��ступен список чисел, на основе которого необходимо создать новый список, элементами которого будут факториалы чисел исходного списка, при этом только те, которые не превышают 1000.

Приведенный ниже код:

from math import factorial

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
new_data = [factorial(x) for x in data if factorial(x) <= 1000]
print(new_data)

выводит:

[1, 2, 6, 24, 120, 720]

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

Приведенный ниже код:

from math import factorial

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
new_data = [fact for num in data if (fact := factorial(num)) <= 1000]
print(new_data)

выводит:

[1, 2, 6, 24, 120, 720]

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

Приведенный ниже код:

users = [
{'name': 'Timur Guev', 'occupation': 'python generation guru'},
{'name': None, 'occupation': 'driver'},
{'name': 'Anastasiya Korotkova', 'occupation': 'python generation bee'},
{'name': None, 'occupation': 'driver'},
{'name': 'Valeriy Svetkin', 'occupation': 'python generation bee'}
]

for user in users:
    name = user.get('name')
    if name is not None:
        print(f'{name} is a {user.get("occupation")}.') 

выводит:

Timur Guev is a python generation guru.
Anastasiya Korotkova is a python generation bee.
Valeriy Svetkin is a python generation bee.

В этом коде мы проходим по списку словарей users, извлекаем значение ключа name для каждого словаря и проверяем, не является ли это значение None, после чего выводим информацию о пользователе.

С использованием оператора := приведенный выше код можно записать в виде:

users = [
{'name': 'Timur Guev', 'occupation': 'python generation guru'},
{'name': None, 'occupation': 'driver'},
{'name': 'Anastasiya Korotkova', 'occupation': 'python generation bee'},
{'name': None, 'occupation': 'driver'},
{'name': 'Valeriy Svetkin', 'occupation': 'python generation bee'}
]

for user in users:
    if (name := user.get('name')) is not None:
        print(f'{name} is a {user.get("occupation")}')

Здесь мы используем оператор := для присваивания значения переменной name внутри условного оператора, что позволяет сократить количество строк кода и сделать его более читабельным.

Пример 5. Нам доступна произвольная строка, в которой необходимо найти совпадение с определенным шаблоном. Если совпадение не найдено, необходимо найти совпадение со вторым шаблоном. Если совпадение снова не найдено, необходимо вывести текст Нет совпадений.

Приведенный ниже код:

import re

text = 'Поколение Python — это серия курсов по языку программирования Python от команды BEEGEEK'
pattern1 = r'beegeek'
pattern2 = r'Python'

m = re.search(pattern1, text)
if m:
    print(f'Найдено совпадение с первым шаблоном: {m.group()}')
else:
    m = re.search(pattern2, text)
    if m:
        print(f'Найдено совпадение со вторым шаблоном: {m.group()}')
    else:
        print('Нет совпадений')

выводит:

Найдено совпадение со вторым шаблоном: Python

С использованием оператора := приведенный выше код можно записать в виде:

import re

text = 'Поколение Python — это серия курсов по языку программирования Python от команды BEEGEEK'
pattern1 = r'beegeek'
pattern2 = r'Python'

if m := re.search(pattern1, text):
    print(f'Найдено совпадение с первым шаблоном: {m.group()}')
else:
    if m := re.search(pattern2, text):
        print(f'Найдено совпадение со вторым шаблоном: {m.group()}')
    else:
        print('Нет совпадений')

Пример 6. Нам доступен список чисел. Необходимо решить две задачи:

  • выяснить, является ли хотя бы одно число из списка больше числа 10

  • выяснить, являются ли все числа из списка меньше числа 10

Приведенный ниже код:

numbers = [1, 4, 6, 2, 12, 4, 15]

print(any(number > 10 for number in numbers))
print(all(number < 10 for number in numbers))

выводит:

True
False

Оператор := в этом случае позволит сохранить значение, на котором закончилось выполнение функций any() и all().

Приведенный ниже код:

numbers = [1, 4, 6, 2, 12, 4, 15]

print(any((value := number) > 10 for number in numbers))
print(value)

print(all((value := number) < 10 for number in numbers))
print(value)

выводит:

True
12
False
12

Подводные камни

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

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

Приведенный ниже код:

from math import factorial

fact = 0
print(fact)

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
factorial_data = [fact for num in data if (fact := factorial(num)) <= 1000]
print(fact)

выводит:

0
3628800

В шестом примере показана возможность использования оператора := в функциях all() и any(). Но если список окажется пуст, переменная не будет создана, что приведет к возбуждению исключения NameError.

Приведенный ниже код:

numbers = []

print(any((value := number) > 10 for number in numbers))
print(value)

приводит к возбуждению исключения:

NameError: name 'value' is not defined. Did you mean: 'False'?

С похожей ситуацией можно столкнуться при проверке нескольких условий. Например, мы хотим узнать, какие числа в диапазоне от 1 до 100 делятся на 2, 3 или 6 без остатка.

Приведенный ниже код:

for i in range(1, 101):
    if (two := i % 2 == 0) and (three := i % 3 == 0):
        print(f"{i} делится на 6.")
    elif two:
        print(f"{i} делится на 2.")
    elif three:
        print(f"{i} делится на 3.")

приводит к возбуждению исключения:

NameError: name 'three' is not defined

Проблемой этого кода является то, что если выражение (two := i % 2 == 0) является ложным, выражение (three := i % 3 == 0) не выполнится и переменная three не будет создана, в результате чего будет возбуждено исключение NameError.

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

Подведем итоги

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

Присоединяйтесь к нашему телеграм-каналу, будет интересно и познавательно!

❤️ Happy Pythoning! 🐍