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

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

PEP 572

Гвидо ван Россум на протяжении долгого времени выполнял центральную роль в принятии решений о развитии Python. Он фактически в одиночку определял, как будет развиваться Python: изучал обратную связь от пользователей, а потом лично отбирал изменения, которые войдут в следующую версию языка. За это коллеги Гвидо придумали для него полуюмористическую должность "великодушного пожизненного диктатора" проекта Python.

В 2018 году Гвидо объявил об уходе с этой позиции. Причиной такого решения стал документ PEP 572, который вводит в язык выражения присваивания — моржовый оператор. Этот документ вызвал ожесточенные споры в сообществе Python. Многие программисты считали, что идеи, представленные в нем, противоречат философии языка и отражают, скорее, личное мнение Гвидо ван Россума, чем передовые практики в отрасли. Например, некоторым разработчикам не нравился сложный и неочевидный синтаксис оператора :=. Однако Гвидо все равно утвердил PEP 572, и в Python 3.8 появился моржовый оператор.

После публикации документа пользователи Python писали Гвидо ван Россуму множество негативных отзывов. Он был ошеломлен количеством комментариев, которые получил в ответ на принятие PEP 572. В конце концов Гвидо решил покинуть свой пост. Он отправил своим коллегам письмо, в котором написал: "Я больше не хочу когда-либо сражаться за PEP и видеть, как множество людей презирают мои решения". Полный текст письма Гвидо доступен по ссылке.

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

Что же представляет собой моржовый оператор? Где он может оказаться полезным? Почему внедрение моржового оператора вызвало у некоторых участников сообщества Python негативную реакцию? Давайте поговорим об этом подробно.

Оператор :=

Итак, моржовый оператор появился в Python 3.8 и дает возможность решить сразу две задачи (выполнить два действия):

  1. присвоить значение переменной

  2. вернуть это значение

Базовый синтаксис использования оператора := следующий:

variable := expression

Сначала выполняется выражение expression, а затем значение, полученное в результате выполнения этого выражения, присваивается переменной variable, после чего это значение будет возвращено.

Кстати, оператор := часто называют моржовым, потому что он похож на глаза и бивни моржа.

Отличие оператора := от оператора =

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

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

num = 7
print(num)

Однако при использовании оператора := данный код можно сократить до одной строчки:

print(num := 7)

Значение 7 присваивается переменной num, а затем возвращается и становится аргументом для функции print().

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

Приведенный ниже код:

print(num = 7)

приводит к возбуждению исключения:

TypeError: 'num' is an invalid keyword argument for print()

Полезные сценарии использования моржового оператора

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

Пример 1. Необходимо вывести информацию о ключевых словах Python, длина которых больше пяти символов.

Приведенный ниже код:

from keyword import kwlist

for word in kwlist:
    if len(word) > 5:
        print(f'В ключевом слове {word} всего {len(word)} символов.')

выводит:

В ключевом слове assert всего 6 символов.
В ключевом слове continue всего 8 символов.
В ключевом слове except всего 6 символов.
В ключевом слове finally всего 7 символов.
В ключевом слове global всего 6 символов.
В ключевом слове import всего 6 символов.
В ключевом слове lambda всего 6 символов.
В ключевом слове nonlocal всего 8 символов.
В ключевом слове return всего 6 символов.

Проблема этого кода заключается в том, что значение длины ключевого слова (len(word)) вычисляется дважды: один раз в условном операторе, второй — при выводе текста. Решить проблему можно с помощью дополнительной переменной:

from keyword import kwlist

for word in kwlist:
    n = len(word)
    if n > 5:
        print(f'В ключевом слове {word} всего {n} символов.')

или с помощью оператора :=:

from keyword import kwlist

for word in kwlist:
    if (n := len(word)) > 5:
        print(f'В ключевом слове {word} всего {n} символов.')

Обратите внимание на то, что в данном случае выражение (n := len(word)) нужно обязательно заключать в скобки.

