Зачем в инструкции присваивания используются скобки python

Привет, Хабр. В этот раз мы рассмотрим PEP 572, который рассказывает про выражения присваивания. Если Вы до сих пор скептически относитесь к оператору «:=» или не до конца понимаете правила его использования, то эта статья для Вас. Здесь вы найдёте множество примеров и ответов на вопрос: «Почему именно так?». Эта статья получилась максимально полной и если у Вас мало времени, то просмотрите раздел, написанный мной. В его начале собраны основные «тезисы» для комфортной работы с выражениями присваивания.

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

Содержание

  • Аннотация
  • Обоснование
  • Синтаксис и семантика
  • Спецификация изменяется во время реализации
  • Примеры
  • Отклоненные альтернативны
  • Частые возражения
  • Рекомендации по стилю
  • Благодарность
  • Приложение A: выводы Тима Петерса
  • Приложение B: Грубый интерпретатор кода для генераторов
  • Приложение C: Никаких изменений в семантике области видимости
  • Ссылки
  • Авторские права
  • Моя часть

Аннотация

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

Присваивание в Python и его каноническая форма

В Python, как и во многих других языках программирования, каноническая форма операции присваивания имеет вид a = b,
где слева от оператора присваивания записывается целевое имя переменной или компонент объекта, а в качестве правого операнда выступает произвольное выражение, которое в результате
вычислений дает объект (см. пример №1).

# Присвоили выражение. 
a = 5 + 0.3
# Выведет 5.3.
print(a, end='nn')

# Присвоили список.
b = [1, 1, 2]
print(b, end='nn')        

# Изменили первый эл-т списка.
b[0] = 0
# Выведет [0, 1, 2].
print(b, end='nn')       

# Объявили ф-цию со значением по умолчанию.
def my_func(li=[]):
    return li

# Присвоили объект функции.
f = my_func        
# Выведет [0, 1, 2].
print(f(b))          
        
# Присвоили вызов функции.
e = my_func(b)        
# Выведет [0, 1, 2].        
print(e)
5.3

[1, 1, 2]

[0, 1, 2]

[0, 1, 2]
[0, 1, 2]
















	
	
	

Пример №1. Каноническая форма присваивания.

Как видим, присваивание действительно выполняется достаточно просто. Однако при этом хотелось бы обратить внимание на следующие особенности присваивания
в Python:

  • Инструкции присваивания всегда сохраняют ссылки на объекты, например, в переменных или в элементах структур данных, и никогда не создают копии присваиваемых объектов.
    Как следствие, переменные в Python больше напоминают указатели, чем области хранения данных.
  • Объявлять переменные заранее нельзя, т.к. интерпретатор создает переменные в момент присваивания им значений (то есть ссылок на объекты). После инициализации, когда
    имя переменной будет встречаться в выражениях, оно будет автоматически замещаться объектом, на который ссылается данная переменная. Если же интерпретатор
    встретит в программе неинициализированную переменную, он возбудит исключение вместо того, чтобы вернуть какое-либо значение по умолчанию (такое поведение призвано
    облегчить поиск ошибок и опечаток в коде).
  • Некоторые инструкции пусть и неявно, но тоже выполняют операцию присваивания. Примерами могут служить импорт модуля, определение функции или класса, указание
    переменной в цикле for или же передача аргументов функции. В любом случае результатом явных или неявных инструкций присваивания будет
    связывание имен с ссылками на объекты.

Ну, и в конце пункта еще раз напомним, что в программировании знак равно (=) обычно означает не равенство двух величин, а присваивание
значения переменной. Если же нужно указать на равенство двух значений, то используется комбинация символов из двух знаков равно (==).

Комбинированные инструкции присваивания

В дополнение к базовой инструкции присваивания в Python имеется и целый ряд комбинированных инструкций присваивания, которые объединяют
операцию присваивания с другой операцией. В общем виде инструкцию присваивания с комбинированным оператором x operator= y можно считать
сокращенной записью инструкции x = x operator y. Например, x += y является сокращенной записью
инструкции присваивания x = x + y, в которой к значению переменной x прибавляется значение переменной
y, а результат присваивается переменной x (см. пример №2).

# Присвоили начальное значение.
a = 3   
print('a = 3 ->', a, end='nn')
 
# Теперь a == 9, что равнозначно a = a + 6.
a += 6   
print('a += 6 ->', a, end='nn')
 
# Теперь a == 18, что равнозначно a = a*2.
a *= 2  
print('a *= 2 ->', a, end='nn')
 
# Теперь a == 9.0, что равнозначно a = a/2.
a /= 2  
print('a /= 2 ->', a, end='nn')
 
# Теперь a == 81.0, что равнозначно a = a**2.
a **= 2 
print('a **= 2 ->', a, end='nn')
 
# Теперь a == 1.0, что равнозначно a = a%2.
a %= 2  
print('a %= 2 ->', a)
a = 3 -> 3

a += 6 -> 9

a *= 2 -> 18

