Рубрики
Без рубрики

Dead Simple Python: список понимания и выражения генераторов

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

Dead Simple Python (13 серии деталей)

Если бы мне пришлось выбрать любимую особенность Python, это должно быть Список понимания, руки вниз. На мой взгляд, они инкапсулируют саму сущность «питонического» кода … что иронично, поскольку они на самом деле заимствованы у Хаскелла.

Я был так взволнован, чтобы добраться до них; Это статья I запланированный При написании нескольких недель назад, Но я понял, что понимание итераторов будет необходимым для по -настоящему грокинга понимания списка и их потенциала.

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

Выражения генератора это краткий способ создания контейнеров. Чаще всего вы услышите о Список понимания , но Установите понимание и Дикта понимания также существуют. Разница в терминах несколько важна, однако: это только Понимание списка Если вы на самом деле составляете список.

A выражение генератора окружен скобкой () , в то время как Понимание списка Окружен квадратными скобками [ ] . Установите понимание прилагаются в кудрявые брекеты {} Анкет Помимо этих различий, синтаксис идентичен во всех трех случаях!

(В понимании дикта есть немного больше, о котором мы поговорим позже.)

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

Выражение генератора – выражение, которое возвращает итератор.

Выражение генератора (или понимание списка/установки) немного похоже на для петля, которая была перевернута.

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

temps_f = [67.0, 72.5, 71.3, 78.4, 62.1, 80.6]
temps_c = []

def f_to_c(temp):
    return round((temp - 32) / 1.8, 1)

for c in map(f_to_c, temps_f):
    temps_c.append(c)

print(temps_c)

Хотите верьте, хотите нет, но понимание списка позволит мне сократить всю эту программу до трех строк! Мы упростим его по одному кусочку за раз, чтобы вы могли видеть, что я имею в виду.

Начнем с замены цикла для понимания списка …

temps_f = [67.0, 72.5, 71.3, 78.4, 62.1, 80.6]

def f_to_c(temp):
    return round((temp - 32) / 1.8, 1)

temps_c = [f_to_c(temp) for temp in temps_f]

print(temps_c)

Важная линия – temps_c = [f_to_c (temp) для температуры в temps_f] Анкет Это ведет себя очень похоже на map () делает. Для каждого элемента темп В списке temps_f , мы применяем функцию f_to_c () Анкет

Теперь, если бы мне понадобилось это f_to_c () Функция в другом месте, я бы действительно остановился здесь и назвал бы это хорошим. Однако, если бы это было единственное место, где мне нужна была логика Fahrenheit-to-Celsius, я мог бы вообще избежать функции и перенести логику непосредственно в понимание:

temps_f = [67.0, 72.5, 71.3, 78.4, 62.1, 80.6]
temps_c = [round((temp-32) / 1.8, 1) for temp in temps_f]
print(temps_c)

Что я тебе сказал? Три строки!

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

Представьте себе, что у вас есть программа, которая получает кучу целых чисел на одной линии, разделенной такими пространствами, как 5 4 1 9 5 7 5 Анкет Вы хотите найти сумму всех этих целых чисел. (Ради простоты предположим, что у вас нет риска плохого ввода.)

Давайте начнем с написания этого очевидного способа, без Понимание списка.

user_input = input()
values = user_input.split(' ')

total = 0

for v in values:
    n = int(v)
    total += n

print(total)

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

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

values = input().split(' ')
total = 0

for v in values:
    total += int(v)

print(total)

Мы не можем стать намного проще, если мы не используем понимание списка, так что давайте сделаем это сейчас!

values = input().split(' ')
total = sum(int(v) for v in values)
print(total)

Выражение генератора здесь (int (v) для V в значениях) . Для каждого значения V в списке Значения , мы бросаем его в целое число ( int (v) ).

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

Теперь, если мне не понадобилось ценности Список для чего -либо еще, я мог бы на самом деле переместить эту логику прямо в выражение генератора!