Приведенный ниже код:

from keyword import kwlist

for word in kwlist:
    if n := len(word) > 5:
        print(f'В ключевом слове {word} всего {n} символов.')

выводит:

В ключевом слове assert всего True символов.
В ключевом слове continue всего True символов.
В ключевом слове except всего True символов.
В ключевом слове finally всего True символов.
В ключевом слове global всего True символов.
В ключевом слове import всего True символов.
В ключевом слове lambda всего True символов.
В ключевом слове nonlocal всего True символов.
В ключевом слове return всего True символов.

Оператор :=, как и оператор =, имеет наименьший приоритет перед всеми остальными встроенными операторами, поэтому выражение n := len(word) > 5 равнозначно n := (len(word) > 5), что в контексте истинного условия равнозначно n := True.

Пример 2. На вход поступает произвольное количество слов. Необходимо добавлять эти слова в список до тех пор, пока не будет введено значение stop. Приведенный ниже код решает эту задачу:

words = []
word = input()

while word != 'stop':
    words.append(word)
    print(f'Значение {word!r} добавлено в список.')
    word = input()

Проблема этого кода заключается в том, что нам приходится объявлять переменную word перед циклом для первой итерации, тем самым дублируя строку кода word = input().

С использованием оператора := приведенный выше код можно записать в виде:

words = []
while (word := input()) != 'stop':
    words.append(word)
    print(f'Значение {word!r} добавлено в список.')

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

Приведенный ниже код:

with open('input.txt', 'r') as file:
    line = file.readline().rstrip()
    while line:
        print(line)
        line = file.readline().rstrip()

с использованием оператора := можно записать в виде:

with open('input.txt', 'r') as file:
    while line := file.readline().rstrip():
        print(line)

Пример 3. Нам доступен список чисел, на основе которого необходимо создать новый список, элементами которого будут факториалы чисел исходного списка, при этом только те, которые не превышают 1000.

Приведенный ниже код:

from math import factorial

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
new_data = [factorial(x) for x in data if factorial(x) <= 1000]
print(new_data)

выводит:

[1, 2, 6, 24, 120, 720]

Проблема этого кода заключается в том, что факториал каждого числа вычисляется дважды: один раз в условном операторе, второй — при записи в список. Решить проблему можно с помощью оператора :=.

Приведенный ниже код:

from math import factorial

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
new_data = [fact for num in data if (fact := factorial(num)) <= 1000]
print(new_data)

выводит:

[1, 2, 6, 24, 120, 720]

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

Приведенный ниже код:

users = [
{'name': 'Timur Guev', 'occupation': 'python generation guru'},
{'name': None, 'occupation': 'driver'},
{'name': 'Anastasiya Korotkova', 'occupation': 'python generation bee'},
{'name': None, 'occupation': 'driver'},
{'name': 'Valeriy Svetkin', 'occupation': 'python generation bee'}
]

for user in users:
    name = user.get('name')
    if name is not None:
        print(f'{name} is a {user.get("occupation")}.') 

выводит:

Timur Guev is a python generation guru.
Anastasiya Korotkova is a python generation bee.
Valeriy Svetkin is a python generation bee.

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

С использованием оператора := приведенный выше код можно записать в виде:

users = [
{'name': 'Timur Guev', 'occupation': 'python generation guru'},
{'name': None, 'occupation': 'driver'},
{'name': 'Anastasiya Korotkova', 'occupation': 'python generation bee'},
{'name': None, 'occupation': 'driver'},
{'name': 'Valeriy Svetkin', 'occupation': 'python generation bee'}
]

for user in users:
    if (name := user.get('name')) is not None:
        print(f'{name} is a {user.get("occupation")}')

Здесь мы используем оператор := для присваивания значения переменной name внутри условного оператора, что позволяет сократить количество строк кода и сделать его более читабельным.