a /= 2 -> 9.0

a **= 2 -> 81.0

a %= 2 -> 1.0









	
	
	

Пример №2. Комбинированная форма присваивания.

Таким образом, комбинированная инструкция присваивания объединяет в себе выражение и присваивание, являясь по сути краткой формой записи кода. И хотя, например,
инструкции num += 25 и num = num + 25 дадут один и тот же результат, первая из них выглядит явно
компактнее. Кроме того, если объект справа относится к категории изменяемых объектов и поддерживает указанную операцию, комбинированная инструкция присваивания может
выполняться даже быстрее за счет непосредственного изменения объекта вместо создания и изменения его копии (см. пример №3).

# Импортируем модуль time.
import time
        
# Получаем стартовое значение времени.
t_1 = time.time()

# Размер начнет быстро расти, что с 
# увеличением кол-ва итераций приведет в 
# данном случае к замедлению работы. 
li = [0]

# Запускаем цикл 10 млн. раз.
for i in range(0, 10**5):	
    # Обычное присваивание.
    li = li + [1]
    
# Получаем конечное значение времени.
t_2 = time.time()
# Вывело 1.7589106559753 сек.
print(round((t_2 - t_1), 13), 'сек', end='nn')

# Тоже самое, но для комбинированного присваивания.

# Получаем стартовое значение времени.
t_1 = time.time()

# Здесь список тоже будет расти, но список будет 
# изменяться напрямую, а не через создание копии.
li = [0]

# Запускаем цикл 10 млн. раз.
for i in range(0, 10**5):	
    # Комбинированное присваивание.
    li += [1]
    
# Получаем конечное значение времени.
t_2 = time.time()
# Вывело 1.4650475978851 сек.
print(round((t_2 - t_1), 13), 'сек')
21.4193923473358 сек

0.0101900100708 сек
































	
	
	
 

Пример №3. Преимущества комбинированной формы присваивания.

Метод time() одноименного модуля time стандартной библиотеки Python
возвращает время, выраженное в секундах с начала эпохи. В операционных системах Unix, например, за начало эпохи принимается
1 января 1970,
00:00:00 (UTC). Но поскольку в
программах в основном используются интервалы времени между двумя событиями, а не время, прошедшее с начала эпохи, знать дату начала эпохи какой-либо конкретной
операционной системы вовсе необязательно, т.к. разница между двумя временными точками для всех платформ получается совершенно одинаковой.

Давайте еще раз и в одном месте перечислим основные инструкции присваивания, комбинированные с арифметическими операторами:

  • a += b – тоже самое, что и a = a + b;
  • a -= b – тоже самое, что и a = a — b;
  • a *= b – тоже самое, что и a = a * b;
  • a **= b – тоже самое, что и a = a**b;
  • a /= b – тоже самое, что и a = a / b;
  • a //= b – тоже самое, что и a = a // b;
  • a %= b – тоже самое, что и a = a % b.

Позиционное присваивание

Еще одной формой присваивания в Python является позиционное присваивание, которое связывает объекты
последовательностей справа от оператора присваивания с именами, перечисляемыми слева от него. При этом присваивание выполняется слева направо согласно местоположениям
имен и соответствующих им объектов (см. пример №4).

# Позиционное присваивание кортежа значений кортежу переменных.
a, b = 3, 5   
# Выведет a == 3, b == 5.
print('a ==', a, ', b ==', b, end='nn')
 
# Позиционное присваивание списка значений кортежу переменных.
a, b = [3, 5]   
# Выведет a == 3, b == 5.
print('a ==', a, ', b ==', b, end='nn')               

# Позиционное присваивание списка значений списку переменных.
[a, b] = [3, 5]   
# Выведет a == 3, b == 5.
print('a ==', a, ', b ==', b, end='nn')       

# Позиционное присваивание строки списку переменных.
[a, b] = 'AB'   
# Выведет a == A, b == B.
print('a ==', a, ', b ==', b, end='nn')          

# Кол-во присв-ых значений должно совпадать с кол-м переменных.
a, b = 'ABC'   
# too many values to unpack (expected 2).
print('a ==', a, ', b ==', b) 
a == 3 , b == 5

a == 3 , b == 5

a == 3 , b == 5

a == A , b == B

too many values to unpack (expected 2)












	
	
	

Пример №4. Позиционное присваивание (часть 1).

Как видим, данная форма присваивания позволяет смешивать последовательности разных типов. Здесь главное, чтобы количество присваиваемых значений совпадало с количеством переменных, которым
эти значения будут присваиваться. Но и эта проблема может быть решена за счет имеющейся возможности использования в присваивании расширенного синтакиса распаковывания последовательностей
(см. пример №5).

# Используем синтаксис распаковывания последовательности.
a, b, *c = [1, 2, 3, 4, 5]  
# Выведет a == 1, b == 2, c == [3, 4, 5].
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')
 
*a, b, c = [1, 2, 3, 4, 5]   
# Выведет a == [1, 2, 3], b == 4, c == 5.
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')               

