List Comprehension — пояснение для тех, кто никак не может понять

Перевод статьи «If You Find if..else in List Comprehensions Confusing, Read This, Else…».

Я долго шел путем проб и ошибок, вместо того чтобы разобраться. При написании list comprehension (русск. «списковое включение», «представление списка») я никак не мог запомнить, идет ли одиночное if перед for, а комбинация if..else после него, или наоборот (спойлер: наоборот). Но теперь всё будет иначе. Теперь мне всё совершенно ясно. И вам тоже будет ясно, когда прочитаете!

О чем речь?

Что же меня так долго путало? Начну с простого примера. Допустим, у вас есть список имен, и вы хотите оставить только те имена, которые состоят из более чем пяти букв. Это можно сделать с помощью следующего list comprehension:

names = ["Jim", "Audrey", "Clara", "Aaron", "Ishaan", "Amelia"]
 ​
long_names = [name for name in names if len(name) > 5]
 ​
print(long_names)
# OUTPUT: ['Audrey', 'Ishaan', 'Amelia']

Это представление списка состоит из трех частей, стоящих в определенном порядке:

  1. Переменная с именем name.
  2. Цикл for для перебора списка names.
  3. Часть с if для определения, какие имена сохранить.

Но что, если вместо того, чтобы полностью отбрасывать более короткие имена, вы хотите заменить их плейсхолдером?

names = ["Jim", "Audrey", "Clara", "Aaron", "Ishaan", "Amelia"]
 ​
long_names = [name if len(name) > 5 else "?" for name in names]
 ​
print(long_names)
# OUTPUT: ['?', 'Audrey', '?', '?', 'Ishaan', 'Amelia']

Порядок частей в list comprehension теперь другой:

  1. Переменная с именем name [некоторые вещи неизменны].
  2. Часть с if..else для определения, какие имена сохранить, а какие заменить.
  3. Часть с for для перебора списка names.

Заметили разницу? Напоминает неуловимое движение фокусника. Вот оба представления списка, показанные вместе, с дополнительными пробелами, не предусмотренными PEP 8, только для улучшения видимости:

[ name     for name in names             if len(name) > 5  ]
[ name     if len(name) > 5 else "?"     for name in names ]

Во втором случае if..else предшествовало for.

Бессмыслица какакя-то, верно? У вас есть два варианта:

  1. Смириться и просто запомнить две версии.
  2. Читать дальше.

Анатомия list comprehension (сокращенная версия)

Вот что говорит наш источник мудрости, то есть документация Python: «List Comprehension состоит из одного выражения, за которым следует как минимум одна операторная часть for и ноль или более операторных частей for или if».

Начнем с первого представления списка, все еще с несоответствующими пробелами:

[ name     for name in names     if len(name) > 5  ]

Перечитайте фразу из документации. Вот три части:

  1. Состоит из одного выражения… — идентификатор name, который мы часто называем переменной, соответствует этому описанию.
  2. …за которым следует как минимум одна операторная часть for — в этом примере есть одно условие for, что соответствует части «как минимум одна».
  3. … и ноль или более операторных частей for или if — в этом представлении списка есть один оператор if, что удовлетворяет этой части описания.

А как насчет второго представления списка, того, что с if..else?

[ name     if len(name) > 5 else "?"     for name in names ]

Не торопитесь. Можете ли вы как-то соотнести его с описанием представления списка из документации?

Нет?

Это потому, что лишние пробелы, которые я добавил, неправильные. (Напоминаю, что я добавляю эти лишние пробелы, чтобы облегчить обсуждение — не делайте этого в реальном коде, иначе вы получите выговор от PEP 8!)

Вот тот же list comprehension с другим выделением значимых частей:

[ name if len(name) > 5 else "?"       for name in names ]

В этом представлении списка есть только две части:

  1. Состоит из одного выражения… – да, name if len(name) > 5 else "?" – это одно выражение. Подробнее об этом чуть позже.
  2. …за которым следует как минимум одна операторная часть for – здесь есть одно условие for, так что все в порядке.
  3. … и ноль или более операторных частей for или if — в нашем list comprehension больше ничего не осталось, но это все равно удовлетворяет условию благодаря слову «ноль»!

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

