Python

Когда использовать List Comprehension в Python

Spread the love

Перевод оригинальной статьи:  James TimminsWhen to Use a List Comprehension in Python

Прим. переводчика: В русской терминологии нет общепризнанного перевода list comprehension. Гугл его переводит как списковое включение или абстракция списков. Хотя наиболее часто можно встретить фразу генератор списков, но мне кажется это не совсем правильно, так как в Python есть отдельное понятие генератора. По моему, возможно наиболее подходящий перевод был бы представление списков. Поэтому в этой статье эта фраза будет использоваться без перевода, либо будет переводится следующим образом: list comprehensionпредставление списков, set comprehensionпредставление множества и dictionary comprehensionпредставление словаря)

Содержание

Спонсор поста ХОСТИНГ FOZZY БЫСТРЕЕ БЫСТРОГО
https://fozzy.com/ru/
Fozzy — компания, созданная профессионалами хостинг-индустрии, в рамках холдинга XBT — одного из мировых лидеров хостинга, представленного по всему миру: США, Нидерланды, Люксембург, Россия, Сингапур, Индия.
Fozzy работает честно и открыто. Мы не продаем волшебные «безлимитные тарифы» и «заоблачные возможности». Информация о наших серверах, технологиях и дата-центрах открыта для всех.

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

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

В этом уроке вы узнаете, как:

  • Переписать циклы и вызовы map() с использованием list comprehension
  • Выбрать между comprehensions, циклами и вызовами map()
  • Использовать comprehensions с условной логикой
  • Использовать comprehensions, для замены filter()
  • Профилировать свой код для решения вопросов производительности

Как создаются списки в Python

Существует несколько способов создания списков в Python. Чтобы лучше понять компромиссы связанные с использованием list comprehension, давайте сначала рассмотрим способы создания списков с помощью этих подходов.

Использование цикла for

Наиболее распространенным типом цикла является цикл for. Использование цикла for можно разбить на три этапа:

  1. Создание пустого списка.
  2. Цикл по итерируемому объекту или диапазону элементов range.
  3. Добавляем каждый элемент в конец списка.

Допустим на надо создать список squares, то эти шаги будут в трех строках кода:

>>> squares = []
>>> for i in range(10):
...     squares.append(i * i)
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Здесь мы создаем пустой список squares. Затем используем цикл for для перебора range(10). Наконец умножаем каждое число отдельно и добавляете результат в конец списка.

Использование объектов map()

map() предоставляет альтернативный подход, основанный на функциональном программировании. Мы передаем функцию и итерируемый объект (iterable), а map() создаст объект. Этот объект содержит выходные данные, которые мы получаем при запуске каждого итерируемого элемента через предоставленную функцию.

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

>>> txns = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08

>>> def get_price_with_tax(txn):
...     return txn * (1 + TAX_RATE)

>>> final_prices = map(get_price_with_tax, txns)
>>> list(final_prices)

[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]

Здесь у вас есть итерируемый объект txns (в нашем случае простой список) и функция get_price_with_tax(). Мы передаем оба этих аргумента в map() и сохраняем полученный объект в final_prices. Мы можем легко преобразовать этот объект map в список, используя list().

Использование List Comprehensions

List comprehensions — это третий способ составления списков. При таком элегантном подходе мы можем переписать цикл for из первого примера всего в одну строку кода:

>>> squares = [i * i for i in range(10)]
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

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

new_list = [expression for member in iterable]

Каждое представление списков в Python включает три элемента:

  1. expression какое либо вычисление, вызов метода или любое другое допустимое выражение, которое возвращает значение. В приведенном выше примере выражение i * i является квадратом значения члена.
  2. member является объектом или значением в списке или итерируемым объекте (iterable). В приведенном выше примере значением элемента является i.
  3. iterable список, множество, последовательность, генератор или любой другой объект, который может возвращать свои элементы по одному. В приведенном выше примере iterable является range(10).

