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! ?
Комментарии (31)
gmist
31.07.2024 07:08+23На практике не встречал вариантов, когда моржик дает что-то сильно полезнее, чем экономия пары строк кода, а взамен он дает куда больше - замедление чтения этого самого кода.
greenfork
31.07.2024 07:08+1Почему у вас наступает эффект замедления чтения кода при виде моржового оператора?
Romano
31.07.2024 07:08Это очевидно даже из примеров в статье - усложнение кода одной строки.
greenfork
31.07.2024 07:08+1Для меня это не очевидно. Если вы говорите про усложнение, то здесь два противодействующих фактора:
Усложнение кода одной строки
Выражение мысли в двух строках
Для меня совершенно не очевидно, что прочитать две "простые" строки будет быстрее, чем одну "сложную".
Dolby
31.07.2024 07:08Бывает накрывает желание в перфекционизм удариться, сделать код более красивым, эффективным и лаконичным, вот тут морж иногда бывает уместен
Fen1kz
31.07.2024 07:08+8Интересно, в js такое поведение изначально с
=
и это считается очень плохим стилем, практически нигде кроме "хакерских однострочников" не вижуalienator
31.07.2024 07:08+2Это не только в JS, это во многих языках, потому что унаследовано от C, в котором assignment is expression с самого начала. Но и C не был первым, в Algol 68 оно точно было, и даже в моржовом виде. Возможно, было в каких-то расширениях Fortran, не уверен. С большой натяжкой можно и Lisp упомянуть.
ahabreader
31.07.2024 07:08+1Можно отметить две вещи в C: возможность "опасного" присваивания внутри if (плохо) и возможность объявления, в нём нет места для ошибки (хорошо):
if (x = foo()) {...} // точно ли нет ошибки? '=' или '=='? if (int x = foo()) {...} // точно нет
Из этих двух вещей в одних языках унаследовали только хорошую (D), в других - только плохую (JS).
ahabreader
31.07.2024 07:08и возможность объявления
Тьфу, это же только в C++ так. "A condition can either be an expression or a simple declaration."
https://en.cppreference.com/w/cpp/language/if
https://en.cppreference.com/w/c/language/if
funca
31.07.2024 07:08+1В условиях часто ошибаются, когда пишут = (присваивание) вместо == (сравнение):
if (x = 1) { /* всегда TRUE */}
if (x == 1) {}
Поэтому Гвидо в питоне с самого начала запретил использовать присваивание в подобных случаях.
Моржовый оператор содержит дополнительный символ := из-за чего вероятность допустить такую ошибку существенно меньше. Но в питоне он не вписывается в общий дизайн языка и выглядит как костыль.
dyadyaSerezha
31.07.2024 07:08+1Эти ошибочные условия ловятся всеми компиляторами уже лет 20 как.
Dozer88
31.07.2024 07:08Не все компиляторы, и не всегда ошибки. Например, на сях нет исключений, и на каждой строчке писать if ((res = f()) != ok) return res; намного лаконичнее, чем res = f(); if (res != ok) return res; в две строчки... Хотя я тоже не люблю такие конструкции...
dyadyaSerezha
31.07.2024 07:08На сях как раз и начали их ловить раньше всех. Выводится warning, а дальше уже сам.
sl_1me
31.07.2024 07:08+1Хоть и оператор не нашёл большого применения, но читать статьи о нём и его истории очень интересно!
AlexeyK77
31.07.2024 07:08Нужно быть готовым в скором будущем к такому коду
num := 7
print(num)
Потому что так тоже можно написать и это станет источником путаницы. Либо моржа надо было как-то ограничить в применении, что бы он не вытеснил обычное =, либо вообще не вводить, т.к. появляется целый лишний оператор ради некритических сценариев.
ValeryIvanov
31.07.2024 07:08+1А так и нельзя писать
>>> a := 123 File "<stdin>", line 1 a := 123 ^^ SyntaxError: invalid syntax
AlexeyK77
31.07.2024 07:08+7отлично! Получается что ввели целый оператор ради одного краевого случая. как по мне не лучшая это практика, плодить операторы.
funca
31.07.2024 07:08+3y := f(x) # INVALID
(y := f(x)) # Valid, though not recommended
По идее, нужно было делать с самого начала как в первом варианте. Но поезд ушел.
Самая жесть теперь когда скобки меняют семантику:
>>> f'{(x:=10)}' # Valid, uses assignment expression
'10'
>>> x = 10
>>> f'{x:=10}' # Valid, passes '=10' to formatter
' 10'
После такого Гвидо правильно сделал, что самовыпилился. Это уже маразм.
ahabreader
31.07.2024 07:08По идее, нужно было делать с самого начала как в первом варианте
Так не делают со времён Алгола-68, наверное. Потому что
-:=
,+:=
,*:=
,/:=
,%:=
...Но была и другая возможность - переиспользовать
as
.
AlexeyK77
31.07.2024 07:08зато появился отличный вопрос валить джунов (а может и не только джунов) на собеседовании простым вопросом по операторам языка ;)
icya
31.07.2024 07:08+6>>> with open("./words.txt", "r") as file: ... while line := file.readline().rstrip(): ... print(line) ... foo bar >>> with open("./words.txt", "r") as file: ... for line in file.readlines(): ... print(line.rstrip()) ... foo bar baz
В примере 2 (чтение строк из файла), реализация через моржовый оператор завершает чтение на первой пустой строке
HyperWin
31.07.2024 07:08+1Я использовал его разок, очень удобно в цикле, чтобы работал пока значение из функции не равно None
Megadeth77
31.07.2024 07:08+1Впилили бы что то вроде using во включения, а то извращаться приходится
a = [foo: (bar,baz) for foo in arr if (bar:="qq", baz:="ww")]
LF69ssop
31.07.2024 07:08+5Какие-то надуманные проблемы. Как по мне, лучше десяток лишних строчек чем дублирующий оператор языка.
Синтаксис языка должен быть логичным, простым, понятным и по возможности без вариантов и исключений.
Dynasaur
31.07.2024 07:08Если под каждую алгоритмическую ситуацию придумывать новый оператор, то питон превратится в китайскую грамоту с тысячами иероглифов. Китайская грамота, кстати, очень лаконична, в одном иероглифе помещается несколько слов. Только учить и читать её замучаешься.
johngetman
31.07.2024 07:08офигенная вещь, я конечно не очень люблю пайтон но конкретно моржовый оператор это оч годная штука, всем кому она не нравится советую пересесть на луа, там вообще ничего лишнего нет
HemulGM
Когда ожидается введение begin/end?
abramov_a
Скорее всего никогда))) Отступы - наше все.
dyadyaSerezha
Велик Питон, а отступать некуда! (с) Ну или почти так)
RichardMerlock
Вправо! Вправо отступайте!