a, *b, c = [1, 2, 3, 4, 5]   
# Выведет a == 1, c == [2, 3, 4], b == 5.
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')       

# Граничный случай.
a, b, *c = [1, 2, 3]  
# Выведет a == 1, b == 2, c == [3].
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')  

# Еще один граничный случай.
a, b, *c = [1, 2]  
# Выведет a == 1, b == 2, c == [].
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')        

# Еще один граничный случай.
*c, = [1, 2]  
# Выведет c == [1, 2].
print('c ==', c, end='nn') 

# Использовать несколько звездочек запрещено.
# *a, b, *c = [1, 2]  
# Так тоже нельзя, нужна последовательность имен.
# *c = [1, 2]
a == 1 , b == 2 , c == [3, 4, 5]

a == [1, 2, 3] , b == 4 , c == 5

a == 1 , b == [2, 3, 4] , c == 5

a == 1 , b == 2 , c == [3]

a == 1 , b == 2 , c == []

c == [1, 2]


















	
	
	

Пример №5. Позиционное присваивание (часть 2).

Благодаря наличию имени со звездочкой все лишние значения автоматически помещаются в соответсвующий список именно так, как и ожидается. Однако нужно не забывать, что можно использовать
только одну переменную со звездочкой, а сама переменная должна принадлежать последовательности даже в том случае, если она будет одна (поставив запятую в инструкции
*c, = [1, 2] мы автоматически получили кортеж и смогли избежать ошибки).

Следует добавить, что расширенная операция распаковывания последовательностей может использоваться не только в случаях явного присваивания значений, но также и в заголовках циклов, вызовах
функций и других операциях неявного присваивания значений.

Групповое присваивание одного значения

В случае группового присваивания объект, расположенный справа, присваивается всем переменным группы (см. пример №6). В результате несколько отдельных
инструкций присваивания могут быть заменены одной эквивалентной и более компактной инструкцией.

# Используем 3 инструкции присваивания по отдельности.
a = 'Ok'  
b = a
c = b
# Выведет a == 'Ok', b == 'Ok', c == 'Ok'.
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')

# Или 1 эквивалентную инст-цию группового присваивания.
c = b = a = 'Ok'  
# Опять же выведет a == 'Ok', b == 'Ok', c == 'Ok'.
print('a ==', a, ', b ==', b, ', c ==', c) 
a == Ok , b == Ok , c == Ok

a == Ok , b == Ok , c == Ok





	
	
	

Пример №6. Присваивание одного значения группе переменных (часть 1).

Следует иметь в виду, что при групповом присваивании в памяти создается всего лишь один объект, разделяемый всеми переменными группы. Поэтому такая форма присваивания будет полезна для
неизменяемых объектов, например, чисел или строк. А вот при использовании изменяемых объектов типа списков или словарей нужно быть осторожными, т.к. изменение объекта через одну из переменных
группы будет оказывать влияние и на другие переменные (см. пример №7). Это связано с тем, что переменные в Python хранят
не сами объекты, а ссылки на них. А раз так, то даже после изменения объекта через одну из ссылок все остальные по-прежнему будут указывать на тот же, пусть и модифицированный, объект.

# Используем число (неизменяемый объект).
c = b = a = 0  
# Изменим b и c.
b = 1
c = 2
# Выведет a == 0, b == 1, c == 2.
print('a ==', a, ', b ==', b, ', c ==', c, end='nn')        
       
# Используем список (изменяемый объект).
c = b = a = []  
# Пробуем изменить b и c.
b += [1]
c += [2]
# Выведет для всех [1, 2].
print('a ==', a, ', b ==', b, ', c ==', c)
a == 0 , b == 1 , c == 2

a == [1, 2] , b == [1, 2] , c == [1, 2]









	
	
	

Пример №7. Присваивание одного значения группе переменных (часть 2).

Таким образом, во избежание проблем с изменяемыми объектами, их инициализацию лучше производить в отдельных инструкциях, как было показано в первой части примера
№6.

Выражения присваивания в Python

Все рассмотренные нами формы присваивания относятся к инструкциям. Но их запрещено использовать в выражениях, что иногда может быть весьма полезным. Поэтому начиная с версии
Python 3.8 было решено ввести новую конструкцию NAME := expr с возможностью использования в выражениях
(см. пример №8). Конструкция получила название выражение присваивания или же
именованное выражение, а оператор := стал неофициально называться моржовым оператором.

# В выражении инициализируются сразу 3 переменные.       
a = (b := 3) + (c := 5)
# Выведет a == 8, b == 3, c == 5.
print('a ==', a, ', b ==', b, ', c ==', c, end='nn') 

# Список функций (для однозначности используем скобки).       
li = [(x_2 := lambda y: y**2), (x_3 := lambda y: y**3)]
# Выведет li[0](5) == 25, li[1](5) == 125.
print('li[0](5) ==', li[0](5), ', li[1](5) ==', li[1](5))
# Выведет те же результаты: x_2(5) == 25, x_3(5) == 125.
print('x_2(5) ==', x_2(5), ', x_3(5) ==', x_3(5), end='nn')
a == 8 , b == 3 , c == 5