Поскольку требования к expression (выражению) настолько гибки, представление списков хорошо работает во многих местах, где вы будете использовать map(). Вы так же можем переписать пример ценообразования:

>>> txns = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08

>>> def get_price_with_tax(txn):
...     return txn * (1 + TAX_RATE)

>>> final_prices = [get_price_with_tax(i) for i in txns]
>>> final_prices

[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]

Единственное различие между этой реализацией и map() состоит в том, что list comprehension возвращает список, а не объект map.

Преимущества использования представления списков

Представление списков часто описываются как более Pythonic, чем циклы или map(). Но вместо того, чтобы слепо принимать эту оценку, стоит понять преимущества использования list comprehension по сравнению с альтернативами. Позже вы узнаете о нескольких сценариях, в которых альтернативы являются лучшим выбором.

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

Это основная причина, почему list comprehension считаются Pythonic, поскольку Python включает в себя простые и мощные инструменты, которые вы можете использовать в самых разных ситуациях. Дополнительным преимуществом является то, что всякий раз, когда вы используете представления списков, вам не нужно запоминать правильный порядок аргументов, как при вызове map().

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

Погружаемся в Comprehensions

Чтобы понять всю ценность, которую могут предоставить list comprehensions, полезно понять их диапазон возможных функций. Мы также коснемся изменений, которые были внесены в Python 3.8.

Использование условной логики

Ранее вы видели это шаблон для создания представлений списка:

new_list = [expression for member in iterable]

Хотя этот шаблон точен, он также немного неполон. Более полное описание шаблона добавляет поддержку необязательных условных выражений. Наиболее распространенный способ добавить условную логику к list comprehension — добавить условное выражение в конец выражения:

new_list = [expression for member in iterable (if conditional)]

Здесь наше условное утверждение предшествует закрывающей скобке.

Условные выражения позволяют отфильтровывать нежелательные значения, без вызова filter():

>>> sentence = 'the rocket came back from mars'
>>> vowels = [i for i in sentence if i in 'aeiou']
>>> vowels

['e', 'o', 'e', 'a', 'e', 'a', 'o', 'a']

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

Условие может содержать любое допустимое выражение. Если вам нужен более сложный фильтр, вы можете даже переместить условную логику в отдельную функцию:

>>> sentence = 'The rocket, who was named Ted, came back \
... from Mars because he missed his friends.'

>>> def is_consonant(letter):
...     vowels = 'aeiou'
...     return letter.isalpha() and letter.lower() not in vowels

>>> consonants = [i for i in sentence if is_consonant(i)]

['T', 'h', 'r', 'c', 'k', 't', 'w', 'h', 'w', 's', 'n', 'm', 'd', \
'T', 'd', 'c', 'm', 'b', 'c', 'k', 'f', 'r', 'm', 'M', 'r', 's', 'b', \
'c', 's', 'h', 'm', 's', 's', 'd', 'h', 's', 'f', 'r', 'n', 'd', 's']

Здесь мы создаем сложный фильтр is_consonant() и передаете эту функцию как условный оператор для нашего представления списка. Обратите внимание, что значение элемента i также передается в качестве аргумента нашей функции.

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

new_list = [expression (if conditional) for member in iterable]

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

>>> original_prices = [1.25, -9.45, 10.22, 3.78, -5.92, 1.16]
>>> prices = [i if i > 0 else 0 for i in original_prices]
>>> prices
[1.25, 0, 10.22, 3.78, 0, 1.16]

Здесь, наше выражение i содержит условный оператор, if i> 0 else 0. Это говорит Python выводить значение i, если число положительное, но менять i на 0, если число отрицательное. Если это окажется недостаточно, то может быть полезно рассматривать условную логику как свою отдельную функцию:

>>> def get_price(price):
...     return price if price > 0 else 0

>>> prices = [get_price(i) for i in original_prices]
>>> prices

[1.25, 0, 10.22, 3.78, 0, 1.16]

Теперь наш условный оператор содержится в get_price(), и вы можете использовать его как часть выражения вашего списка.

Использование Set и Dictionary Comprehensions

