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 и дает возможность решить сразу две задачи (выполнить два действия):
- присвоить значение переменной
- вернуть это значение
Базовый синтаксис использования оператора := следующий:
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.
Злоупотребление оператором := может привести к ошибкам и ухудшению читабельности кода, поэтому не следует использовать его при любом удобном случае, а только тогда, когда это действительно необходимо.
Подведем итоги
Как мы видим, в некоторых ситуациях с помощью моржового оператора можно написать лаконичный и более производительный код с точки зрения вычислений. Однако использовать моржовый оператор стоит аккуратно. Не следует внедрять его в код при каждом удобном случае. Применяйте оператор := только для несложных выражений, чтобы ваш код не терял читабельность.