li[0](5) == 25 , li[1](5) == 125
x_2(5) == 25 , x_3(5) == 125




	
	
	

Пример №8. Использование выражений присваивания (часть 1).

Использовать выражение присваивания в коде верхнего уровня без скобок запрещается. Например, инструкция x = y := 5 не пройдет, нужно использовать скобки:
x = (y := 5). И вообще, использование скобок с моржовым оператором следует сразу же взять на вооружение, т.к. это поможет избежать многих досадных ошибок.

Возможно, кто-то сочтет такое нововведение нецелесообразным, ведь выражения порой и так бывают довольно громоздкими, чтобы вводить туда еще и дополнительные конструкции с набором
ограничений. Но ситуации бывают разные как, например, в примере №9.

# Выводим сообщение и призыв к вводу.
print('Введите 2 положительных числа.')
x = float(input('Введите 1-е число: '))       
y = float(input('Введите 2-е число: '))

# Для уменьшения объема кода.
mess = 'Разрешены только положительные числа!'

# Осуществляем проверки и вывод результатов.
if y > 0:
    res = x/y
    if res > 0:
        print('x/y =', res)
    else:
        print(mess, end='nn')             
else:
    print(mess, end='nn')
	
# Выводим сообщение и призыв к вводу.
print('Введите 2 положительных числа.')
x = float(input('Введите 1-е число: '))       
y = float(input('Введите 2-е число: '))

# Проверка в одном if.
if y > 0 and (res := x/y) > 0:
    # Выводим результат.
    print('x/y =', res)
# Иначе сообщение об ошибке.             
else:
    print('Разрешены только положительные числа!')
Введите 2 положительных числа.
Введите 1-е число: 5			
Введите 2-е число: 10
x/y = 0.5

Введите 2 положительных числа.
Введите 1-е число: 5			
Введите 2-е число: 10
x/y = 0.5




















			

Пример №9. Использование выражений присваивания (часть 2).

Подробнее о выражениях присваивания можете почитать на официальном сайте в документации к
PEP 572 – Assignment Expressions.

Краткие итоги параграфа

  • В Python инструкции присваивания всегда сохраняют ссылки на объекты, например, в переменных или в элементах структур данных, и никогда не создают
    копии присваиваемых объектов.
  • Классическая форма присваивания имеет вид NAME = expr, что соответствует синтаксису многих других языков программирования. Однако в
    Python доступны и другие формы инструкций присваивания: комбинированные инструкции присваивания (например, y **= 3),
    позиционное присваивание (например, x, y = [3, 5] или x, *y = ‘abcdef’), а также групповое присваивание одного значения
    (например, a = b = c = 33).
  • При групповом присваивании одного значения в памяти создается всего лишь один объект, разделяемый всеми переменными группы. Поэтому такая форма присваивания будет полезна для
    неизменяемых объектов, например, чисел или строк. А вот при использовании изменяемых объектов типа списков или словарей нужно быть осторожными, т.к. изменение объекта через одну из
    переменных группы будет оказывать влияние и на другие переменные.
  • Поскольку в Python запрещается использовать инструкции в выражениях, начиная с версии 3.8 была введена специальная
    конструкция NAME := expr, получившая название выражения присваивания и предназначенная для присваивания значений внутри выражений, например,
    a = (b := 3) + (c := 5).

Вопросы и задания для самоконтроля

1. Перечислите основные формы инструкций присваивания в Python.

Показать решение.

Ответ. Классическая форма NAME = expr, комбинированные инструкции присваивания
NAME operator= expr, позиционное присваивание NAME_1, …, NAME_n = expr_1, …, expr_n, а также групповое
присваивание одного значения NAME_1 = … = NAME_n = expr.

2. Имеются две инициализированные переменные a = ‘one’ и b = ‘two’. Поменяйте значения
переменных местами, использовав позиционную форму инструкции присваивания.

Показать решение.

Ответ. Так как в процессе позиционного присваивания интерпретатор создает временный кортеж, где сохраняются оригинальные значения переменных
справа, данная форма присваивания может использоваться для реализации обмена значений переменных без создания временной переменной. Таким образом, кортеж справа автоматически запоминает
предыдущие значения переменных.

# Инициализируем переменные.       
a = 'one'
b = 'two'

# Производим замену значений.       
a, b = 'two', 'one'
print('a == ', a, ', b == ', b, sep='')
a == two, b == one





			

3. О чем следует помнить, когда трем переменным присваивается один и тот же изменяемый объект?

Показать решение.

Ответ. Если, например, выполнить присваивание следующим образом: a = b = c = [], то все три переменные
будут ссылаться на один и тот же объект. Поэтому непосредственное изменение объекта с помощью одной переменной (например, A.append(5)) отразится и на
других. Однако это справедливо только для изменений, производимых непосредственно в изменяемых объектах, таких как списки или словари. Для неизменяемых объектов, вроде чисел и строк,
такой проблемы не возникает.