total = sum(int(v) for v in input().split(' '))
print(total)

Легко, как пирог, верно?

Вложенные списки понимания

Что если вместо этого мы хотели сумму квадраты каждого введенного числа? На самом деле есть два способа сделать это. Очевидный вариант – сделать это:

total = sum(int(v)**int(v) for v in input().split(' '))
print(total)

Это работает, но каким -то образом это просто чувствует себя неправильно, не так ли? Мы кастингом V в целое число дважды Анкет

Мы можем обойти это гнездование Понимание списка в нашем выражении генератора!

total = sum(n**2 for n in [int(v) for v in input().split(' ')])

Оцениты в список понимания и выражения генераторов внутренний к внешний . Внутреннее выражение, int (v) для v in input (). split ('') , запускается первым, и входные квадратные кронштейны [] преобразовать это в список (итерабильный).

Далее, внешнее выражение, n ** 2 для n в [списке] запускается, где [Список] это тот список, который мы сгенерировали мгновение назад.

Это может быть легко для этого гнездования уйти от вас. Я пытаюсь использовать это экономно. Когда мне нужно гнездовать, я пишу каждое понимание списка на отдельной строке и храню …

the_list = [int(v) for v in input().split(' ')]
total = sum(n**2 for n in the_list)
print(total)

… Проверьте это, а затем начните гнездовать через копию и вставьте.

Условия в выражениях генераторов

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

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

the_list = [int(v) for v in input().split(' ')]
total = sum(n**2 for n in the_list if n%2==0)
print(total)

Новая часть находится на второй строке. В конце выражения генератора я добавил Если Анкет Вы можете распознать оператор Modulo ( % ), который дает нам остаток разделения. Любое равномерное число делится на 2 , то есть у него не будет остаться. Таким образом, n%2 == 0 верно только для даже числа

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

output = []
for n in the_list:
    if n%2==0:
        output.append(n**2)

По сути, чтобы превратить это в выражение генератора, вы просто захватываете логику внутри append () , припарковать его впереди …

n**2
for n in the_list:
    if n%2==0:

… и затем удалите колонны ( : ), разрывы строки и отступа из для и Если заявления…

n**2 for n in the_list if n%2==0

Несколько иеры

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

Рассмотрим следующую логику:

num_a = [1, 2, 3, 4, 5]
num_b = [6, 7, 8, 9, 10]
output = []

for a in num_a:
    for b in num_b:
        output.append(a*b)

Мы можем выполнить те же шаги, которые я дал минуту, чтобы превратить это в понимание списка! Мы приносим аргумент для append () впереди …

a*b
for a in num_a:
    for b in num_b:

… А потом мы разрушаем остальное на одну линию, удаляя колоны.

a*b for a in num_a for b in num_b

Наконец, оберните его в квадратные кронштейны и назначьте его на вывод.

output = [a*b for a in num_a for b in num_b]

Как я упоминал в начале статьи, так же, как вы можете создать список, используя выражение генератора, завернутое в квадратные скобки [] , вы также можете создать установить Используя вьющиеся скобки {} вместо.

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

odd_remainders = {100%n for n in range(1,100,2)}
print(odd_remainders)

Запуск этого кода дает нам …

{0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 29, 30, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49}

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

Понимание словаря следует почти Та же структура, что и другие формы выражений генератора, но для одного отличия: Колоны Анкет

Если вы помните, когда вы создаете набор или Словарь, вы используете Curly Braces {} Анкет Единственная разница в том, что в словаре вы используете толстую кишку : Чтобы разделить пары ключевых значений, то, что вы бы не делали в наборе. Тот же принцип применяется здесь.

Например, если мы хотим создать словарь, который хранит целое число между 1 и 100 в качестве ключа, а квадрат этого числа в качестве значения …

squares = {n : n**2 for n in range(1,101)}
print(squares)

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

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

Тем не менее, я должен предотвратить вас от того, чтобы сходить с ума. Помните Дзен питона ! Вот часть, которая имеет отношение к этой теме …

