Сообщить об ошибке.

Присваивание значений в выражении walrus в Python

Оператор "моржа" walrus в Python

Выражения присваивания введено в Python 3.8 и означает способ присваивания значения переменной в выражении.

name := expr

Оператор := неофициально называют как "оператор моржа", в связи его схожестью с изображением морды моржа (два глаза и усы, смотрящие вниз).

Рекомендуется вокруг := всегда ставить пробелы, аналогично рекомендации PEP 8 для = при использовании для присваивания

Содержание:


Применение оператора "присваивания в выражении" 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'