4. Какие из представленных фрагментов кода содержат ошибки:
a, b = [3, ‘3’]
x, *y = 1, 2, 3
*x, = 1, 2, 3
*x, y, *z = 1, 2, 3, 4? Объясните ответ.
Показать решение.

Ответ. Все фрагменты кода, кроме последнего, верны. А вот последняя инструкция содержит две переменные со звездочкой, что
расширенным синтаксисом распаковывания последовательностей не допускается.

Быстрый переход к другим страницам

«Curly Braces» are used in Python to define a dictionary. A dictionary is a data structure that maps one value to another — kind of like how an English dictionary maps a word to its definition.

Python:

dict = {
    "a" : "Apple",
    "b" : "Banana",
}

They are also used to format strings, instead of the old C style using %, like:

ds = ['a', 'b', 'c', 'd']
x = ['has_{} 1'.format(d) for d in ds]

print x

['has_a 1', 'has_b 1', 'has_c 1', 'has_d 1']

They are not used to denote code blocks as they are in many «C-like» languages.

C:

if (condition) {
    // do this
}

Update: In addition to Python’s dict data types Python has (since Python 2.7) set as well, which uses curly braces too and are declared as follows:

my_set = {1, 2, 3, 4}

В Python операции присваивания бывают трех видов: простое присваивание, составное присваивания и выражения присваивание.

Простое присваивание значения переменной name = expr, это способ, создать новую переменную или повторно связать существующую переменную с другим значением. Простое присваивание значения атрибуту объекта x.attr = expr, это запрос к объекту x создать или повторно связать атрибут. Простое присваивание значения элементу контейнера, x[k] = expr, это запрос к контейнеру x создать или повторно связать элемент с индексом или ключом k.

Составное присваивание, например name += expr не может само по себе создать новую ссылку. Такое присваивание может лишь повторно связать переменную, а также попросить объект повторно связать один из его существующих атрибутов или элементов или изменить самого себя. Когда идет обращаетесь к объекту, он самостоятельно решает, как именно удовлетворить этот запрос или бросить исключение.

Выражения присваивания введено в Python 3.8. Это способ присваивания значение переменной в выражении с использованием обозначения name := expr. Оператор := стал неофициально известен как «оператор моржа», в связи его схожестью с моржом. Формальное имя конструкции — «Выражения присваивания», но они также могут упоминаться как «Именованные выражения» (например, эталонная реализация CPython использует это имя внутри).


Обычный способ присваивания значений в Python

Когда выполняется операция присваивания, Python вычисляет выражение expr, а затем связывает полученное значение с целевой ссылкой. Это связывание не зависит от типа значения, поэтому допускается связывать с переменными функции, методы, типы и други

Групповое присваивания значений в Python

Простое присваивание значений допускает использование цепочек, образованных целевыми ссылками и знаками равенства `=`.

Распаковка/упаковка последовательностей в Python.

Целевой ссылкой в операции присваивания может служить список, состоящий из двух и более ссылок, разделенных запятыми, которые могут заключаться в необязательные круглые или квадратные скобки.

Присваивание значений на месте в Python

Операция составного присваивания, другое название «присваивание на месте» отличается от простоrо присваивания тем, что вместо знака равенства `=` в ней используется составной оператор, это комбинация бинарного оператора и оператора `=`

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

Выражения присваивания введено в Python 3.8 и означает способ присваивания значения переменной в выражении. В большинстве случаев область, в которой будет привязана переменная при таком присваивании, это текущая область видимости.

Предыдущий урок: Try…Except

Этот материал посвящен инструкциям и выражениям в Python, а также отличиям между ними. Также речь пойдет об инструкциях, занимающих несколько строк, и отступах в Python. Попробуем ответить на вопрос: «Почему отступы так важны в Python?»

Что такое инструкция?

Инструкция в Python — это логическая инструкция, которую интерпретатор Python может прочесть и выполнить. Она может являться выражением или оператором присваивания в Python.

Присваивание — фундаментальный элемент этого языка. Он определяет способ создания и сохранения объектов с помощью выражений.

Что такое выражение?

Выражение — один из типов инструкции, который содержит логическую последовательность чисел, строк, объектов и операторов python. Значение и переменная являются выражениями сами по себе.

С помощью выражений можно выполнять такие операции, как сложение, вычитание, конкатенация и другие. Они также могут представлять собой вызовы функций, которые возвращают результат.


# Использование арифметических выражений
>>> ((20 + 2) * 10 / 5 - 20)
24.0

# Использование функций в выражении
>>> pow(2, 10)
1024

# Использование eval в выражении
>>> eval("2.5+2.5")
5.0

Простая операция присваивания

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


# Синтаксис
variable = expression

Рассмотрим типы выражений присваивания в Python и посмотрим, как они работают внутри.

Пример 1. С правой стороны — выражение со значением