Красиво лучше уродливого… Просто лучше, чем сложный. Комплекс лучше, чем сложный. Квартира лучше, чем вложенная. Рубкий лучше, чем плотный. Читабельности…

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

1. Они становятся нечитаемыми быстро

Я одолжил этот пример из опрос OpenEDX

primary = [ c for m in status['members'] if m['stateStr'] == 'PRIMARY' for c in rs_config['members'] if m['name'] == c['host'] ]
secondary = [ c for m in status['members'] if m['stateStr'] == 'SECONDARY' for c in rs_config['members'] if m['name'] == c['host'] ]
hidden = [ m for m in rs_config['members'] if m['hidden'] ]

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

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

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

primary = [
    c
    for m in status['members']
        if m['stateStr'] == 'PRIMARY'
    for c in rs_config['members']
        if m['name'] == c['host']
    ]

secondary = [
    c
    for m in status['members']
        if m['stateStr'] == 'SECONDARY'
    for c in rs_config['members']
        if m['name'] == c['host']
    ]

hidden = [
    m
    for m in rs_config['members']
        if m['hidden']
    ]

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

Все еще не убежден, что это легко насилие на Python? Мой приятель IRC грим был достаточно любезен, чтобы поделиться этим реальный мир Пример он столкнулся. Мы понятия не имеем, что это делает.

cropids = [self.roidb[inds[i]]['chip_order'][
               self.crop_idx[inds[i]] % len(self.roidb[inds[i]]['chip_order'])] for i in
           range(cur_from, cur_to)]

Моя душа горит, просто глядя на это.

2. Они не заменяют петли

грим указал на следующий сценарий. (Этот код является вымышленным, к вашему сведению.)

some_list = getTheDataFromWhereever()
[API.download().process(foo) for foo in some_list]

Это выглядит достаточно безобидным к неопытному глазу, но обратите внимание на то, что происходит … Данные в some_list Модифицируется (мутировано) напрямую, но результат не хранится Анкет Это случай понимания списка или даже выражения генератора, который подвергается жестокому обращению с разбирательством. Это делает для некоторого трудного чтения, не говоря уже о отладке.

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

some_list = getTheDataFromWhereever()
for foo in some_list:
    API.download().process(foo)

3. Им может быть трудно отлаживать

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

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

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

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

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

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

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

Давайте рассмотрим ключевые моменты …

  • Выражение генератора следует за структурой для в if Анкет Необязательно, Если Раздел может быть оставлен. Несколько для ... в и Если Раздели могут использоваться в одном выражении генератора.
  • Вы можете изменить стандартную петлю и условную блоку на выражение генератора, перемещая внутренний код на переднюю часть и удалив колоны после оставшихся операторов, обычно перемещая их все на одну линию.
  • Вложенные выражения генератора и понимание списка разрешены.
  • Понимание списка создает список. Это выражение генератора, завернутое в квадратные скобки [ ] .
  • Понимание набора создает набор. Это выражение генератора, завернутое в кудрявые скобки {} Анкет
  • Понимание дикта создает дикт. Это выражение генератора, завернутое в кудрявые скобки {} , с парой ключей в выражении, разделенной толстой кишкой : .
  • Выражения генератора в любой форме не предназначены как прямые замены стандартных петлей. Используйте мудрость в применении их, особенно в том, что их может быть трудно читать, понять или отладить.

Выражения генератора – это только верхушка айсберга, который является генераторы Анкет Мы будем исследовать эту тему в следующем разделе.

Как обычно, я настоятельно рекомендую вам прочитать документацию:

Спасибо тебе Альтенки , грим , а также Недбат (Freenode IRC #python ) Для предлагаемых изменений и включений.

Dead Simple Python (13 серии деталей)

Оригинал: “https://dev.to/codemouse92/dead-simple-python-list-comprehensions-and-generator-expressions-2mb5”