Как стать автором
Обновить

PEP 572 (Выражения присваивания в python 3.8)

Время на прочтение28 мин
Количество просмотров21K
Автор оригинала: Chris Angelico, Tim Peters, Guido van Rossum
Привет, Хабр. В этот раз мы рассмотрим PEP 572, который рассказывает про выражения присваивания. Если Вы до сих пор скептически относитесь к оператору ":=" или не до конца понимаете правила его использования, то эта статья для Вас. Здесь вы найдёте множество примеров и ответов на вопрос: «Почему именно так?». Эта статья получилась максимально полной и если у Вас мало времени, то просмотрите раздел, написанный мной. В его начале собраны основные «тезисы» для комфортной работы с выражениями присваивания.

PEP 572 — Выражения Присваивания

PEP 572
Название: Выражения присваивания
Авторы: Chris Angelico <rosuav at gmail.com>, Tim Peters <tim.peters at gmail.com>, Guido van Rossum <guido at python.org>
Обсуждение: doc-sig at python.org
Статус: Принят
Тип: Стандарт
Создано: 28-Feb-2018
Версия Python: 3.8
Пост-история: 28-Feb-2018, 02-Mar-2018, 23-Mar-2018, 04-Apr-2018, 17-Apr-2018, 25-Apr-2018, 09-Jul-2018, 05-Aug-2019
Разрешение на принятие стандарта: mail.python.org/pipermail/python-dev/2018-July/154601.html (с VPN долго, но грузится)
Содержание


Аннотация


Это соглашение расскажет о появившейся возможности присваивания внутри выражений, с помощью нового обозначения 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, тем самым удалив один уровень отступов.

  • Текущий код:
    if self._is_special:
        ans = self._check_nans(context=context)
        if ans:
            return ans
  • Улучшенный код:
    if self._is_special and (ans := self._check_nans(context=context)):
        return ans

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 смог полностью сосредоточиться только на выражениях присваивания.

Альтернативные варианты написания


В целом, предложенные выражения присваивания имеют ту же семантику, но пишутся по-другому.

  1. 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

      Это усиливает визуальное распознавание выражений присваивания.
  2. 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) стрелочки менее привычны для присваивания.
  3. Добавление оператора «точка» к именам локальных переменных

    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, где область видимости переменной закодирована в ее имени, что затрудняет рефакторинг.
  4. Добавление where: к любой инструкции для создания локальных имен:

    value = x**2 + 2*x where:
        x = spam(1, 4, 7, q)

    Порядок выполнения инвертирован (часть с отступом выполнится первой, а затем последует срабатывание «заголовка»). Это потребует введения нового ключевого слова, хотя возможно «перепрофилирование» другого (скорее всего with:). См. PEP 3150, где раннее обсуждался этот вопрос (предложенным там словом являлось given: ).
  5. 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) < 0, и вы хотите сохранить значение f(x) ). Также это не имеет смысла в генераторах списков.

Преимущества: нет синтаксических неясностей. Недостатки: даже если пользоваться им только в операторах if/while, хорошо работает лишь в части случаев.

Особые случаи в генераторах


Другим распространенным вариантом использования выражения присваивания являются генераторы (list/set/dict и genexps). Как и выше, были сделаны предложения для конкретных решений.

  1. 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 в стандартной библиотеке).
  2. with NAME = EXPR:

    stuff = [(y, x/y) with y = f(x) for x in range(5)]

    Всё тоже самое, как и в верхнем пункте, но используется ключевое слово with. Неплохо читается и не нуждается в дополнительном ключевом слове. Тем не менее, способ более ограничен и не может быть легко преобразован в «петлевой» цикл for. Имеет проблему языка C, где знак равенства в выражении теперь может создавать переменную, а не выполнять сравнение. Также возникает вопрос: «А почему «with NAME = EXPR:» не может быть использовано просто как выражение, само по себе?»
  3. 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), существует две рекомендации:

  1. Если есть возможность использовать оба варианта присваивания, то отдайте предпочтите операторам. Они наиболее чётко выражают ваши намерениях.
  2. Если использование выражений присваивания приводит к неоднозначности порядка выполнения, то перепишите код с использованием классического оператора.

