Сообщить об ошибке.

Генератор списка list в Python

Краткий способ создания списков list

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

Содержание:

В версии Python 3.12 ускорено выполнение генераторов словарей, списков и множеств до двух раз. Смотрите подраздел "Улучшения в Python 3.12 для генераторов..."

Понимание генератора списка.

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

Например мы хотим создать список квадратов:

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

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

squares = list(map(lambda x: x**2, range(10)))

# это эквивалентно:

squares = [x**2 for x in range(10)]

Последняя запись является более краткой и читабельной. "Список - выражение" состоит из скобок, внутри которых, сначала идет нужное нам выражение, за которым следует предложение for ... in, далее выражение может включать ноль или более подвыражений for ... in или предложения if ... else.

Результатом будет новый список, полученный в результате оценки выражения в контексте предложений for ... in и if ... else которые следуют за ним. Например, этот listcomp объединяет элементы двух списков, если они не равны:

>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

# это эквивалентно:

>>> combs = []
>>> for x in [1,2,3]:
...     for y in [3,1,4]:
...         if x != y:
...             combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

Обратите внимание, что порядок операторов for ... in and if ... else одинаков в обоих этих фрагментах.

Если нужное нам выражение является кортежем, как в предыдущем примере, оно должно быть заключено в скобки (x, y).

Вложенные генераторы списков.

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

Рассмотрим следующий пример матрицы 3x4, реализованной в виде списка из трех списков длиной 4:

>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]

Следующий генератор списка будет транспонировать строки и столбцы:

>>> [[row[i] for row in matrix] for i in range(4)]
# [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

Вложенный генератор списка оценивается в контексте следующего за ним for ... in, поэтому этот пример эквивалентен:

>>> transposed = []
>>> for i in range(4):
...     transposed.append([row[i] for row in matrix])
...
>>> transposed
# [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

Код выше, в свою очередь, совпадает с таким кодом:

>>> transposed = []
>>> for i in range(4):
...     # the following 3 lines implement the nested listcomp
...     transposed_row = []
...     for row in matrix:
...         transposed_row.append(row[i])
...     transposed.append(transposed_row)
...
>>> transposed
# [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

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

>>> list(zip(*matrix))
# [(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

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

>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]
>>> [
...    [
...    row[i]
...    for row in matrix
...    ] 
... for i in range(4)
... ]
# [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

Да-да... И все будет работать!

Дополнительно смотрите материал "Эффективные приемы использования генераторов списков".

Улучшения в Python 3.12 для генераторов списков, словарей и множеств

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

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

  • В обратных трассировках больше нет отдельного фрейма для выражений-генераторов, а трассировка/профилирование больше не отображает это выражение как вызов функции.
  • Модуль symtable больше не будет создавать дочерние таблицы символов для каждого выражения-генератора. Теперь локальные значения выражения-генератора будут включены в таблицу символов родительской функции.
  • Вызов locals() внутри выражения-генератора теперь включает переменные извне и больше не включает синтетическую переменную .0 для "аргумента" выражения-генератора.
  • Выражение-генератора, выполняющее итерацию непосредственно через locals() (например, [k for k in locals()]) при запуске в режиме трассировки (например, измерение покрытия кода), может вызвать исключение "RuntimeError: dictionary changed size during iteration". Это то же самое поведение, которое уже наблюдалось в циклах for k in locals():. Чтобы избежать ошибки, сначала необходимо создать список ключей для перебора:

    >>> keys = list(locals())
    >>> [k for k in keys]
    

В версии Python 3.12, переменные, используемые в целевой части выражений-генераторов, которые не сохраняются, теперь могут использоваться в выражениях присваивания (:=). Например, в [(b := 1) for a, b.prop in some_iter] теперь разрешено присвоение b. Обратите внимание, что присвоение переменных, хранящихся в целевой части понятий (например, a), по-прежнему запрещено. (Добавлено Никитой Соболевом.)

Примеры создания простых генераторов списков.

>>> vec = [-4, -2, 0, 2, 4]

# новый список с удвоенными значениями
>>> [x*2 for x in vec]
# [-8, -4, 0, 4, 8]

# фильтр списка для исключения отрицательных чисел
>>> [x for x in vec if x >= 0]
# [0, 2, 4]

# применить функцию ко всем элементам
>>> [abs(x) for x in vec]
# [4, 2, 0, 2, 4]

# вызов метода для каждого элемента
>>> freshfruit = ['  banana', '  loganberry ', 'passion fruit  ']
>>> [weapon.strip() for weapon in freshfruit]
# ['banana', 'loganberry', 'passion fruit']

# создаст список из кортежей типа (число, квадрат)
>>> [(x, x**2) for x in range(6)]
# [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]

# кортеж должен быть заключен в скобки,
# иначе возникнет ошибка
>>> [x, x**2 for x in range(6)]
#   File "<stdin>", line 1, in <module>
#     [x, x**2 for x in range(6)]
#                ^
# SyntaxError: invalid syntax

# сгладим список с помощью двух выражений 'for ... in'
>>> vec = [[1,2,3], [4,5,6], [7,8,9]]
>>> [num for elem in vec for num in elem]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

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

>>> vec = [
... [1,2,3], 
... [4,5,6], 
... [7,8,9]
]

>>> [
... num
... for elem in vec 
... for num in elem
... ]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# или вот еще пример 'понятной' записи
>>> import random
>>> n = 10
>>> tree = [
...         ' '*(n-i)+'/'+''.join(random.choice(' # *') 
...         for _ in range(2*i))+'\\' 
...         for i in range(n)
...         ]
>>> print('\n'.join(tree))
#          /\
#         / *\
#        /#  *\
#       /  * ##\
#      /   *  #*\
#     /# **  * * \
#    /#  #*# *  *#\
#   /   **##  *   #\
#  /  * ** *   #*# #\
# /** **#*## **  # #*\

И самое главное все работает, правда здорово! Используйте эту приятную особенность языка Python в своем коде, что-бы он был более понятным другим.

Генераторы списков могут содержать сложные подвыражения и вложенные функции:

>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
# ['3.1', '3.14', '3.142', '3.1416', '3.14159']