Пример 5. Нам доступна произвольная строка, в которой необходимо найти совпадение с определенным шаблоном. Если совпадение не найдено, необходимо найти совпадение со вторым шаблоном. Если совпадение снова не найдено, необходимо вывести текст Нет совпадений.

Приведенный ниже код:

import re

text = 'Поколение Python — это серия курсов по языку программирования Python от команды BEEGEEK'
pattern1 = r'beegeek'
pattern2 = r'Python'

m = re.search(pattern1, text)
if m:
    print(f'Найдено совпадение с первым шаблоном: {m.group()}')
else:
    m = re.search(pattern2, text)
    if m:
        print(f'Найдено совпадение со вторым шаблоном: {m.group()}')
    else:
        print('Нет совпадений')

выводит:

Найдено совпадение со вторым шаблоном: Python

С использованием оператора := приведенный выше код можно записать в виде:

import re

text = 'Поколение Python — это серия курсов по языку программирования Python от команды BEEGEEK'
pattern1 = r'beegeek'
pattern2 = r'Python'

if m := re.search(pattern1, text):
    print(f'Найдено совпадение с первым шаблоном: {m.group()}')
else:
    if m := re.search(pattern2, text):
        print(f'Найдено совпадение со вторым шаблоном: {m.group()}')
    else:
        print('Нет совпадений')

Пример 6. Нам доступен список чисел. Необходимо решить две задачи:

  • выяснить, является ли хотя бы одно число из списка больше числа 10

  • выяснить, являются ли все числа из списка меньше числа 10

Приведенный ниже код:

numbers = [1, 4, 6, 2, 12, 4, 15]

print(any(number > 10 for number in numbers))
print(all(number < 10 for number in numbers))

выводит:

True
False

Оператор := в этом случае позволит сохранить значение, на котором закончилось выполнение функций any() и all().

Приведенный ниже код:

numbers = [1, 4, 6, 2, 12, 4, 15]

print(any((value := number) > 10 for number in numbers))
print(value)

print(all((value := number) < 10 for number in numbers))
print(value)

выводит:

True
12
False
12

Подводные камни

Как видно из примеров выше, оператор := может оказаться весьма полезен в различных сценариях. Однако при его использовании можно столкнуться с некоторыми непредвиденными ситуациями, одна из которых представлена в первом примере, где необходимо правильно расставить скобки. Рассмотрим и другие ситуации.

В третьем примере показана возможность использования оператора := в списочном выражении. Однако переменная остается доступна и после создания списка, поэтому можно случайно перезаписать одноименную переменную в объемлющей или глобальной области видимости.

Приведенный ниже код:

from math import factorial

fact = 0
print(fact)

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
factorial_data = [fact for num in data if (fact := factorial(num)) <= 1000]
print(fact)

выводит:

0
3628800

В шестом примере показана возможность использования оператора := в функциях all() и any(). Но если список окажется пуст, переменная не будет создана, что приведет к возбуждению исключения NameError.

Приведенный ниже код:

numbers = []

print(any((value := number) > 10 for number in numbers))
print(value)

приводит к возбуждению исключения:

NameError: name 'value' is not defined. Did you mean: 'False'?

С похожей ситуацией можно столкнуться при проверке нескольких условий. Например, мы хотим узнать, какие числа в диапазоне от 1 до 100 делятся на 2, 3 или 6 без остатка.

Приведенный ниже код:

for i in range(1, 101):
    if (two := i % 2 == 0) and (three := i % 3 == 0):
        print(f"{i} делится на 6.")
    elif two:
        print(f"{i} делится на 2.")
    elif three:
        print(f"{i} делится на 3.")

приводит к возбуждению исключения:

NameError: name 'three' is not defined

Проблемой этого кода является то, что если выражение (two := i % 2 == 0) является ложным, выражение (three := i % 3 == 0) не выполнится и переменная three не будет создана, в результате чего будет возбуждено исключение NameError.