Благодарность


Авторы этого стандарта хотели бы поблагодарить Ника Коглана (Nick Coghlan) и Стивена Д'Апрано (Steven D'Aprano) за их значительный вклад в этот PEP, а также членов Python Core Mentorship за помощь в реализации.

Приложение A: выводы Тима Петерса


Вот краткое эссе, которое Тим Питерс написал на данную тематику.

Мне не нравятся «замороченный» код, а также не нравится помещать концептуально не связанную логику в одну строку. Так, например, вместо:

i = j = count = nerrors = 0

Я предпочитаю писать:

i = j = 0
count = 0
nerrors = 0

Поэтому я думаю, что найду несколько мест, в которых захочу использовать выражения присваивания. Я даже не хочу говорить про их использование в выражениях, которые и так растянуты на половину экрана. В остальных же случаях такое поведение, как:

mylast = mylast[1]
yield mylast[0]

Значительно лучше, чем это:

yield (mylast := mylast[1])[0]

Эти два кода имеют совершенно разные концепции и их перемешивание было бы безумно. В других случаях объединение логических выражений усложняет понимание кода. Например, переписав:

while True:
    old = total
    total += term
    if old == total:
        return total
    term *= mx2 / (i*(i+1))
    i += 2

В более краткой форме мы потеряли «логичность». Нужно хорошо понимать, как работает этот код. Мой мозг не хочет этого делать:

while total != (total := total + term):
    term *= mx2 / (i*(i+1))
    i += 2
return total

Но такие случаи редки. Задача сохранения результата встречается очень часто, и «разреженное лучше, чем плотное» не означает, что «почти пустое лучше, чем разреженное» [прим. отсылка к Дзену пайтона]. Например, у меня есть много функций, которые возвращают None или 0, чтобы сообщить «У меня нет ничего полезного, но так как это часто происходит, я не хочу надоедать вам исключениями». По сути, этот механизм используется и в регулярных выражениях, которые возвращают None, когда нет совпадений. Поэтому в таком примере много кода:

result = solution(xs, n)
if result:
    # use result

Я считаю следующий вариант более понятным, и конечно же более удобным для чтения:

if result := solution(xs, n):
    # use result

Сначала я не придавал этому особого значения, но такая короткая конструкция появлялась настолько часто, что меня довольно скоро начало раздражать, что я не могу воспользоваться ею. Это меня удивило! [прим. видимо это было написано до того, как официально вышел Python 3.8]

Есть и другие случаи, когда выражения присваивания действительно «выстреливают». Вместо того, чтобы ещё раз порыться в моём коде, Кирилл Балунов (Kirill Balunov) привел прекрасный пример функции copy() из стандартной библиотеки copy.py:

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(shallow)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(shallow)copyable object of type %s" % cls)

Простое использование выражений присваивания позволяет визуальной структуре кода подчеркнуть «плоскость» логики. А вот постоянно увеличивающийся отступ делает её неявной.

Вот ещё небольшой пример из моего кода, который очень порадовал меня, поскольку позволял поместить внутренне связанную логику в одну строку и удалить раздражающий «искусственный» уровень отступа. Это именно то, чего я хочу от оператора if и это позволяет упростить чтение. Следующий код:

diff = x - x_base
if diff:
    g = gcd(diff, n)
    if g > 1:
        return g

Превратился в:

if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
    return g

Итак, в большинстве строк, где происходит присваивание переменной, я бы не использовал выражения присваивания. Но эта конструкция настолько частая, что всё ещё есть много мест, где я бы воспользовался такой возможностью. В большинстве последних случаев я немного выиграл, поскольку они часто появлялись. В оставшейся под-части это привело к средним или большим улучшениям. Таким образом, я бы использовал выражения присваивания гораздо чаще, чем тройной if, но и значительно реже, чем augmented assignment [прим. короткие варианты: *=, /=, += и т.д.].

