В Python 3.8 появился новый оператор, официально называющийся выражением присваивания, — :=
. Но чаще его называют моржовым оператором (англ. walrus — «морж»), поскольку он напоминает морду моржа с клыками.
После добавления этого оператора в обычно спокойном мире Python возникли споры, и его использование до сих пор относительно ограничено. Тем не менее, выражение присваивания теперь является частью Python, и оно может быть полезным в некоторых ситуациях.
В этом руководстве мы рассмотрим, как работает моржовый оператор в Python, и обсудим ситуации, в которых он может пригодиться.
Содержание
- Что собой представляет моржовый оператор в Python?
- Примеры использования моржового оператора
- Споры относительно моржового оператора
- Правила синтаксиса моржового оператора
- Заключение
Что собой представляет моржовый оператор в Python?
Выражение присваивания (англ. assignment expression), в котором используется оператор :=
, похоже на более распространенное предложение присваивания (англ. assignment statement), в котором используется оператор =
. Однако между ними есть важное различие.
Предложение (англ. statement) — это единица кода, которая выполняет некоторое действие. Выражение — это предложение, которое может быть оценено в значение. Поэтому выражение возвращает значение, в то время как предложения, не являющиеся выражениями, никакого значения не возвращают.
В REPL Python значение выражения отображается сразу после его оценки. Однако при выполнении предложения, не являющегося выражением, значение не отображается:
>>> import random # Не выражение >>> 10 * 2 # Выражение 20
Предложение import
не оценивается в какое-либо значение. А вот 10 * 2
— это выражение, которое оценивается в значение.
Предложение присваивания, использующее оператор =
, присваивает объект переменной или другой ссылке, но не возвращает значение. Выражение присваивания, или моржовый оператор, выполняет то же действие, что и предложение присваивания, но при этом возвращает объект:
>>> value = 10 # Предложение присваивания >>> value 10 >>> (value := 20) # Выражение присваивания 20
Обратите внимание, что предложение присваивания не возвращает значение, в отличие от выражения присваивания. Таким образом, выражение присваивания объединяет в себе несколько операций:
- Оценивает выражение в правой части оператора
:=
. - Присваивает это значение имени переменной.
- Возвращает значение.
Круглые скобки являются обязательным синтаксисом при использовании выражения присваивания вместо предложения присваивания. Это помогает избежать двусмысленности. Но к синтаксису мы еще вернемся.
Выражение присваивания можно использовать везде, где в Python в принципе можно использовать выражения. Давайте рассмотрим пример:
print(f"The number is {(value := 50)}") print(value) # Вывод: # The number is 50 # 50
Имя переменной value
определено в f-строке. При этом целое число также возвращается непосредственно в фигурные скобки в f-строке. Фигурные скобки в f-строках должны содержать выражение, поэтому тут нельзя использовать предложение присваивания.
Примеры использования моржового оператора
Два распространенных случая использования моржового оператора — упрощение и оптимизация кода.
Упрощение кода
Чаще всего моржовый оператор используется для упрощения кода, в котором переменная должна быть определена перед использованием, например, в некоторых условных предложениях.
Рассмотрим следующий цикл while
:
import random value = random.randint(1, 20) while value < 18: print(value) value = random.randint(1, 20)
Вывод:
7 5 1 11 12 1 9
Этот код выполняет блок while
до тех пор, пока генерируемое случайное значение меньше 18. Чтобы переменную value
можно было использовать в операторе while
, ее нужно определить перед циклом while
. В конце блока while
переменной value
присваивается новое случайное число.
Для сокращения количества строк кода и вызова функции random.randint()
в одном месте программы можно использовать выражение присваивания :=
:
import random while (value := random.randint(1, 20)) < 18: print(value)
Вывод:
1 9 2 4 9 15 11
Новое случайное число генерируется и присваивается переменной value
каждый раз, когда цикл while
начинает новую итерацию.
Оптимизация кода
Давайте рассмотрим еще один пример с использованием представления списков. Следующий код фильтрует список строк, содержащих даты, и создает список объектов datetime.datetime
, содержащий только даты в пределах заданного года:
from datetime import datetime def format_date(date_str): return datetime.strptime(date_str, "%Y-%m-%d") dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"] formatted_dates = [ format_date(date) for date in dates if format_date(date).year == 2024 ] print(formatted_dates)
Вывод:
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]
Функция format_date()
принимает строку с датой в формате yyyy-mm-dd и возвращает объект datetime.datetime
. Одним из атрибутов объекта datetime.datetime
является .year
, который содержит годовой компонент даты.
Только три даты 2024 года попадают в итоговый список, который содержит объекты datetime.datetime
вместо строк. Представление списка содержит предложение if
, которое вызывает format_date()
. Однако format_date()
также вызывается в первом выражении в представлении списка, которое генерирует значения для добавления в список. Вызывать функцию с одним и тем же аргументом дважды неэффективно, особенно если эта функция является узким местом в производительности программы.
Один из вариантов избежать двойного вызова одной и той же функции — вызвать функцию и присвоить ее переменной перед использованием. Однако этого нельзя добиться с помощью предложения присваивания, используя при этом представление списка. Это решение требует стандартного цикла for
:
from datetime import datetime def format_date(date_str): return datetime.strptime(date_str, "%Y-%m-%d") dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"] formatted_dates = [] for date in dates: formatted_date = format_date(date) if formatted_date.year == 2024: formatted_dates.append(formatted_date) print(formatted_dates)
Вывод:
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]
Моржовый оператор предоставляет альтернативу, совместимую с представлением списка, поскольку отформатированная дата, возвращаемая функцией, может быть присвоена имени переменной и возвращена в том же выражении:
from datetime import datetime def format_date(date_str): return datetime.strptime(date_str, "%Y-%m-%d") dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"] formatted_dates = [ formatted_date for date in dates if (formatted_date := format_date(date)).year == 2024 ] print(formatted_dates)
Вывод:
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]
Выражением присваивания в представлении списка является выражение, заключенное в круглые скобки, (format_date := format_date(date))
. Это выражение вызывает функцию format_date()
, присваивает ее значение переменной formatted_date
и возвращает это значение. Поскольку выражение присваивания оценивает объект datetime.datetime
, код обращается к атрибуту .year
, который сравнивается с 2024 годом с помощью оператора равенства.
Блок if
в представлении списка выполняется перед выражением, которое генерирует значение для сохранения в списке. Поэтому в блоке if
используется моржовый оператор, а переменная, определенная в этом выражении присваивания, используется в качестве первого выражения в представлении списка.
Споры относительно моржового оператора
Не всем программистам Python понравилось выражение присваивания. Некоторые видят в нем способ упростить код, но другие считают, что оно делает код менее читабельным, и предпочитают более длинные варианты. Читабельность может быть субъективной, поэтому мнения о том, делает ли моржовый оператор код более или менее читабельным, расходятся.
Последний пример из предыдущего раздела демонстрирует необходимость компромисса. Без моржового оператора код в этом примере требует сначала инициализировать пустой список, а затем добавлять в него значения в цикле for
. Это решение более длинное и менее эффективное, чем использование представлений списков, которые оптимизированы для этой работы.
Однако стандартное решение с циклом for
может быть более читабельным для некоторых программистов, которые использованию моржового оператора предпочтут большее количество кода, особенно если производительность не является проблемой.
Многие руководства по стилю написания кода до сих пор рекомендуют избегать выражения присваивания или использовать его лишь в редких случаях.
Правила синтаксиса моржового оператора в Python
При первом знакомстве с моржовым оператором часто возникают синтаксические ошибки из-за правил его синтаксиса.
Чтобы избежать путаницы между предложением присваивания =
и выражением присваивания :=
, не существует ситуации, в которой оба варианта синтаксически допустимы. По этой причине часто необходимо заключать выражение присваивания в круглые скобки:
(value := 20) # Допустимо value := 20 # Недопустимо (SyntaxError)
Требование заключить выражение присваивания в круглые скобки в этой ситуации противоположно требованию к предложению присваивания, которое не может быть заключено в круглые скобки.
Однако выражение присваивания не всегда нуждается в круглых скобках:
import random if value := random.randint(0, 3): print(f"{value} is greater than 0") # Вывод: # 1 is greater than 0
Выражение присваивания в операторе if
генерирует случайное число от 0 до 3 и присваивает его значению переменной. Целые числа 1, 2 и 3 являются истинными, и код в блоке if
будет выполняться при генерации этих значений. Однако, когда random.randint()
возвращает 0, что является ложным значением, блок if
не выполняется.
Предложение присваивания не может идти после ключевого слова if
. Поэтому в данной ситуации нет никакой путаницы, и круглые скобки не нужны.
Этот пример также демонстрирует еще один важный момент, связанный с выражением присваивания. Область видимости присваиваемой переменной подчиняется обычным правилам. Поэтому в данном примере значение переменной доступно в глобальной области видимости:
import random if value := random.randint(0, 3): print(f"{value} is greater than 0") print(value) # Вывод: # 2 is greater than 0 # 2
Если выражение присваивания используется в определении функции, переменная будет доступна только локально внутри функции. Это такое же поведение, как и для переменных, созданных с помощью предложения присваивания.
Этот пример можно модифицировать, чтобы показать еще один потенциальный подводный камень при использовании моржового оператора. Давайте изменим этот код, чтобы включить оператор сравнения в оператор if
:
import random if value := random.randint(0, 3) < 2: print(f"{value} is less than 2") # Вывод: # True is less than 2
Показанный вывод намекает на проблему, связанную с этим кодом, поскольку value
— это булево значение True, а не целое число. Моржовый оператор имеет практически наименьший приоритет из всех операторов, меньше — только у запятой. Поэтому оператор меньше чем (<
) оценивается первым, и его результат присваивается значению. Для решения этой ситуации необходимы круглые скобки:
import random if (value := random.randint(0, 3)) < 2: print(f"{value} is less than 2") # Вывод: # 0 is less than 2
Теперь выражение присваивания оценивается первым, так как скобки имеют наивысший приоритет. Число, возвращаемое random.randint()
, присваивается значению value
и используется в качестве первого операнда для оператора <
.
В отличие от предложения присваивания, моржовый оператор может присваивать значения только именам переменных и не может быть использован для присвоения значений атрибутам или с помощью подскриптов:
(name := "James") # Допустимо points = {"James": 10, "Mary": 20} (points["Sarah"] := 30) # Недопустимо class Article: pass article = Article() (article.title := "The Walrus Operator") # Недопустимо
Используя моржовый оператор, невозможно присвоить значение с помощью подскрипта или присвоить значение атрибуту.
Заключение
Выражение присваивания, использующее синтаксис :=
и часто называемое моржовым оператором, является относительно новым дополнением к Python. Предложение присваивания, в котором используется оператор =
, является более распространенным способом присваивания значения имени переменной.
Однако моржовый оператор расширяет эту операцию присваивания, возвращая присвоенное значение, что превращает присваивание в выражение. Таким образом, вы можете использовать выражение присваивания везде, где в Python можно использовать другие выражения.
Однако в сообществе Python существуют разногласия по поводу полезности и читабельности моржового оператора. Поэтому он используется не так часто, как другие новые дополнения к Python. Здорово знать, как использовать этот оператор, но не злоупотребляйте им!
Перевод статьи “Python’s Walrus Operator: The Assignment Expression (Tutorial)”.