Злоупотребление оператором := может привести к ошибкам и ухудшению читабельности кода, поэтому не следует использовать его при любом удобном случае, а только тогда, когда это действительно необходимо. 

Подведем итоги

Как мы видим, в некоторых ситуациях с помощью моржового оператора можно написать лаконичный и более производительный код с точки зрения вычислений. Однако использовать моржовый оператор стоит аккуратно. Не следует внедрять его в код при каждом удобном случае. Применяйте оператор := только для несложных выражений, чтобы ваш код не терял читабельность.

Присоединяйтесь к нашему телеграм-каналу, будет интересно и познавательно!

❤️ Happy Pythoning! ?

Комментарии (31)


  1. HemulGM
    31.07.2024 07:08
    +24

    Когда ожидается введение begin/end?


    1. abramov_a
      31.07.2024 07:08
      +2

      Скорее всего никогда))) Отступы - наше все.


      1. dyadyaSerezha
        31.07.2024 07:08
        +3

        Велик Питон, а отступать некуда! (с) Ну или почти так)


        1. RichardMerlock
          31.07.2024 07:08
          +3

          Вправо! Вправо отступайте!


  1. gmist
    31.07.2024 07:08
    +23

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


    1. gerod
      31.07.2024 07:08
      +2

      вот я тоже не понял, а зачем? только ради сокращения пары строк кода?


    1. greenfork
      31.07.2024 07:08
      +1

      Почему у вас наступает эффект замедления чтения кода при виде моржового оператора?


      1. Romano
        31.07.2024 07:08

        Это очевидно даже из примеров в статье - усложнение кода одной строки.


        1. greenfork
          31.07.2024 07:08
          +1

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

          1. Усложнение кода одной строки

          2. Выражение мысли в двух строках

          Для меня совершенно не очевидно, что прочитать две "простые" строки будет быстрее, чем одну "сложную".


    1. Dolby
      31.07.2024 07:08

      Бывает накрывает желание в перфекционизм удариться, сделать код более красивым, эффективным и лаконичным, вот тут морж иногда бывает уместен


  1. Fen1kz
    31.07.2024 07:08
    +8

    Интересно, в js такое поведение изначально с = и это считается очень плохим стилем, практически нигде кроме "хакерских однострочников" не вижу


    1. alienator
      31.07.2024 07:08
      +2

      Это не только в JS, это во многих языках, потому что унаследовано от C, в котором assignment is expression с самого начала. Но и C не был первым, в Algol 68 оно точно было, и даже в моржовом виде. Возможно, было в каких-то расширениях Fortran, не уверен. С большой натяжкой можно и Lisp упомянуть.


      1. ahabreader
        31.07.2024 07:08
        +1

        Можно отметить две вещи в C: возможность "опасного" присваивания внутри if (плохо) и возможность объявления, в нём нет места для ошибки (хорошо):

        if (x = foo()) {...}      // точно ли нет ошибки? '=' или '=='?
        
        if (int x = foo()) {...}  // точно нет

        Из этих двух вещей в одних языках унаследовали только хорошую (D), в других - только плохую (JS).


        1. ahabreader
          31.07.2024 07:08

          и возможность объявления

          Тьфу, это же только в C++ так. "A condition can either be an expression or a simple declaration."

          https://en.cppreference.com/w/cpp/language/if

          https://en.cppreference.com/w/c/language/if


    1. funca
      31.07.2024 07:08
      +1

      В условиях часто ошибаются, когда пишут = (присваивание) вместо == (сравнение):

      if (x = 1) { /* всегда TRUE */}

      if (x == 1) {}

      Поэтому Гвидо в питоне с самого начала запретил использовать присваивание в подобных случаях.

      Моржовый оператор содержит дополнительный символ := из-за чего вероятность допустить такую ошибку существенно меньше. Но в питоне он не вписывается в общий дизайн языка и выглядит как костыль.


      1. dyadyaSerezha
        31.07.2024 07:08
        +1

        Эти ошибочные условия ловятся всеми компиляторами уже лет 20 как.


        1. Dozer88
          31.07.2024 07:08

          Не все компиляторы, и не всегда ошибки. Например, на сях нет исключений, и на каждой строчке писать if ((res = f()) != ok) return res; намного лаконичнее, чем res = f(); if (res != ok) return res; в две строчки... Хотя я тоже не люблю такие конструкции...


          1. dyadyaSerezha
            31.07.2024 07:08

            На сях как раз и начали их ловить раньше всех. Выводится warning, а дальше уже сам.


  1. sl_1me
    31.07.2024 07:08
    +1

    Хоть и оператор не нашёл большого применения, но читать статьи о нём и его истории очень интересно!


  1. AlexeyK77
    31.07.2024 07:08

    Нужно быть готовым в скором будущем к такому коду
    num := 7

    print(num)

    Потому что так тоже можно написать и это станет источником путаницы. Либо моржа надо было как-то ограничить в применении, что бы он не вытеснил обычное =, либо вообще не вводить, т.к. появляется целый лишний оператор ради некритических сценариев.


    1. ValeryIvanov
      31.07.2024 07:08
      +1

      А так и нельзя писать

      >>> a := 123
        File "<stdin>", line 1
          a := 123
            ^^
      SyntaxError: invalid syntax
      


      1. AlexeyK77
        31.07.2024 07:08
        +7

        отлично! Получается что ввели целый оператор ради одного краевого случая. как по мне не лучшая это практика, плодить операторы.


    1. funca
      31.07.2024 07:08
      +3

      y := f(x) # INVALID

      (y := f(x)) # Valid, though not recommended

      По идее, нужно было делать с самого начала как в первом варианте. Но поезд ушел.

      Самая жесть теперь когда скобки меняют семантику:

      >>> f'{(x:=10)}' # Valid, uses assignment expression

      '10'

      >>> x = 10

      >>> f'{x:=10}' # Valid, passes '=10' to formatter

      ' 10'

      После такого Гвидо правильно сделал, что самовыпилился. Это уже маразм.


      1. ahabreader
        31.07.2024 07:08

        По идее, нужно было делать с самого начала как в первом варианте

        Так не делают со времён Алгола-68, наверное. Потому что -:=,+:=,*:=,/:=,%:=...

        Но была и другая возможность - переиспользовать as.


      1. AlexeyK77
        31.07.2024 07:08

        зато появился отличный вопрос валить джунов (а может и не только джунов) на собеседовании простым вопросом по операторам языка ;)


  1. icya
    31.07.2024 07:08
    +6

    >>> with open("./words.txt", "r") as file:
    ...     while line := file.readline().rstrip():
    ...             print(line)
    ... 
    foo
    bar
    >>> with open("./words.txt", "r") as file:
    ...     for line in file.readlines():
    ...             print(line.rstrip())
    ... 
    foo
    bar
    
    baz

    В примере 2 (чтение строк из файла), реализация через моржовый оператор завершает чтение на первой пустой строке


  1. HyperWin
    31.07.2024 07:08
    +1

    Я использовал его разок, очень удобно в цикле, чтобы работал пока значение из функции не равно None


  1. Megadeth77
    31.07.2024 07:08
    +1

    Впилили бы что то вроде using во включения, а то извращаться приходится

    a = [foo: (bar,baz) for foo in arr if (bar:="qq", baz:="ww")]


  1. LF69ssop
    31.07.2024 07:08
    +5

    Какие-то надуманные проблемы. Как по мне, лучше десяток лишних строчек чем дублирующий оператор языка.

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


    1. Dynasaur
      31.07.2024 07:08

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


  1. johngetman
    31.07.2024 07:08

    офигенная вещь, я конечно не очень люблю пайтон но конкретно моржовый оператор это оч годная штука, всем кому она не нравится советую пересесть на луа, там вообще ничего лишнего нет