Это одно выражение:

name if len(name) > 5 else "?"

Это условное выражение. Нет, нет, я не имел в виду условный оператор, это нечто другое.

Запутались? Я лишь недавно перестал всякий раз заново подсматривать разницу между оператором и выражением, когда мне нужно было использовать эти термины. Итак, небольшое отступление перед возвращением к list comprehension.

Операторы и выражения

Определения — это скучно, я знаю. (Признаюсь: я люблю определения, но никому не говорите.) Я буду краток.

Выражение (англ. expression) может быть оценено как значение. Вот простой способ определить, является ли что-то выражением: напишите его в строке REPL и нажмите Enter/Return. Выводится ли значение? Если да, то это выражение. Вот несколько примеров выражений:

5
# 5
1 + 1
# 2
int("5")
# 5
max(2, 5, 10)
# 10
10 == 5 + 5
# True
# # The next one assumes an import 'statement' first
random.randint(1, 10)
# 7
"hello" or []
# 'hello'

Все они возвращают значения. Следовательно, все они являются выражениями. Если вы можете поместить нечто после знака равенства в присваивании, то это выражение.

Оператор (англ. statement) — это более общее описание строки кода, поскольку операторы могут не оцениваться в какое-либо значение. Например, for _ in range(10): не оценивается в значение, как и while True: или if name == "Stephen".

Оператор присваивания является оператором:

my_name = "Stephen"

Часть после знака равенства является выражением, но вся строка присваивания является оператором.

А в последних версиях Python есть также выражение присваивания, часто называемое моржовым оператором:

my_name := "Stephen"

Это оценивается в значение:

final_var = (my_name := "Stephen")
my_name
# 'Stephen'
final_var
# 'Stephen'

Все выражения являются операторами. Но не все операторы являются выражениями.

Но я отвлекся. Вернёмся к основной теме этой статьи.

Условное выражение (тернарный оператор)

Если вы читаете это, то вы знакомы с условными операторами. Это одна из первых вещей, которые вы изучаете, например, if name == "Stephen". И вы можете строить блоки с помощью операторов if, elif и else. Но вы все это уже знаете.

Это операторы. Они не вычисляются в значение. Их нельзя поместить в правую часть знака равенства в присваивании.

Но есть еще и условное выражение:

name = "Stephen"
name if len(name) > 5 else "?"
# 'Stephen'

name = "Bob"
name if len(name) > 5 else "?"
# '?'

Это выражение, поскольку оно вычисляется в значение. Возвращаемое значение — это либо то, что находится перед if, либо то, что находится после else, в зависимости от того, оценивается ли часть в середине как True или False.

А поскольку это выражение, при желании вы можете присвоить его значение имени переменной:

name = "Stephen"
final_result = name if len(name) > 5 else "?"
final_result
# 'Stephen'

name = "Bob"
final_result = name if len(name) > 5 else "?"
final_result
# '?'

Это условное выражение часто называют тернарным оператором. Тернарный оператор — это любой оператор, который требует трех операндов, так же, как бинарный оператор требует двух операндов.

В Python есть много бинарных операторов, таких как ==, >, or, and, is и другие. Но есть только один тернарный оператор — условное выражение:

<первый операнд> if <второй операнд> else <третий операнд>

Итак, вернёмся к list comprehension:

[ name if len(name) > 5 else "?"       for name in names ]

«Одно выражение», необходимое для представления списка, — это условное выражение (тернарный оператор!). Вот оно:

name if len(name) > 5 else "?"

Три операнда в этом тернарном операторе:

  1. name
  2. len(name) > 5
  3. "?"

Все три являются самостоятельными выражениями!

Описание представления списка требует как минимум одного условия for, которое у вас есть. Последняя часть описания представления списка понимания требует «ноль или более операторов for или if»; в этом представлении списка имеем «ноль».

В общем, все логично. (Важный совет: если вам кажется, что что-то в Python не логично, вам, вероятно, следует копнуть глубже. Тогда вы поймете.)

Теперь вам больше не понадобится метод проб и ошибок, чтобы запомнить, в каком порядке все идет в представлении списка.

Прокрутить вверх