Это базовая форма присваивания в Python.


>>> test = "Изучение python"

Python создаст в памяти строку "Изучение python" и присвоит ей имя test. Узнать адрес в памяти можно с помощью встроенной функции id().


>>> test = "Изучение python"
>>> id(test)
6589040

Номер — это адрес места, где значение хранится в памяти. Дальше несколько интересных вещей, о которых нужно помнить.

  1. Если создать другую строку с тем же значением, Python создаст новый объект и присвоит его другому местоположению в памяти. Это работает в большинстве случаев.
  2. Однако в двух следующих случаях он использует то же место в памяти:
    • Строки без пробелов с менее чем 20 символами;
    • Целые числа от -5 до 255.

Это называется интернированием и делается для сохранения памяти.

Пример 2. С правой стороны — существующая переменная Python

Теперь другой пример инструкции присваивания. С правой стороны находится ранее объявленная переменная python.

Инструкция выше не приведет к выделению нового места в памяти. Обе переменных будут ссылаться на один и тот же объект в памяти. Это как создание псевдонима для существующего объекта. Убедиться в этом можно с помощью все той же функции id().


>>> test = "Изучение python"
>>> id(test)
6589424
>>> another_test = test
>>> id(another_test)
6589424

Пример 3. С правой стороны — операция

В случае такой инструкции результат зависит от исхода операции. Возьмем такой пример.

>>> test = 2 * 2 / 4
>>> print(test)
1.0
>>> type(test)

В примере выше присваивание приведет к созданию переменной типа float. А в этом — к появлению переменной типа int.

>>> test = 3 * 3
>>> print(test)
9
>>> type(test)

Дополненная инструкция присваивания

Арифметические операторы можно объединять для формирования инструкций дополненного присваивания.

Рассмотрим такой пример: x += y. Он является аналогичным этой инструкции — x = x + y.

Следующий пример с добавлением новых элементов в кортеж чуть яснее демонстрирует принцип.


>>> my_tuple = (5, 10, 20)
>>> my_tuple += (40, 80,)
>>> print(my_tuple)
(5, 10, 20, 40, 80)

Следующий пример — список гласных. В нем в список добавляются недостающие значения.


>>> list_vowels = ['a','e','i']
>>> list_vowels += ['o', 'u',]
>>> print(list_vowels)
['a', 'e', 'i', 'o', 'u']

Инструкция в несколько строк

Каждая инструкция в Python заканчивается символом новой строки. Но это поведение можно расширить на несколько строк с помощью символа продолжения строки .

В Python есть два варианта работы с инструкциями, занимающими несколько строк.

1. Явное продолжение строки

Когда сразу используется символ продолжения строки для разбития инструкции на несколько строчек.


