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
.
Злоупотребление оператором :=
может привести к ошибкам и ухудшению читабельности кода, поэтому не следует использовать его при любом удобном случае, а только тогда, когда это действительно необходимо.
Подведем итоги
Как мы видим, в некоторых ситуациях с помощью моржового оператора можно написать лаконичный и более производительный код с точки зрения вычислений. Однако использовать моржовый оператор стоит аккуратно. Не следует внедрять его в код при каждом удобном случае. Применяйте оператор :=
только для несложных выражений, чтобы ваш код не терял читабельность.
Присоединяйтесь к нашему телеграм-каналу, будет интересно и познавательно!
❤️ Happy Pythoning! 🐍
Автор: Тимур Гуев