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! ?
Комментарии (46)
gmist
31.07.2024 07:08+33На практике не встречал вариантов, когда моржик дает что-то сильно полезнее, чем экономия пары строк кода, а взамен он дает куда больше - замедление чтения этого самого кода.
gerod
31.07.2024 07:08+4вот я тоже не понял, а зачем? только ради сокращения пары строк кода?
TIEugene
31.07.2024 07:08# сферический case в вакууме import libvirt if (conn := libvirt.open()) and (dom := conn.lookupByName('myhost')) and dom.isActive(): dom.poweroff()
Ну классно же ж
Classic way:
```pyconn = libvirt.open():
if conn:
dom = conn.lookupByName('myhost'):
if dom:
active = dom.isActive()
if active:dom.poweroff()
```
No comments
PS. дурацкий редактор в хабре, ппц
greenfork
31.07.2024 07:08+1Почему у вас наступает эффект замедления чтения кода при виде моржового оператора?
Romano
31.07.2024 07:08Это очевидно даже из примеров в статье - усложнение кода одной строки.
greenfork
31.07.2024 07:08+2Для меня это не очевидно. Если вы говорите про усложнение, то здесь два противодействующих фактора:
Усложнение кода одной строки
Выражение мысли в двух строках
Для меня совершенно не очевидно, что прочитать две "простые" строки будет быстрее, чем одну "сложную".
boojum
31.07.2024 07:08+1Дзен питона: п.6 Разреженное лучше, чем плотное.
Две строки лучше чем одна. (хотя порой и хочется запихать в одну побольше :)
greenfork
31.07.2024 07:08Это правда, что в Питоне более идиоматично будет написать в две строки, а не в одну. Это не является объективным добром, но в Питоне так принято. Что интересно, мне кажется, что на скорость чтения это может не влиять, если привыкнуть к моржовому оператору. Если привычно, то не будет ступора от непонятной конструкции.
TIEugene
31.07.2024 07:08Есть один нюанс.
Модуль должен быть читабелен. Из моей практики 200..250 строк оптимально. И это с учетом 2 строки между классами/функциями, 1 строка между методами, докстринги и прочие PEPs. Больше 500 строк модуль становится неуправляемым.
А впихнуть надо. Ибо класс.Поэтому таки да таки не - не всегда разреженное лучше.
Dolby
31.07.2024 07:08Бывает накрывает желание в перфекционизм удариться, сделать код более красивым, эффективным и лаконичным, вот тут морж иногда бывает уместен
Fen1kz
31.07.2024 07:08+10Интересно, в js такое поведение изначально с
=
и это считается очень плохим стилем, практически нигде кроме "хакерских однострочников" не вижуalienator
31.07.2024 07:08+3Это не только в JS, это во многих языках, потому что унаследовано от C, в котором assignment is expression с самого начала. Но и C не был первым, в Algol 68 оно точно было, и даже в моржовом виде. Возможно, было в каких-то расширениях Fortran, не уверен. С большой натяжкой можно и Lisp упомянуть.
ahabreader
31.07.2024 07:08+4Можно отметить две вещи в 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+4В условиях часто ошибаются, когда пишут = (присваивание) вместо == (сравнение):
if (x = 1) { /* всегда TRUE */}
if (x == 1) {}
Поэтому Гвидо в питоне с самого начала запретил использовать присваивание в подобных случаях.
Моржовый оператор содержит дополнительный символ := из-за чего вероятность допустить такую ошибку существенно меньше. Но в питоне он не вписывается в общий дизайн языка и выглядит как костыль.
dyadyaSerezha
31.07.2024 07:08+1Эти ошибочные условия ловятся всеми компиляторами уже лет 20 как.
Dozer88
31.07.2024 07:08+1Не все компиляторы, и не всегда ошибки. Например, на сях нет исключений, и на каждой строчке писать if ((res = f()) != ok) return res; намного лаконичнее, чем res = f(); if (res != ok) return res; в две строчки... Хотя я тоже не люблю такие конструкции...
dyadyaSerezha
31.07.2024 07:08+1На сях как раз и начали их ловить раньше всех. Выводится warning, а дальше уже сам.
sl_1me
31.07.2024 07:08+1Хоть и оператор не нашёл большого применения, но читать статьи о нём и его истории очень интересно!
AlexeyK77
31.07.2024 07:08+1Нужно быть готовым в скором будущем к такому коду
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+8отлично! Получается что ввели целый оператор ради одного краевого случая. как по мне не лучшая это практика, плодить операторы.
funca
31.07.2024 07:08+5y := 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+1По идее, нужно было делать с самого начала как в первом варианте
Так не делают со времён Алгола-68, наверное. Потому что
-:=
,+:=
,*:=
,/:=
,%:=
...Но была и другая возможность - переиспользовать
as
.funca
31.07.2024 07:08Кстати да, есть ещё as. Мне кажется, что моржовый оператор в питоне это был реверанс в сторону Go. Но вообще вариантов много https://en.m.wikipedia.org/wiki/Assignment_(computer_science)
AlexeyK77
31.07.2024 07:08зато появился отличный вопрос валить джунов (а может и не только джунов) на собеседовании простым вопросом по операторам языка ;)
kekoz
31.07.2024 07:08Куда проще завалить того, кто не понимает разницы между оператором и операцией.
ahabreader
31.07.2024 07:08Сказал человек, не знающий разницы между оператором и инструкцией /s
Удивительно, но собеседуемый ради денег может даже согласиться по чётным дням statement'ы называть операторами, по нечётным - инструкциями, сохраняя вид лихой и придурковатый и извиняясь за то, что он вчера он выбирал "неправильный" перевод термина.
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 (чтение строк из файла), реализация через моржовый оператор завершает чтение на первой пустой строке
BoBaHPyt
31.07.2024 07:08Возможно изначально в посте был другой код, но в посте в обоих случаях чтение завершается на первой пустой строке, хоть с использованием :=, хоть без него
icya
31.07.2024 07:08Всё верно. К сожалению, автор решил показать "удобство" моржового оператора и, в то же время, отклонился от начальных условий к примерам. Такая же ситуация с примером 6 (All | Any)
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+8Какие-то надуманные проблемы. Как по мне, лучше десяток лишних строчек чем дублирующий оператор языка.
Синтаксис языка должен быть логичным, простым, понятным и по возможности без вариантов и исключений.
Dynasaur
31.07.2024 07:08+3Если под каждую алгоритмическую ситуацию придумывать новый оператор, то питон превратится в китайскую грамоту с тысячами иероглифов. Китайская грамота, кстати, очень лаконична, в одном иероглифе помещается несколько слов. Только учить и читать её замучаешься.
johngetman
31.07.2024 07:08офигенная вещь, я конечно не очень люблю пайтон но конкретно моржовый оператор это оч годная штука, всем кому она не нравится советую пересесть на луа, там вообще ничего лишнего нет
Verstov
31.07.2024 07:08+1Проблема этого кода заключается в том, что значение длины ключевого слова (
len(word)
)
вычисляется дважды: один раз в условном операторе, второй — при выводе
текста. Решить проблему можно с помощью дополнительной переменной:Насколько мне известно длина строки не высчитывается каждый раз. Она высчитывается один раз при создании. Так что пример несколько натянут
kekoz
31.07.2024 07:08Отличие оператора
:=
от классического оператора присваивания=
заключается в том, что благодаря ему можно присваивать переменным значения внутри выражений.Странно говорить о том, что “позволяет” при прямо заданном контексте “отличие”. “Как использовать” — это не отличие.
Во-первых, присваивание = — это операция (operator), а не оператор (statement). Разница существенная, если вы задумываетесь о семантике.
Во-вторых, ключевое, принципиальное отличие = от := заключается в том, что операция (operator) := является выражением (expression), а всякое выражение имеет значение (value). Операция = выражением не является, и значения не имеет.
Таким образом, вы можете использовать := везде, где язык требует наличия значения. И это вовсе не деды от Pascal (который сам заимствовал := из Algol) себе на радость притащили в Python, это скорее “давайте ещё функциональщины в язык добавим.” Взгляните на Rust — там практически любой оператор является выражением. Удобно, чёрт побери, присвоить переменной значение цикла или условного оператора
let x = if y {a} else {b};
HemulGM
Когда ожидается введение begin/end?
abramov_a
Скорее всего никогда))) Отступы - наше все.
dyadyaSerezha
Велик Питон, а отступать некуда! (с) Ну или почти так)
RichardMerlock
Вправо! Вправо отступайте!