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! 🐍