# Инициализация списка с помощью многострочной инструкции
>>> my_list = [1,
... 2, 3
... ,4,5
... ]
>>> print(my_list)
[1, 2, 3, 4, 5]
# Вычислить выражение, используя многострочную инструкцию
>>> eval (
... " 2.5
... +
... 3.5")
6.0

2. Неявное продолжение строки

Неявное продолжение строки работает в тех случаях, когда инструкция разбивается с помощью круглых (), квадратных [] или фигурных {} скобок. В таком случае инструкцию нужно заключить внутрь скобок для переноса.




>>> result = (10 + 100
... * 5 - 5
... / 100 + 10
... )
>>> print(result)
519.95

>>> subjects = [
... 'Метематика',
... 'Английский',
... 'Химия'
... ]
>>> print(subjects)
['Метематика', 'Английский', 'Химия']
>>> type(subjects)

Отступы в Python

Большая часть высокоуровневых языков программирования, таких как C, C++, C# используют фигурные скобки для выделения блоков кода. Python делает это с помощью отступов.

Блок кода, представляющий тело функции или цикл, начинается с отступа и заканчивается первой строкой без отступа.

Сколько места занимает один отступ

Согласно правилам стиля Python (PEP8), размер отступа — 4 символа. Однако у Google есть собственные правила, ограничивающие этот размер 2 символами. Так что можно руководствоваться и собственным стилем, но лучше следовать PEP8.

Почему отступы так важны в Python?

В большинстве языков программирования отступы используются для улучшения читабельности, но добавлять их необязательно. Но в Python их обязательно нужно использовать. Обычно каждую строку предваряет отступ в 4 символа для одного блока кода.

В примерах прошлых разделов были блоки без отступов. Однако в более сложных выражениях без них не обойтись.


def demo_routine(num):
print('Демо функция')
if num % 2 == 0:
return True
else:
return False

num = int(input('Введи число:'))
if demo_routine(num) is True:
print(num, 'четное число')
else:
print(num, 'нечетное число')

Также стоит рассмотреть пример, когда ненужный отступ вызывает ошибку.

>>>  6*5-10
File "", line 1
6*5-10
^
IndentationError: unexpected indent

Выводы

Если вы планируете стать профессиональным программистом, который верит в понятие чистого кода, тогда важно знать о выражениях, инструкциях и отступах в Python. Чтобы получить максимум от этого урока, повторите все представленные примеры в консоли. Для работы с ранее введенными командами можно использовать стрелку вверх на клавиатуре.

Далее: Комментарии

Python – невероятно функциональный и простой в изучении язык программирования, который имеет свои отличительные особенности по сравнению с другими:

  1. Кроссплатформенность. Поскольку Python – это интерпретируемый язык программирования, для многих платформ можно воспользоваться его интерпретатором. Поэтому написанные с его помощью приложения могут использоваться на самых разных устройствах.
  2. Для Python доступно большое количество средств разработки, фреймворков и сервисов. Поэтому не составит труда найти тот вариант, который подходит именно вам.
  3. Возможность подключать библиотеки, которые написаны на C. Это позволяет увеличить эффективность и быстродействие приложений, которые создаются с использованием этого языка программирования.

Но чтобы ощутить на себе все эти преимущества, необходимо понимать фундаментальные основы того, как правильно использовать этот язык программирования.

Поэтому сегодня мы более подробно поговорим о том, как правильно использовать инструкции и выражения Python. Также разберемся, в чем выражаются различия между ними. Помимо этого, мы поговорим об инструкциях, которые занимают несколько строк и об отступах в Python. Также попробуем дать ответ на вопрос, зачем отступы вообще нужны в этом языке программирования. Ведь в других же они не используются настолько часто!

Содержание

  1. Инструкции в Python
  2. Понятие инструкции
  3. Что такое выражение?
  4. Простая операция присваивания
  5. Пример 1: с правой стороны – выражение со значением
  6. Пример 2. С правой стороны – существующая переменная Python
  7. Пример 3. С правой стороны – операция.
  8. Дополнительная инструкция присваивания
  9. Инструкция в несколько строк
  10. 1. Явное продолжение строки
  11. 2. Неявное продолжение строки
  12. Реализация отступов в Python
  13. Сколько места занимает отступ?
  14. Почему без отступов в Python не получится обойтись?
  15. Выводы

Инструкции в Python

Понятие инструкции

Под инструкцией подразумевается логическое выражение, доступное для прочтения и выполнения интерпретатором Python. Он может являться выражением либо оператором присваивания в этом языке программирования.

Присваивание используется в языке повсеместно. С его помощью мы задаем метод создания и хранения объектов с использованием выражений.

Что такое выражение?

Выражение — это тип оператора, который содержит логическую последовательность чисел, строк, объектов и операторов Python. Значение и переменная сами по себе являются выражениями.

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

# Использование арифметических выражений

>>> ((20 + 2) * 10 / 5 - 20)

24.0




# Использование функций в выражении

>>> pow(2, 10)

1024




# Использование eval в выражении

>>> eval("2.5+2.5")

5.0

Простая операция присваивания

Если мы говорим о присваивании, то здесь происходит создание новых переменных, которым уже происходит присваивание значений. Переменные могут подвергаться изменениям. Инструкция предоставляет выражение и имя переменной в качестве метки для сохранения значения выражения. 

# Синтаксис

variable = expression

Давайте теперь попробуем рассмотреть стандартные типы выражений присваивания в Python и посмотрим, как устроена их работа.

Пример 1: с правой стороны – выражение со значением

Это стандартная форма присваивания в этом языке программирования. 

>>> test = "Изучение python"

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

>>> test = "Изучение python"

>>> id(test)

6589040

Номер — это адрес места, где хранится значение. Вот несколько интересных моментов, о которых следует помнить.

  1. Если вы создадите другую строку с тем же значением, Python создаст новый объект и назначит его другому месту в памяти. Это работает в большинстве случаев.
  2. Однако в следующих двух случаях он использует одну и ту же ячейку памяти:
    • Строки без пробелов, содержащие менее 20 символов;
    • Целые числа от -5 до 255.

Это называется интернированием и делается для сохранения памяти.

Пример 2. С правой стороны – существующая переменная Python

Теперь давайте приведем еще один пример, как может использоваться инструкция присваивания. С правой стороны расположена переменная Python, которая была объявлена ранее.  

Если мы используем инструкцию, приведенную выше, у нас не будет выделено дополнительное пространство в памяти. Обе переменных будут ссылаться на одинаковй объект в памяти. Это аналогично созданию псевдонима для того объекта, который уже существует. Чтобы в этом удостовериться, можно воспользоваться той же функцией id()

>>> test = "Изучение python"

>>> id(test)

6589424

>>> another_test = test

>>> id(another_test)

6589424

Пример 3. С правой стороны – операция.

А если мы используем такую инструкцию, то исход операции непосредственно влияет на ее результат. Давайте приведем такой пример для наглядности. 

>>> test = 2 * 2 / 4

>>> print(test)

1.0

>>> type(test)

В этом примере присваивание приведет к созданию переменной типа float. А в этом – к появлению переменной типа int

>>> test = 3 * 3

>>> print(test)

9

>>> type(test)

Дополнительная инструкция присваивания

Арифметические операторы можно комбинировать для формирования инструкций дополненного присваивания.

Рассмотрим этот пример: x + = y. Это похоже на эту инструкцию — x = x + y.

Следующий пример, добавляющий новые элементы в кортеж, немного более наглядно демонстрирует принцип. 

>>> my_tuple = (5, 10, 20)

>>> my_tuple += (40, 80,)

>>> print(my_tuple)

(5, 10, 20, 40, 80)

А этот пример – это список, элементами которого являются гласные буквы английского алфавита. В нем в список добавляются недостающие значения. 

>>> list_vowels = ['a','e','i']

>>> list_vowels += ['o', 'u',]

>>> print(list_vowels)

['a', 'e', 'i', 'o', 'u']

Инструкция в несколько строк

Каждая инструкция Python оканчивается знаком новой строки. Но это действие можно расширить до нескольких строк, используя символ .

В Python есть два способа обработки операторов, занимающих несколько строк.

1. Явное продолжение строки

Когда символ продолжения строки сразу используется для разделения инструкции на несколько строк. 

# Инициализация списка с помощью многострочной инструкции

>>> my_list = [1, 

... 2, 3

... ,4,5 

... ]

>>> print(my_list)

[1, 2, 3, 4, 5]

# Вычислить выражение, используя многострочную инструкцию

>>> eval ( 

... " 2.5 

... + 

... 3.5")

6.0

2. Неявное продолжение строки

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

>>> result = (10 + 100

... * 5 - 5

... / 100 + 10

... )

>>> print(result)

519.95

 

>>> subjects = [

... 'Математика',

... 'Английский',

... 'Химия'

... ]

>>> print(subjects)

['Математика', 'Английский', 'Химия']

>>> type(subjects)

Реализация отступов в Python

Как правило, почти любой высокоуровневый язык программирования использует фигурные скобки для того, чтобы выделять определенные блоки кода. Это заставляет совершать лишние действия, прописывать эти скобки. Суммарно это занимает довольно немало времени, даже если эти действия были доведены до автоматизма. Чтобы сделать код более простым для чтения и написания, в Python разработчики решили отказаться от использования скобок для выделения какого-то фрагмента кода. А их роль стали выполнять отступы.

Тот блок кода, который играет роль тела функции либо цикла, начинается с отступа и завершается первой строкой, в которой отступа нет.

Сколько места занимает отступ?

Общепринятый размер отступа в Python – 4 символа. Правда, Google предусматривает немного другие правила: размер может ограничиваться исключительно двумя символами. Таким образом, можно руководствоваться и собственным стилем, задавая такое количество отступов, которое нужно. Тем не менее, рекомендуется все же делать четыре отступа.

Почему без отступов в Python не получится обойтись?

Большинство языков программирования используют отступы для улучшения читабельности, но добавлять отступы необязательно. Но в Python их обязательно надо применять, в этом особенность данного языка программирования. Обычно каждой строке предшествует 4-символьный отступ для блока кода.

В примерах из предыдущих разделов были блоки без отступов. Однако в более сложных выражениях без них не обойтись. 

def demo_routine(num):

    print('Демо функция')

    if num % 2 == 0:

        return True

    else:

        return False




num = int(input('Введи число:'))

if demo_routine(num) is True:

    print(num, 'четное число')

else:

    print(num, 'нечетное число')

А также давайте рассмотрим пример случая, когда ненужный отступ приводит к возникновению ошибки. 

>>>  6*5-10

File "", line 1

    6*5-10

    ^

IndentationError: unexpected indent

Выводы

Таким образом, стать профессиональным разработчиком на Python без понимания того, что собой являют выражения, инструкции и отступы, нельзя. Чтобы получить максимум от этого урока, рекомендуется самостоятельно прописать весь код в консоли. Чтобы работать с командами, которые были указаны ранее, можно использовать клавишу «стрелка вверх» на клавиатуре.

В целом, Python – это понятный и простой язык программирования, который можно легко использовать как новичкам, так и опытным специалистам. Нет необходимости изучать сложный синтаксис. Да и использование отступов все же упрощает код, по сравнению с фигурными скобками. 

Все это позволяет добиться высокой скорости написания программ. Конечно, сами по себе приложения, написанные на Python, не настолько быстры по сравнению с теми, которые были написаны на других языках. Но чтобы научиться программировать, нужно начинать с простого. И видим, что такие аспекты разработки, как выражения, инструкции и отступы, вовсе не так сложны в понимании, как может показаться на первый взгляд.

Оцените качество статьи. Нам важно ваше мнение:

Понравилась статья? Поделить с друзьями:
  • Rg108 max квадрокоптер инструкция на русском языке
  • Бромгексин оболенское таблетки инструкция по применению цена
  • Ролик кварцевый для лица инструкция по применению
  • Nespresso vertuo next gcv1 инструкция на русском
  • Амброгексал сироп инструкция по применению цена взрослым