Числовой пример


У меня есть еще один пример, который поразил меня раннее.

Если все переменные являются положительными целыми числами, а переменная a больше n-ого корня из x, то этот алгоритм возвращает «нижнее» округление n-го корня из x (и примерно удваивает количество точных битов за итерацию):

while a > (d := x // a**(n-1)):
    a = ((n-1)*a + d) // n
return a

Непонятно почему, но такой вариант алгоритма менее очевиден, нежели бесконечный цикл с условной веткой break (loop and a half). Также трудно доказать правильность этой реализации, не опираясь на математическое утверждение («среднее арифметическое — среднее геометрическое неравенство») и не зная некоторых нетривиальных вещей о том, как ведут себя вложенные функции округления в меньшую сторону. Но здесь уже проблема заключена в математике, а не в программировании.

А если вы все это знаете, то вариант, использующий выражения присваивания читается очень легко, как простое предложение: «Проверьте текущую «догадку» и если она слишком велика, то уменьшите её» и условие позволяет сразу сохранять промежуточное значение из условия цикла. На мой взгляд, классическая форма труднее для понимания:

while True:
    d = x // a**(n-1)
    if a <= d:
        break
    a = ((n-1)*a + d) // n
return a

Приложение B: Грубый интерпретатор кода для генераторов


В этом приложении делается попытка прояснить (хотя и не указать) правила, по которым должно происходить создание переменной в генераторных выражениях. Для ряда иллюстративных примеров мы покажем исходный код, где генератор заменяется эквивалентной ему функцией в комбинации с некоторыми «строительными лесами».

Поскольку [x for ...] эквивалентно list(x for ...), то примеры не теряют своей общности. И поскольку эти примеры предназначены лишь для разъяснения общих правил, они не претендуют на реалистичность.

Примечание: генераторы сейчас реализованы через создание вложенных функций-генераторов (подобных тем, которые приведены в этом приложении). В примерах показана новая часть, которая добавляет соответствующий функционал для работы с областью видимости выражений присваивания (такой областью видимости, как если бы присваивание было выполнено в блоке, содержащем самый внешний генератор). Для упрощения «вывода типов» (type inference), эти иллюстративные примеры не учитывают, что выражения присваивания являются необязательными (но они учитывают, в какой область видимости будет находиться переменная, созданная внутри генератора).

Давайте сначала вспомним, какой код создаётся «под капотом» для генераторов без выражений присваивания:

  • Исходный код (EXPR чаще всего использует в себе переменную VAR):

    def f():
        a = [EXPR for VAR in ITERABLE]
  • Преобразованный код (давайте не будем беспокоиться о конфликтах имен):

    def f():
        def genexpr(iterator):
            for VAR in iterator:
                yield EXPR
        a = list(genexpr(iter(ITERABLE)))


Давайте добавим простое выражение присваивания.

  • Исходный код:

    def f():
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Преобразованный код:

    def f():
        if False:
            TARGET = None  # Dead code to ensure TARGET is a local variable
        def genexpr(iterator):
            nonlocal TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

Теперь давайте добавим инструкцию global TARGET в объявление функции f().

  • Исходный код:

    def f():
        global TARGET
        a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Преобразованный код:

    def f():
        global TARGET
        def genexpr(iterator):
            global TARGET
            for VAR in iterator:
                TARGET = EXPR
                yield TARGET
        a = list(genexpr(iter(ITERABLE)))

Или наоборот, давайте добавим nonlocal TARGET в объявление функции f().

  • Исходный код:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            a = [TARGET := EXPR for VAR in ITERABLE]
    
  • Преобразованный код:

    def g():
        TARGET = ...
        def f():
            nonlocal TARGET
            def genexpr(iterator):
                nonlocal TARGET
                for VAR in iterator:
                    TARGET = EXPR
                    yield TARGET
            a = list(genexpr(iter(ITERABLE)))

И наконец, давайте вложим два генератора.

  • Исходный код:

    def f():
        a = [[TARGET := i for i in range(3)] for j in range(2)]
        # I.e., a = [[0, 1, 2], [0, 1, 2]]
        print(TARGET)  # prints 2
    
  • Преобразованный код:

    def f():
        if False:
            TARGET = None
        def outer_genexpr(outer_iterator):
            nonlocal TARGET
            def inner_generator(inner_iterator):
                nonlocal TARGET
                for i in inner_iterator:
                    TARGET = i
                    yield i
            for j in outer_iterator:
                yield list(inner_generator(range(3)))
        a = list(outer_genexpr(range(2)))
        print(TARGET)

Приложение C: Никаких изменений в семантике области видимости


Обратите внимание, что в Python семантика области видимости не изменилась. Области видимости локальных функций по-прежнему определяются во время компиляции и имеют неопределенную временную протяженность во время выполнения (замыкания). Пример:

a = 42
def f():
    # `a` is local to `f`, but remains unbound
    # until the caller executes this genexp:
    yield ((a := i) for i in range(3))
    yield lambda: a + 100
    print("done")
    try:
        print(f"`a` is bound to {a}")
        assert False
    except UnboundLocalError:
        print("`a` is not yet bound")

Тогда:

>>> results = list(f()) # [genexp, lambda]
done
`a` is not yet bound
# The execution frame for f no longer exists in CPython,
# but f's locals live so long as they can still be referenced.
>>> list(map(type, results))
[<class 'generator'>, <class 'function'>]
>>> list(results[0])
[0, 1, 2]
>>> results[1]()
102
>>> a
42

Ссылки


  1. Доказательство реализации концепции
  2. Обсуждение семантики выражений присваивания (с VPN туго, но грузится)
  3. Обсуждение TargetScopeError в PEP 572 (грузится аналогично предыдущему)

Авторские права


Этот документ был размещен в открытом доступе.

Источник: github.com/python/peps/blob/master/pep-0572.rst

Моя часть


Для начала, подведём итоги:
  • Чтобы люди не говнокодили убрать смысловую двойственность, во многих «классических» местах, где можно было бы использовать и "=" и ":=" есть ограничения, поэтому оператор ":=" нужно часто заключать в скобки. Эти случаи придётся просмотреть в разделе, описывающем базовое использование.
  • Приоритет выражений присваивания чуть выше, чем у запятой. Благодаря этому, при присваивании не образуются кортежи. Также это даёт возможность использовать оператор := при передаче аргументов в функцию.
  • Выражения присваивания, находящиеся в генераторах, используют ту область видимости, в которой находится генератор. Это позволяет сохранять значения для повторного использования. А вот в lambda функциях это не сработает, они создают свою «анонимную» область видимости.
  • Теперь и в генераторах словарей строго определён порядок вычислений: сначала считается ключ, а потом соответствующее ему значение
  • Нельзя изменить в генераторе через присваивание переменную, использующуюся в итераторе.
  • Можно отстрелить левую ногу при попытке через генератор с присваиванием изменить/создать переменную класса.
  • Можно отстрелить правую ногу, подставив выражение присваивания в итерационное выражение.

В итоге, я хочу сказать, что мне понравился новый оператор. Он позволяет писать более плоский код в условиях, «фильтровать» списки, а также (наконец-то) убрать «ту самую», одинокую строчку перед if. Если люди будут использовать выражения присваивания по назначению, то это будет очень удобный инструмент, который повысит читабельность и красоту кода (Хотя, такое можно сказать про любой функционал языка....)
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как нужно переводить?
7.25% Переводить максимально слово в слово5
76.81% Если переводчик разбирается в теме, то он может слегка изменить текст, чтобы было понятнее53
15.94% Переводчик — ваш главный товарищ. Он может изменять текст, добавлять свои пояснения пример, удалять что-то не нужное и т.д.11
Проголосовали 69 пользователей. Воздержались 9 пользователей.
Теги:
Хабы:
Всего голосов 17: ↑17 и ↓0+17
Комментарии13

Публикации

Истории

Работа

Data Scientist
61 вакансия
Python разработчик
137 вакансий

Ближайшие события