Хотя list comprehension в Python является распространенным инструментом, вы также можете создавать множественные и словарные представления (set and dictionary comprehensions). set comprehension почти точно такое же, как представление списка. Разница лишь в том, что заданные значения обеспечивают, чтобы выходные данные не содержали дубликатов. Вы можете создать set comprehension, используя фигурные скобки вместо скобок:

>>> quote = "life, uh, finds a way"
>>> unique_vowels = {i for i in quote if i in 'aeiou'}
>>> unique_vowels
{'a', 'e', 'u', 'i'}

В нашем примере set comprehension выводит все уникальные гласные, которые он нашел в quote. В отличие от списков, наборы не гарантируют, что элементы будут сохранены в определенном порядке. Вот почему первым членом набора является a, хотя первый гласный в quote — i.

Dictionary comprehensions аналогично, с дополнительным требованием определения ключа:

>>> squares = {i: i * i for i in range(10)}
>>> squares
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

Чтобы создать словарь квадратов, воспользуемся фигурными скобками ({}), а также парой ключ-значение (i: i * i) в своем выражении.

Использование оператора Walrus

Python 3.8 представил выражение присваивания (assignment expression), также известное как оператор walrus (оператор моржа). Чтобы понять, как он используется, рассмотрим следующий пример.

Скажем, нам нужно сделать десять запросов к API, которое будет возвращать данные о температуре. Мы хотим вернуть только результаты, которые превышают 100 градусов по Фаренгейту. Предположим, что каждый запрос будет возвращать разные данные. В этом случае в Python нет способа использовать list comprehension для решения проблемы. Выражение шаблона expression for member in iterable (if conditional) не позволяет условию назначить данные переменной, к которой можно обращаться в выражение.

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

>>> import random

>>> def get_weather_data():
...     return random.randrange(90, 110)

>>> hot_temps = [temp for _ in range(20) if (temp := get_weather_data()) >= 100]

>>> hot_temps
[107, 102, 109, 104, 107, 109, 108, 101, 104]

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

Когда не использовать List Comprehension в Python

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

Остерегайтесь вложенных Comprehensions

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

>>> cities = ['Austin', 'Tacoma', 'Topeka', 'Sacramento', 'Charlotte']

>>> temps = {city: [0 for _ in range(7)] for city in cities}

>>> temps
{
    'Austin': [0, 0, 0, 0, 0, 0, 0],
    'Tacoma': [0, 0, 0, 0, 0, 0, 0],
    'Topeka': [0, 0, 0, 0, 0, 0, 0],
    'Sacramento': [0, 0, 0, 0, 0, 0, 0],
    'Charlotte': [0, 0, 0, 0, 0, 0, 0]
}

Мы создали внешнюю коллекцию temps как представление словаря. Выражение представляет собой пару ключ-значение, которая содержит еще одно comprehension. Этот код быстро сгенерирует список данных для каждого города в cities.

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

>>> matrix = [[i for i in range(5)] for _ in range(6)]
>>> matrix
[
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4]
]

Внешнее представление списка [… for _ in range(6)] создает шесть строк, в то время как внутреннее представление списка [i for i in range(5)] заполняет каждую из этих строк значениями.

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

matrix = [
...     [0, 0, 0],
...     [1, 1, 1],
...     [2, 2, 2],
... ]
>>> flat = [num for row in matrix for num in row]
>>> flat
[0, 0, 0, 1, 1, 1, 2, 2, 2]

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

>>> matrix = [
...     [0, 0, 0],
...     [1, 1, 1],
...     [2, 2, 2],
... ]
>>> flat = []
>>> for row in matrix:
...     for num in row:
...         flat.append(num)
...
>>> flat
[0, 0, 0, 1, 1, 1, 2, 2, 2]

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

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

Используйте Генераторы для больших наборов данных

Представление списков в Python работает путем загрузки всего списка в память. Для небольших или даже средних списков это обычно хорошо. Например если вы хотите сложить квадраты первых тысячи целых чисел, то list comprehension решит эту проблему превосходно:

>>> sum([i * i for i in range(1000)])
332833500

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

Когда размер списка становится проблематичным, часто полезно использовать генератор вместо list comprehension. Генератор не создает единую большую структуру данных в памяти, а вместо этого возвращает итерацию. Ваш код будет запрашивать следующее значение из итерируемого столько раз, сколько необходимо или пока вы не достигните конца своей последовательности, сохраняя при этом только одно значение за раз.

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

>>> sum(i * i for i in range(1000000000))
333333332833333333500000000

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

Приведенный выше пример все еще требует много времени для выполнения, но программа не зависнит так как операции выполняются отложено. Из-за отложенных вычислений значения рассчитываются только по явному запросу. После того, как генератор выдаст значение (например, 567 * 567), он может добавить это значение к текущей сумме, затем отбросить это значение и сгенерировать следующее значение (568 * 568). Когда функция sum запрашивает следующее значение, цикл начинается заново. Для этого процесса необходим небольшой объем памяти.

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

>>> sum(map(lambda i: i*i, range(1000000000)))
333333332833333333500000000

Вам решать, предпочитаете ли вы выражение генератора или map().

Профилирование для оптимизации производительности

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

Если вы реализуете сценарии, где важна производительность, то обычно лучше профилировать различные подходы и смотреть на данные. timeit — полезная библиотека для определения времени выполнения кусков кода. Вы можете использовать timeit для сравнения времени выполнения map(), циклов и списков:

>>> import random
>>> import timeit

>>> TAX_RATE = .08
>>> txns = [random.randrange(100) for _ in range(100000)]

>>> def get_price(txn):
...     return txn * (1 + TAX_RATE)
...
>>> def get_prices_with_map():
...     return list(map(get_price, txns))
...
>>> def get_prices_with_comprehension():
...     return [get_price(txn) for txn in txns]
...
>>> def get_prices_with_loop():
...     prices = []
...     for txn in txns:
...         prices.append(get_price(txn))
...     return prices
...
>>> timeit.timeit(get_prices_with_map, number=100)
2.0554370979998566
>>> timeit.timeit(get_prices_with_comprehension, number=100)
2.3982384680002724
>>> timeit.timeit(get_prices_with_loop, number=100)
3.0531821520007725

Здесь мы определяем три метода, каждый из которых использует свой подход для создания списка. Затем мы сообщаем timeit, что каждая из этих функций должна запускаться по 100 раз. timeit возвращает общее время, необходимое для выполнения этих 100 выполнений.

Как показывает код, наибольшее различие заключается в подходе на основе цикла и map(), причем выполнение цикла занимает на 50% больше времени. То, имеет ли это значение, зависит от потребностей вашего приложения.

Заключение

В этом посте вы узнали, как использовать list comprehension в Python для выполнения сложных задач без чрезмерного усложнения кода.

Теперь вы можете:

  • Упростите циклы и вызовы map() с помощью использования декларативный представлений
  • Использовать условную логику в представление
  • Создать set и dictionary представления
  • Определить, когда ясность кода или производительность диктует альтернативный подход

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

Помните, что хотя list comprehensions привлекает к себе большое внимание, ваша интуиция и способность использовать расчетные данные, помогут вам написать чистый код, который выполняет поставленную задачу. Это, в конечном счете, ключ к созданию вашего кода Pythonic!

Упражнение на закрепление

Использование представление списков в Python

Была ли вам полезна эта статья?
[120 / 4.9]

Spread the love
Editorial Team

View Comments

  • Спасибо что помогли разобраться в теме

  • Очень подробно и круто, примеры классные и показательные. даже уже совсем взрослому новичку всё понятно!

  • Отличный перевод! Спасибо за этот маитериал.

  • Очень трудно читать: ОГРОМНЫЕ заголовки h1-h3 и ппц какой мелкий код

  • А нормально по PEP8 использовать такие имена как l1 и l2?

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

11 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

11 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

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

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago