Выражения присваивания введено в Python 3.8 и означает способ присваивания значения переменной в выражении.
name := expr
Оператор :=
неофициально называют как "оператор моржа", в связи его схожестью с изображением морды моржа (два глаза и усы, смотрящие вниз).
Рекомендуется вокруг :=
всегда ставить пробелы, аналогично рекомендации PEP 8 для =
при использовании для присваивания
walrus
;walrus
и оператором присваивания;walrus
;walrus
;walrus
.# Обрабатывать подходящее регулярное выражение if (match := pattern.search(data)) is not None: # Сделать что-нибудь с `match` # Цикл, который нельзя "по простому" # переписать с помощью `iter()` while chunk := file.read(8192): # Сделать что-нибудь с 'chunk' # Повторно использовать значение, которое дорого вычислять [y := f(x), y**2, y**3] # Совместное использование подвыражения # при фильтрации последовательностей filtered_data = [y for x in data if (y := f(x)) is not None]
Оператор :=
может использоваться непосредственно в позиционном аргументе функции, но недействителен непосредственно в аргументе ключевого слова. Некоторые примеры для уточнения, что является технически действительным или недействительным:
# НЕДОПУСТИМО x := 0 # Допустимая альтернатива (x := 0) # НЕДОПУСТИМО x = y := 0 # Допустимая альтернатива x = (y := 0) # Допустимо len(lines := f.readlines()) # Допустимо foo(x := 3, cat='vector') # НЕДОПУСТИМО foo(cat=category := 'vector') # Допустимая альтернатива foo(cat=(category := 'vector'))
Большинство приведенных выше допустимых примеров не рекомендуются, поскольку читатели исходного кода Python, которые быстро просматривают какой-то блок кода, могут пропустить различие. Но простые случаи не являются нежелательными:
if any(len(longline := line) >= 100 for line in lines): print("Extremely long line:", longline)
walrus
и оператором присваивания.Поскольку :=
является выражением, его можно использовать в тех случаях, когда операторы недопустимы, включая лямбда-функции и действиях типа [total := total + v for v in values]. И наоборот, выражения присваивания не поддерживают расширенные функции, которые можно найти в операторах присваивания:
Групповое присваивание не поддерживаются напрямую:
x = y = z = 0 # Эквивалент (z := (y := (x := 0)))
Присваивание по индексу элемента или по атрибуту не поддерживаются:
# Нет эквивалента a[i] = x self.rest = []
Приоритет вокруг запятых отличается:
x = 1, 2 print(x) # (1, 2) (x := 1, 2) print(x) # 1
Упаковка и распаковка, как обычной, так и расширенной формы не поддерживаются:
# Эквиваленту нужны дополнительные скобки loc = x, y # Использовать (loc := (x, y)) info = name, phone, *rest # Использовать (info := (name, phone, *rest)) # Нет эквивалента px, py, pz = position name, phone, email, *other_info = contact
Расширенное присваивание не поддерживается:
total += tax # Эквивалент (total := total + tax)
Встроенные аннотации типа не поддерживаются:
walrus
.Выражение присваивания walrus
не вводит новую область видимости. В большинстве случаев область, в которой будет привязана переменная при таком присваивании, это текущая область видимости. Если эта область видимости содержит nonlocal
или global
объявление для переменной, выражение присваивания учитывает это.
Выражение присваивания walrus
, встречающееся в действиях со списками, наборами или вхождениями или в выражениями генераторами, связывает переменную этого выражения еще и с областью видимости данного действия, выполняя объявление переменной в этой области, если таковая существует. Для описанного случая, ее область видимости nonlocal
или global
будет расширена вложенной областью действия (Лямбда считается вложенной областью действия), в которой она может быть изменена! То есть переменная выражения присваивания будет доступна для изменения как из вложенной области действия, так и из nonlocal
или global
.
Во-первых, использование оператора walrus
позволяет удобно захватывать "свидетеля" для выражения функций any()
или "контрпример" для all()
, например:
if any((comment := line).startswith('#') for line in lines): print("Первый комментарий:", comment) else: print("Комментариев нет") if all((nonblank := line).strip() == '' for line in lines): print("Все пустые строки") else: print("Первая непустая строка:", nonblank)
Во-вторых, использование оператора walrus
предоставляет компактный способ обновления изменяемого состояния переменной из вложенной области видимости, например:
# без оператора `walrus` >>> values = [3, 5, 2, 6, 12, 7, 15] >>> tmp = 'unmodified' >>> dummy = [tmp for tmp in values] # Как и ожидалось, переменная `tmp` не изменилась >>> tmp # `unmodified` # использование оператора `walrus` total = 0 partial_sums = [total := total + v for v in values] # переменная `total` изменилась! >>> total
Целевая переменная выражения присваивания НЕ ДОЛЖНА совпадать с именем i
цикла for i in ...
. Переменная i
являются вложенной по отношению к действиям с элементами последовательности.
Например, [i := i+1 for i in range(5)]
НЕДОПУСТИМО: часть for i
устанавливает, что i
является вложенной для действия с range()
, но часть i :=
настаивает на том, что i
не является внутренней для range()
и расширяет ее до nonlocal
или global
.
# Эти выражения будут НЕ ПРАВИЛЬНЫМИ [[(j := j) for i in range(5)] for j in range(5)] [i := 0 for i, j in stuff] [i+1 for i in (i := stuff)] # Это ограничение применяется, даже если # выражение присваивания никогда не выполняется: [False and (i := 0) for i, j in stuff] # НЕ ПРАВИЛЬНО [i for i, j in stuff if True or (j := 1)] # НЕ ПРАВИЛЬНО
walrus
.Первый пример, это то, как можно использовать оператор walrus
для уменьшения количества вызовов функций. Представим функцию с именем func()
, которая выполняет очень ресурсоемкие вычисления. Вычисление результатов занимает много времени, поэтому нежелательно вызывать её много раз.
Вот как это сделать:
# `func()` вызывается 3 раза result = [func(x), func(x)**2, func(x)**3] # Повторно используется только результат `func()`. result = [y := func(x), y**2, y**3]
Да, можно просто добавить y = func(x)
перед объявлением списка, и тогда не нужен оператор walrus
, но это одна дополнительная ненужная строка кода.
Рассмотрим еще один пример с той же дорогостоящей функцией func()
, которая будет использоваться в генераторе списка:
# `func()` вызывается 2 раза result = [func(x) for x in data if func(x)] # `func()` вызывается только 1 раз result = [y for x in data if (y := func(x))]
В первой строке функция func(x)
вызывается дважды в каждом цикле. Во второй сроке, с помощью оператора walrus
функция вычисляется один раз в операторе if
, а затем повторно используется только ее результат y
. Длина кода одинакова, обе строки одинаково читаемы, но вторая в два раза эффективнее. Можно избежать использования оператора walrus
, сохранив при этом производительность, при этом придется использовать полный цикл for/in
.
Одним из наиболее распространенных вариантов использования оператора walrus
является сокращение вложенных условных выражений, например, при использовании сопоставления с регулярным выражением.
import re test = "Something to match" pattern1 = r"^.*(thing).*" pattern2 = r"^.*(not present).*" # код без использования оператора `walrus` m = re.match(pattern1, test) if m: print(f"Совпало с 1-м образцом: {m.group(1)}") else: m = re.match(pattern2, test) if m: print(f"Совпало с 2-м образцом: {m.group(1)}") # эквивалентный код с использованием # оператора `walrus` if m := (re.match(pattern1, test)): print(f"Совпало с 1-м образцом: '{m.group(1)}'") elif m := (re.match(pattern2, test)): print(f"Совпало с 2-м образцом: '{m.group(1)}'")
Следующим примером в списке является так называемая идиома "loop-and-half". Вот как это выглядит:
# код без использования оператора `walrus` while True: # Loop command = input("> ") if command == 'exit': # And a half break print("Your command was:", command) # эквивалентный код с использованием # оператора `walrus` while (command := input("> ")) != "exit": print("Your command was:", command)
Обычное решение - использовать фиктивный бесконечный цикл while
с делегированием потока управления оператору break
. Вместо этого можно использовать оператор walrus
для переназначения значения команды, а затем использовать его в условном цикле while
в той же строке.
Аналогичное упрощение можно применить и к другим циклам while
, например, при построчном чтении файлов или при получении данных из сокета.
walrus
.Смотрим пример:
for i in range(1, 100): 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
В приведенном выше фрагменте создается условие с двумя присваиваниями walrus
, соединенными оператором and
. Они проверяют, делится ли число на 2, 3 или 6 в зависимости от того, выполнены ли первое, второе или оба условия. На первый взгляд это может показаться хорошим трюком, но из-за замыкания оператора and
, если выражение (two := i % 2 == 0)
не выполняется, то вторая часть будет пропущена, а значит, three
не вычислится или будет иметь устаревшее значение из предыдущего цикла.
Замыкание также может быть полезным/преднамеренным. Его можно использовать с регулярными выражениями для поиска нескольких шаблонов в строке:
import re tests = ["Something to match", "Second one is present"] pattern1 = r"^.*(thing).*" pattern2 = r"^.*(present).*" # код без использования оператора `walrus` for test in tests: m = re.match(pattern1, test) if m: print(f"Совпало с 1-м образцом: {m.group(1)}") else: m = re.match(pattern2, test) if m: print(f"Совпало с 2-м образцом: {m.group(1)}") # Вывод # Совпало с 1-м образцом: 'thing' # Совпало с 2-м образцом: 'present' # эквивалентный код с использованием # оператора `walrus` for test in tests: if m := (re.match(pattern1, test) or re.match(pattern2, test)): print(f"Matched: '{m.group(1)}'") # Вывод # Подходит: 'thing' # Подходит: 'present'