[Перевод] PEP 572 (Выражения присваивания в python 3.8)
Привет, Хабр. В этот раз мы рассмотрим PEP 572, который рассказывает про выражения присваивания. Если Вы до сих пор скептически относитесь к оператору »:=» или не до конца понимаете правила его использования, то эта статья для Вас. Здесь вы найдёте множество примеров и ответов на вопрос: «Почему именно так?». Эта статья получилась максимально полной и если у Вас мало времени, то просмотрите раздел, написанный мной. В его начале собраны основные «тезисы» для комфортной работы с выражениями присваивания. Заранее простите, если найдёте ошибки (пишите про них мне, я исправлю). Ну, начнём:
PEP 572 — Выражения Присваивания
Содержание
Аннотация
Это соглашение расскажет о появившейся возможности присваивания внутри выражений, с помощью нового обозначения NAME:= expr.
В рамках нововведений был обновлен порядок вычисления генераторов словарей (dictionary comprehension). Это гарантирует, что выражение ключа вычислится перед выражением значения (это позволяет привязывать ключ к переменной, а затем повторно использовать созданную переменную в вычислении значения, соответствующего ключу).
Во время обсуждения этого PEP, данный оператор стал неофициально известен как «моржовый оператор» (the walrus operator). Формальное имя конструкции — «Выражение присваивания» (согласно заголовку PEP: Assignment Expressions), но она может упоминаться, как «Именованные выражения» (Named Expressions). Например, эталонная реализация в CPython использует именно это название.
Обоснование
Именование является важной частью программирования, которая позволяет использовать «описательное» имя вместо более длинного выражения, а также упрощает повторное использование значений. В настоящее время это возможно сделать лишь в виде инструкции, что делает эту операцию недоступной при генерации списков (list comprehension), а также в других выражениях.
Кроме того, именование частей большого выражения может помочь при интерактивной отладке, предоставив инструменты отображения подсказок и промежуточных результатов. Без возможности захвата результатов вложенных выражений, потребуется изменение исходного кода, но используя выражения присваивания вам достаточно вставить несколько «маркеров» вида «имя := выражение». Это устраняет лишний рефакторинг, а значит снижает вероятность непреднамеренного изменения кода в процессе отладки (частая причина Heisenbugs [прим. гейзенбаги — ошибки, которые меняют свойства кода во время отладки и могут неожиданно проявиться в продакшене]), а также данный код будет более понятен другому программисту.
Важность реального кода
Во время разработки этого PEP многие люди (как сторонники, так и критики) слишком концентрировались на игрушечных примерах с одной стороны, и на чрезмерно сложных примерах, с другой.
Опасность игрушечных примеров двояка: они часто слишком абстрактны, чтобы заставить кого-то сказать «о, это неотразимо», а также их легко отвергнуть со словами «Я бы никогда так не написал». Опасность же чрезмерно сложных примеров состоит в том, что они предоставляют удобную среду для критиков, предлагающих убрать такой функционал («Это слишком запутано», говорят такие люди).
Тем не менее, для таких примеров есть хорошее применение: они помогают уточнить предполагаемую семантику. Поэтому мы приведём ниже некоторые из них. Однако, чтобы быть убедительными, примеры должны основываться на реальном коде, который был написан без размышлений об этом PEP-е. То есть код, являющийся частью реально полезного приложения (без разницы: большое оно, или маленькое). Тим Питерс очень сильно помог нам, просмотрев свои личные репозитории и выбрав примеры написанного им кода, которые (по его мнению) стали бы более понятным, если их переписать (без фанатизма) с использованием выражений присваивания. Его вывод таков: текущие изменения привнесли бы скромное, но явное улучшение в нескольких битах его кода.
Другой пример реального кода — это косвенное наблюдение за тем, насколько программисты ценят компактность. Гвидо ван Россум проверил кодовую базу Dropbox и обнаружил некоторые доказательства того, что программисты предпочитают писать меньше строк кода, нежели чем использовать несколько небольших выражений.
Показательный случай: Гвидо нашел несколько иллюстративных моментов, когда программист повторяет подвыражение (тем самым замедляя программу), но экономит лишнюю строку кода. Например, вместо того, чтобы писать:
match = re.match(data)
group = match.group(1) if match else None
Программисты предпочитали такой вариант:
group = re.match(data).group(1) if re.match(data) else None
Вот ещё пример, показывающий, что программисты иногда готовы сделать больше работы, чтобы сохранить «прежний уровень» отступов:
match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
result = match1.group(1)
elif match2:
result = match2.group(2)
else:
result = None
Этот код вычисляет pattern2, даже если pattern1 уже совпал (в этом случае второе под-условие никогда не выполнится). Поэтому следующее решение является более эффективным, но менее привлекательным:
match1 = pattern1.match(data)
if match1:
result = match1.group(1)
else:
match2 = pattern2.match(data)
if match2:
result = match2.group(2)
else:
result = None
Синтаксис и семантика
В большинстве случаев, где в Python используются произвольные выражения (arbitrary expressions), теперь можно применять выражения присваивания. Они имеют форму NAME:= expr, где expr — любое допустимое выражение Python, кроме кортежа без скобок (unparenthesized tuple), а NAME — идентификатор. Значение такого выражения совпадает с исходным, но дополнительным эффектом является присвоение значения целевому объекту:
# Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match
# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
process(chunk)
# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]
# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]
Исключительные случаи
Есть несколько мест, где выражения присваивания не допускаются, чтобы избежать двусмысленности или путаницы среди пользователей:
- Выражения присваивания, не заключённые в скобки, запрещены на «верхнем» уровне:
y := f(x) # НЕДОПУСТИМО (y := f(x)) # Сработает, но не рекомендуется
Это правило упростит программисту выбор между оператором присваивания и выражением присваивания — не будет существовать синтаксической ситуации, в которой оба варианта равноценны. - Не заключенные в скобки выражения присваивания запрещены в правой части каскадного присваивания. Пример:
y0 = y1 := f(x) # НЕДОПУСТИМО y0 = (y1 := f(x)) # Сработает, но не рекомендуется
Не заключенные в скобки выражения присваивания запрещены в значениях ключевого аргумента при вызове функции. Пример:foo(x = y := f(x)) # НЕДОПУСТИМО foo(x=(y := f(x))) # Возможно, хотя и сбивает с толку
Опять же, такой механизм уменьшает у людей желание окончательно запутать и так сложный синтаксический анализ ключевых аргументов. - Не заключенные в скобки выражения присваивания запрещены в значениях аргумента по умолчанию. Пример:
def foo(answer = p := 42): # НЕДОПУСТИМО ... def foo(answer=(p := 42)): # Valid, though not great style ...
Это правило создано для предотвращения побочных эффектов в местах, где точная семантика и так сбивает с толку многих пользователей (см. Рекомендацию по общему стилю, которая против использования изменяемых «сущностей» в качестве значений по умолчанию). - Не заключенные в скобки выражения присваивания запрещены в качестве аннотаций для аргументов, возвращаемых значений и присваиваний. Пример:
def foo(answer: p := 42 = 5): # НЕДОПУСТИМО ... def foo(answer: (p := 42) = 5): # Разрешено, но бесполезно ...
Рассуждения по поводу введения этого правила аналогичны предыдущим: код, состоящий из комбинации операторов »=» и »:=» трудно правильно понять. - Не заключенные в скобки выражения присваивания запрещены в лямбда-функциях. Пример:
(lambda: x := 1) # НЕДОПУСТИМО lambda: (x := 1) # Разрешено, но бесполезно (x := lambda: 1) # Разрешено lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
Лямбда-функция имеет приоритет более высокий, чем »:=». Удобное присваивание лямбды к переменной здесь важнее. В случаях, когда переменная используется несколько раз, вам и так (наверняка) понадобятся скобки, потому это ограничение не сильно повлияет на ваш код. - Выражения присваивания внутри f-строк требуют скобок. Пример:
>>> f'{(x:=10)}' # Разрешено, выражение присваивания '10' >>> x = 10 >>> f'{x:=10}' # Разрешено, будет отформатировано, как '=10' ' 10'
Это показывает, что не всё выглядящее, как оператор присваивания в f-строке, является таковым. Парсер f-строки использует символ »:» для указания параметров форматирования. Чтобы сохранить обратную совместимость, при использовании оператора присваивания внутри f-строк он должен быть заключен в скобки. Как отмечено в примере выше, такое использование оператора присваивания не рекомендуется.
Область видимости
Выражение присваивания не вводит новую область видимости. В большинстве случаев область видимости, в которой будет создана переменная, не требует пояснений: она будет текущей. Если прежде переменная использовала ключевые слова nonlocal или global, тогда выражение присваивания учтёт это. Только лямбда (будучи анонимным определением функции) считается для этих целей отдельной областью видимости.
Существует один особый случай: выражение присваивания, встречающееся в генераторах списков, множеств, словарей или же в самих «выражениях генераторах» (ниже все вместе именуемые «генераторами» (comprehensions)), привязывает переменную к области видимости, которая содержит генератор, соблюдая модификатор globab или nonglobal, если таковой существует.
Обоснование для этого особого случая двояко. Во-первых, это позволяет нам удобно захватывать «участника» в выражениях any () и all (), например:
if any((comment := line).startswith('#') for line in lines):
print("First comment:", comment)
else:
print("There are no comments")
if all((nonblank := line).strip() == '' for line in lines):
print("All lines are blank")
else:
print("First non-blank line:", nonblank)
Во-вторых, это предоставляет компактный способ обновления переменной из генератора, например:
# Compute partial sums in a list comprehension
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)
Однако имя переменной из выражения присваивания не может совпадать с именем, которое уже используется в генераторах циклом for для итерации. Последние имена являются локальными по отношению к генератору, в котором появляются. Было бы противоречиво, если бы выражения присваивания ссылались ещё и к области видимости внутри генератора.
Например, [i: = i + 1 for i in range (5)] недопустимо: цикл for устанавливает, что i является локальной для генератора, но часть «i:= i+1» настаивает на том, что i является переменной из внешней области видимости. По той же причине следующие примеры не сработают:
[[(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)] # НЕДОПУСТИМО
Хотя технически возможно назначить согласованную семантику для таких случаев, но трудно определить, сработает то, как мы понимаем эту семантику, в вашем реальном коде. Именно поэтому эталонная реализация гарантирует, что такие случаи вызывают SyntaxError, а не выполняются с неопределённым поведением, зависящим от конкретной аппаратной реализации. Это ограничение применяется, даже если выражение присваивания никогда не выполняется:
[False and (i := 0) for i, j in stuff] # НЕДОПУСТИМО
[i for i, j in stuff if True or (j := 1)] # НЕДОПУСТИМО
# [прим. для новичков. Из-за "ленивой" реализации логических
# операторов, второе условие никогда не вычислится в обоих
# случаях, ведь результат заранее известен, но ошибка будет]
Для тела генератора (часть перед первым ключевым словом «for») и выражения-фильтра (часть после «if» и перед любым вложенным «for») это ограничение применяется исключительно к именам перемененных, которые одновременно используются в качестве итерационных переменных. Как мы уже сказали, Лямбда-выражения вводят новую явную область видимости функции и следовательно могут использоваться в выражениях генераторов без дополнительных ограничений. [прим. опять же, кроме таких случаев: [i for i in range (2, (lambda: (s:=2)()))] ]
Из-за конструктивных ограничений в эталонной реализации (анализатор таблицы символов не может распознать, используются ли имена из левой части генератора в оставшейся части, где находится итерируемое выражение), поэтому выражения присваивания полностью запрещены как часть итерируемых (в части после каждого «in» и перед любым последующим ключевым словом «if» или «for»). То есть все эти случаи недопустимы:
[i+1 for i in (j := stuff)] # НЕДОПУСТИМО
[i+1 for i in range(2) for j in (k := stuff)] # НЕДОПУСТИМО
[i+1 for i in [j for j in (k := stuff)]] # НЕДОПУСТИМО
[i+1 for i in (lambda: (j := stuff))()] # НЕДОПУСТИМО
Еще одно исключение возникает, когда выражение присваивания применяется в генераторах, которые находятся в области видимости класса. Если при использовании выше перечисленных правил должно произойти создание перемеренной в области видимости класса, то такое выражение присваивания недопустимо и приведёт к возникновению SyntaxError:
class Example:
[(j := i) for i in range(5)] # НЕДОПУСТИМО
(Причиной последнего исключения является неявная область видимости функции, созданной генератором — в настоящее время нет runtime механизма для функций, чтобы сослаться на переменную, расположенную в области видимости класса, и мы не хотим добавлять такой механизм. Если эта проблема когда-либо будет решена, то этот особый случай (возможно) будет удалён из спецификации выражений присваивания. Обратите внимание, что эта проблема возникнет, даже если вы раннее создали переменную в области видимости класса и пытаетесь изменить её выражением присваивания из генератора.)
Смотрите приложение B для примеров того, как выражения присваивания находящиеся в генераторах, преобразуются в эквивалентный код.
Относительный приоритет :=
Оператор := группируется сильнее, чем запятая во всех синтаксических позициях где это возможно, но слабее, чем все другие операторы, включая or, and, not, и условные выражения (A if C else B). Как следует из раздела «Исключительные случаи» выше, выражения присваивания никогда не работают на том же «уровне», что и классическое присваивание =. Если требуется другой порядок операций, используйте круглые скобки.
Оператор := может использоваться непосредственно при вызове позиционного аргумента функции. Однако непосредственно в аргументе это не сработает. Некоторые примеры, уточняющие, что является технически разрешённым, а что невозможно:
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')) # Рабочая альтернатива
Большинство приведенных выше «допустимых» примеров не рекомендуется использовать на практике, поскольку люди, быстро просматривающие ваш исходный код, могут не правильно понять его смысл. Но в простых случаях это разрешено:
# Valid
if any(len(longline := line) >= 100 for line in lines):
print("Extremely long line:", longline)
Этот PEP рекомендует абсолютно всегда ставить пробелы вокруг :=, аналогично рекомендации PEP 8 для = для классического присваивания. (Отличие последней рекомендации в том, что она запрещает пробелы вокруг =, который используется для передачи ключевых аргументов функции.)
Изменение порядка вычислений.
Чтобы иметь точно определенную семантику, данное соглашение требует, чтобы порядок оценки был четко определен. Технически это не является новым требованием. В Python уже есть правило, что подвыражения обычно вычисляются слева направо. Однако выражения присваивания делают эти «побочные эффекты» более заметными, и мы предлагаем одно изменение в текущем порядке вычислений:
- В генераторах словарей {X: Y for …}, Y в настоящее время вычисляется перед X. Мы предлагаем изменить это так, чтобы X вычислялся до Y. (В классическом dict, таком как {X: Y}, а также в dict ((X, Y) for …) это уже реализовано. Поэтому и генераторы словарей должны соответствовать этому механизму)
Различия между выражениями присваивания и инструкциями присваивания.
Что наиболее важно,»:=» является выражением, а значит его можно использовать в случаях, когда инструкции недопустимы, включая лямбда-функции и генераторы. И наоборот, выражения присваивания не поддерживают расширенный функционал, который можно использовать в инструкциях присваивания:
- Каскадное присваивание не поддерживается на прямую
x = y = z = 0 # Equivalent: (z := (y := (x := 0)))
- Отдельные «цели», кроме простого имени переменной NAME, не поддерживаются:
# No equivalent a[i] = x self.rest = []
- Функционал и приоритет «вокруг» запятых отличается:
x = 1, 2 # Sets x to (1, 2) (x := 1, 2) # Sets x to 1
- Распаковка и упаковка значений не имеют «чистую» эквивалентность или вообще не поддерживаются
# Equivalent needs extra parentheses loc = x, y # Use (loc := (x, y)) info = name, phone, *rest # Use (info := (name, phone, *rest)) # No equivalent px, py, pz = position name, phone, email, *other_info = contact
- Встроенные аннотации типов не поддерживаются:
# Closest equivalent is "p: Optional[int]" as a separate declaration p: Optional[int] = None
- Укороченная форма операций отсутствует:
total += tax # Equivalent: (total := total + tax)
Спецификация изменяется во время реализации
Следующие изменения были сделаны на основе полученного опыта и дополнительного анализа после первого написания данного PEP и перед выпуском Python 3.8:
- Для обеспечения согласованности с другими подобными исключениями, а также чтобы не вводить новое название, которое не обязательно будет удобно для конечных пользователей, первоначально предложенный подкласс TargetScopeError для SyntaxError был убран и понижен до обычного SyntaxError. [3]
- Из-за ограничений в анализе таблицы символов CPython, эталонная реализация выражения присваивания вызывает SyntaxError для всех случаев использования внутри итераторов. Раньше это исключение возникало только если имя создаваемой переменной совпадало с тем, которое уже используется в итерационном выражении. Это может быть пересмотрено при наличии достаточно убедительных примеров, но дополнительная сложность кажется нецелесообразной для чисто «гипотетических» вариантов использования.
Примеры
Примеры из стандартной библиотеки Python
site.py
env_base используется только в условии, поэтому присваивание можно поместить в if, как «заголовок» логического блока.
- Текущий код:
env_base = os.environ.get("PYTHONUSERBASE", None) if env_base: return env_base
- Улучшенный код:
if env_base := os.environ.get("PYTHONUSERBASE", None): return env_base
_pydecimal.py
Вы можете избегать вложенных if, тем самым удалив один уровень отступов.
copy.py
Код выглядит более классическим, а также позволяет избежать множественной вложенности условных операторов. (См. Приложение A, чтобы узнать больше о происхождении этого примера.)
- Текущий код:
reductor = dispatch_table.get(cls) if reductor: rv = reductor(x) else: reductor = getattr(x, "__reduce_ex__", None) if reductor: rv = reductor(4) else: reductor = getattr(x, "__reduce__", None) if reductor: rv = reductor() else: raise Error( "un(deep)copyable object of type %s" % cls)
- Улучшенный код:
if reductor := dispatch_table.get(cls): rv = reductor(x) elif reductor := getattr(x, "__reduce_ex__", None): rv = reductor(4) elif reductor := getattr(x, "__reduce__", None): rv = reductor() else: raise Error("un(deep)copyable object of type %s" % cls)
datetime.py
tz используется только для s += tz. Перемещение его внутрь if помогает показать его логическую область использования.
- Текущий код:
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) tz = self._tzstr() if tz: s += tz return s
- Улучшенный код:
s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) if tz := self._tzstr(): s += tz return s
sysconfig.py
Вызов fp.readline (), как «условие» в цикле while (, а также вызов метода .match ()) в условии if делает код более компактным, не усложняя его понимание.
- Текущий код:
while True: line = fp.readline() if not line: break m = define_rx.match(line) if m: n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v else: m = undef_rx.match(line) if m: vars[m.group(1)] = 0
- Улучшенный код:
while line := fp.readline(): if m := define_rx.match(line): n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v elif m := undef_rx.match(line): vars[m.group(1)] = 0
Упрощение генераторов списков
Теперь генератор списка может эффективно фильтроваться путем «захвата» условия:
results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]
После этого переменная может быть повторно использована в другом выражении:
stuff = [[y := f(x), x/y] for x in range(5)]
Ещё раз обратите внимание, что в обоих случаях переменная y находится в той же области видимости, что и переменные result и stuff.
«Захват» значений в условиях
Выражения присваивания могут быть эффективно использованы в условиях оператора if или while:
# Loop-and-a-half
while (command := input("> ")) != "quit":
print("You entered:", command)
# Capturing regular expression match objects
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
# of this effect
if match := re.search(pat, text):
print("Found:", match.group(0))
# The same syntax chains nicely into 'elif' statements, unlike the
# equivalent using assignment statements.
elif match := re.search(otherpat, text):
print("Alternate found:", match.group(0))
elif match := re.search(third, text):
print("Fallback found:", match.group(0))
# Reading socket data until an empty string is returned
while data := sock.recv(8192):
print("Received data:", data)
В частности, такой подход может устранить необходимость создавать бесконечный цикл, присваивание и проверку условия. Он также позволяет провести гладкую параллель между циклом, который использует вызов функции в качестве своего условия, а также циклом, который не только проверяет условие, но и использует фактическое значение, возвращённое функцией, в дальнейшем.
Fork
Пример из низкоуровневого мира UNIX: [прим. Fork () — системный вызов в Unix-подобных операционных системах, создающий новый под-процесс, по отношению к родительскому.]
if pid := os.fork():
# Parent code
else:
# Child code
Отклоненные альтернативны
В целом, схожие предложения довольно часто встречаются в python сообществе. Ниже приведен ряд альтернативных синтаксисов для выражений присваивания, которые являются слишком специфическими для понимания и были отклонены в пользу приведенного выше.
Изменение области видимости для генераторов
В предыдущей версии этого PEP предлагались внести тонкие изменения в правила области видимости для генераторов, чтобы сделать их более пригодными для использования в области видимости классов. Однако эти предложения привели бы к обратной несовместимости, поэтому были отклонены. Поэтому данный PEP смог полностью сосредоточиться только на выражениях присваивания.
Альтернативные варианты написания
В целом, предложенные выражения присваивания имеют ту же семантику, но пишутся по-другому.
- EXPR as NAME:
stuff = [[f(x) as y, x/y] for x in range(5)]
Так как конструкция EXPR as NAME уже имеет семантический смысл в выражениях import, except и with, это могло создать ненужную путаницу и некоторые ограничения (например, запрет выражения присваивания внутри заголовков этих конструкций).(Обратите внимание, что «with EXPR as VAR» не просто присваивает значение EXPR в VAR, а вызывает EXPR.__enter__() и уже после присваивает полученный результат в VAR.)
Дополнительные причины, чтобы предпочесть »:=» выше предложенному написанию:
- В том случае, если if f (x) as y не бросится вам в глаза, то его можно случайно прочитать как if f x blah-blah, и визуально такая конструкция слишком похожа на if f (x) and y.
- Во всех других ситуациях, когда as разрешено, даже читателям со средними навыками приходится прочитывать всю конструкцию от начала, чтобы посмотреть на ключевое слово:
- import foo as bar
- except Exc as var
- with ctxmgr () as var
И наоборот, as не относится к оператором if или while и мы преднамеренно создаём путаницу, допуская использование as в «не родной» для него среде. - Также существует «параллель» соответствия между
- NAME = EXPR
- if NAME:= EXPR
Это усиливает визуальное распознавание выражений присваивания.
- EXPR → NAME
stuff = [[f(x) -> y, x/y] for x in range(5)]
Этот синтаксис основан на таких языках, как R и Haskell, ну и некоторых программируемых калькуляторах. (Обратите внимание, что направление стрелки справа-налево y < — f (x) невозможно в Python, поскольку конструкция будет интерпретироваться как меньше-чем и унарный минус.) Данный синтаксис имеет небольшое преимущество перед «as» в том смысле, что не конфликтует с конструкциями import, except и with, но в остальном проблемы те же. Но эти проблемы совершенно не связано с другим использованием такой стрелки в Python (в аннотациях возвращаемого типа функции), а просто по сравнению с »:=» (которое восходит к Algol-58) стрелочки менее привычны для присваивания. - Добавление оператора «точка» к именам локальных переменных
stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as" stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="
Это позволит легко обнаруживать и устраняя некоторые формы синтаксической неоднозначности. Однако такое нововведение стало бы единственным местом в Python, где область видимости переменной закодирована в ее имени, что затрудняет рефакторинг. - Добавление where: к любой инструкции для создания локальных имен:
value = x**2 + 2*x where: x = spam(1, 4, 7, q)
Порядок выполнения инвертирован (часть с отступом выполнится первой, а затем последует срабатывание «заголовка»). Это потребует введения нового ключевого слова, хотя возможно «перепрофилирование» другого (скорее всего with:). См. PEP 3150, где раннее обсуждался этот вопрос (предложенным там словом являлось given: ). - TARGET from EXPR:
stuff = [[y from f(x), x/y] for x in range(5)]
Этот синтаксис меньше конфликтует с другими, чем as (если только не считать конструкции raise Exc from Exc), но в остальном сравним с ними. Вместо параллели с with expr as target: (что может быть полезно, но может и сбить с толку), этот вариант вообще не имеет параллелей ни с чем, но к удивлению лучше запоминается.
Особые случаи в условных операторах
Один из самых популярных вариантов использования выражений присваивания — это операторы if и while. Вместо более общего решения, использование as улучшает синтаксис этих двух операторов, добавляя средство захвата сравниваемого значения:
if re.search(pat, text) as match:
print("Found:", match.group(0))
Это прекрасно работает, но ТОЛЬКО, когда желаемое условие основано на «правильности» возвращаемого значения. Таким образом, данный способ эффективен для конкретных случаев (проверки совпадения регулярных выражений, чтения сокетов, возвращающее пустую строку, когда заканчивается выполнение), и совершенно бесполезен в более сложных случаях (например, когда условие равно f (x)
Преимущества: нет синтаксических неясностей. Недостатки: даже если пользоваться им только в операторах if/while, хорошо работает лишь в части случаев.
Особые случаи в генераторах
Другим распространенным вариантом использования выражения присваивания являются генераторы (list/set/dict и genexps). Как и выше, были сделаны предложения для конкретных решений.
- where, let, or given:
stuff = [(y, x/y) where y = f(x) for x in range(5)] stuff = [(y, x/y) let y = f(x) for x in range(5)] stuff = [(y, x/y) given y = f(x) for x in range(5)]
Этот способ приводит появлению подвыражения между циклом «for» и основным выражением. Он также вводит дополнительное ключевое слово языка, что может создать конфликты. Из трех вариантов, where является наиболее чистым и читабельным, но потенциальные конфликты всё ещё существуют (например, SQLAlchemy и numpy имеют свои методы where, также как и tkinter.dnd.Icon в стандартной библиотеке). - with NAME = EXPR:
stuff = [(y, x/y) with y = f(x) for x in range(5)]
Всё тоже самое, как и в верхнем пункте, но используется ключевое слово with. Неплохо читается и не нуждается в дополнительном ключевом слове. Тем не менее, способ более ограничен и не может быть легко преобразован в «петлевой» цикл for. Имеет проблему языка C, где знак равенства в выражении теперь может создавать переменную, а не выполнять сравнение. Также возникает вопрос: «А почему «with NAME = EXPR:» не может быть использовано просто как выражение, само по себе?» - with EXPR as NAME:
stuff = [(y, x/y) with f(x) as y for x in range(5)]
Похоже на второй вариант, но с использованием as, а не знака равенства. Синтаксически родственно другими видами присваивания промежуточных имён, но имеет те же проблемы с циклами for. Смысл при использованием ключевого слова with в генераторах и в качестве отдельной инструкции будет совершенно различным
Независимо от выбранного способа, будет введено резкое семантическое различие между генераторами и их развёрнутыми версиями через цикл for. Стало бы невозможно обернуть цикл в генератор без переработки этапа создания переменных. Единственное ключевое слово, которое могло бы быть переориентировано для этой задачи, это слово with. Но это придаст ему различную семантику в разных частях код, а значит нужно создать новое ключевое слово, но это сопряжено с большим затратами.
Понижение приоритета оператора
Оператор := имеет два логических приоритета. Либо он должен иметь настолько низкий приоритет, насколько это возможно (наравне оператора присваивания). Либо должен иметь приоритет больший, чем операторы сравнения. Размещение его приоритета между операторами сравнения и арифметическими операциями (если быть точным: чуть ниже, чем побитовое ИЛИ) позволит при использовании операторов while и if в большинстве случаев обходиться без скобок, так как более вероятно, что вы хотите сохранить значение чего-либо до того, как выполнится сравнение над ним:
pos = -1
while pos := buffer.find(search_term, pos + 1) >= 0:
...
Как только find () возвращает -1, цикл завершается. Если := связывает операнды также свободно, как и =, то результат find () будет сначала «захвачен» в оператор сравнения и вернёт обычно значение True, либо False, которое менее полезно.
Хоть такое поведение и было бы удобно на практике во многих ситуациях, но его и было бы сложнее объяснить. А так мы можем сказать, что «оператор := ведет себя так же, как и оператор обычного присваивания». То есть приоритет для := был выбран максимально близко к оператору = (за исключением того, что := имеет приоритет выше, чем запятая).
Даёшь запятые справа
Некоторые критики утверждают, что выражения присваивания должны распознавать кортежи без добавления скобок, чтобы эти две записи были эквивалентны:
(point := (x, y))
(point := x, y)
(В текущей версии стандарта последняя запись будет эквивалентна выражению ((point: = x), y) .)
Но логично, что в таком раскладе, при использовании в вызове функции выражения присваивания оно также имело бы приоритет меньший, чем запятая, поэтому мы получили бы следующую запутанную эквивалентность:
foo (x: = 1, y)
foo (x: = (1, y))
И мы получаем единственный менее запутанный выход: сделать оператор := меньшего приоритета, чем запятую.
Всегда требующие скобки
Было предложено всегда заключать в скобки выражения присваивания. Это избавило бы нас от многих двусмысленностей. И действительно, скобки часто будут необходимы, чтобы извлечь желаемое значение. Но в следующих случаях наличие скобок явно показалось нам излишними:
# Top level in if
if match := pattern.match(line):
return match.group(1)
# Short call
len(lines := f.readlines())
Частые возражения
Почему бы просто не превратить инструкции присваивания в выражения?
C и подобные ему языки определяют оператор = как выражение, а не инструкцию, как это делает Python. Это позволяет осуществлять присваивание во многих ситуациях, включая места, где происходит сравнение переменных. Синтаксическое сходство между if (x == y) и if (x = y) противоречит их резко отличающейся семантике. Таким образом, этот PEP вводит оператор := для уточнения их различия.
Зачем заморачиваться с выражениями присваивания, если существуют инструкции присваивания?
Две этих формы имеют различные гибкие возможности. Оператор := можно использовать внутри большего выражения, а в операторе = может использоваться «семейством мини-операторов» по типу »+=». Также = позволяет присваивать значения по атрибутам и индексам.
Почему бы не использовать локальную область видимости и предотвратить загрязнение пространства имен?
Предыдущие версии этого стандарта включали в себя реальную локальную область действия (ограниченную одним оператором) для выражений присваивания, предотвращая утечку имен и загрязнения пространства имен. Несмотря на то, что в ряде ситуаций это давало определенное преимущество, во многих других это усложняет задачу, и выгоды не оправдываются преимуществами существующего подхода. Это сделано в интересах простоты языка. Вам больше не нужна эта переменная? Есть выход: удалите переменную через ключевое слово del или добавьте к её названию нижнее подчеркивание.
(Автор хотел бы поблагодарить Гвидо ван Россума и Кристофа Грота за их предложения по продвижению стандарта PEP в этом направлении. [2])
Рекомендации по стилю
Поскольку выражения присваивания иногда могут использоваться наравне с оператором присваивания, возникает вопрос, чему всё-таки отдавать предпочтение?… В соответствии с другими соглашениями о стиле (такими, как PEP 8), существует две рекомендации:
- Если есть возможность использовать оба варианта присваивания, то отдайте предпочтите операторам. Они наиболее чётко выражают ваши намерениях.
- Если использование выражений присваивания приводит к неоднозначности порядка выполнения, то перепишите код с использованием классического оператора.
Благодарность
Авторы этого стандарта хотели бы поблагодарить Ника Коглана (Nick Coghlan) и Стивена Д'Апрано (Steven D’Aprano) за их значительный вклад в этот PEP, а также членов Python Core Mentorship за помощь в реализации.
Приложение A: выводы Тима Петерса
Вот краткое эссе, которое Тим Питерс написал на данную тематику.
Мне не нравятся «замороченный» код, а также не нравится помещать концептуально не связанную логику в одну строку. Так, например, вместо:
i = j = count = nerrors = 0
Я предпочитаю