Генераторы списков
Одна из ситуаций, где программирование проявляет свою наибольшую силу, — обработка последовательностей элементов и событий. Компьютеры хорошо умеют повторять действия. Например, в предыдущих частях этого материала мы разными способами итерировались по строкам, спискам и словарям.
Предположим, у нас есть список целых чисел, и нам нужен такой же список элементов в строковом формате. Традиционный способ выполнить задачу мог бы выглядеть так:
numbers = [1, 2, 3, 6, 5, 4, 7]
strings = []
for number in numbers:
strings.append(str(number))Генераторы списков
Есть и более "питонический" способ создавать списки на основе существующих списков. Он называется list comprehension, или генератором списка.
Идея в том, чтобы в одной строке уместить и описание того, что нужно сделать с каждым элементом списка, и присваивание результата новому списку.
В примере выше операция, выполняемая с каждым элементом списка, была очень простой: каждое целое число преобра зовывалось в строку. Посмотрим, как это выглядело бы с генератором списка:
numbers = [1, 2, 3, 6, 5, 4, 7]
strings = [str(number) for number in numbers]Вторая строка выше содержит многие из тех же элементов, что и более традиционный итеративный подход, но синтаксис отличается. Один из способов обобщить инструкцию генератора списка выглядит так:
[<expression> for <item> in <series>]
Квадратные скобки вокруг выражения генератора списка сообщают Python, что результатом должен быть новый список. Каждый элемент исходного списка по одному обрабатывается, а результат сохраняетс я в новом списке, как и в итеративном подходе выше. В результате у нас появляется новый список ровно с таким же количеством элементов, как в исходном, и все элементы обработаны одинаковым образом.
(Обратите внимание: исходники изображений в этой части временно отсутствуют, поэтому в иллюстрациях этой части встречается финская лексика. Мы работаем над исправлением этого.)
Генераторы списков могут обрабатывать и гораздо более сложные операции. Мы можем выполнять вычисления, например умножать исходные элементы на десять:
numbers = list(range(1,10))
print(numbers)
numbers_multiplied = [number * 10 for number in numbers]
print(numbers_multiplied)[1, 2, 3, 4, 5, 6, 7, 8, 9] [10, 20, 30, 40, 50, 60, 70, 80, 90]
На самом деле выражение внутри генератора списка может быть любым выражением Python. Можно даже вызывать функции, которые вы определили сами:
def factorial(n: int):
""" The function calculates the factorial n! for integers above zero """
k = 1
while n >= 2:
k *= n
n -= 1
return k
if __name__ == "__main__":
numbers = [5, 2, 4, 3, 0]
factorials = [factorial(number) for number in numbers]
print(factorials)[120, 2, 24, 6, 1]
С более привычным циклом for тот же процесс можно было бы выразить так:
def factorial(n: int):
""" The function calculates the factorial n! for integers above zero """
k = 1
while n >= 2:
k *= n
n -= 1
return k
if __name__ == "__main__":
numbers = [5, 2, 4, 3, 0]
factorials = []
for number in numbers:
factorials.append(factorial(number))
print(factorials)Генераторы списков позволяют выразить ту же функциональность более кратко, обычно без потери читаемости.
Можно также возвращать выражение генератора списка напрямую из функции. Если бы нам нужна была функция для вычисления факториалов для списков чисел, это можно сделать очень лаконично:
def factorials(numbers: list):
return [factorial(number) for number in numbers]Фильтрация элементов
В примерах выше все наши списки оставались той же длины до и после операции генератора списка. В каждом случае все элементы исходного списка использовались как основа для нового списка. Но иногда нам нужны только некоторые исходные элементы. Как этого добиться?
Инструкция генератора списка также допускает условие, чтобы мы могли проверять элементы по условию и выбирать только подходящие. Общий синтаксис следующий:
[<expression> for <item> in <series> if <Boolean expression>]
Инструкция выше в остальном идентична общей форме, представленной в начале этого раздела, но теперь в конце есть инструкция if. Только те элементы исходного списка, для которых булево выражение истинно, используются как основа нового списка.
В примере ниже мы выбираем все четные элементы исходного списка как основу нового списка. На самом деле эти элементы дальше никак не обрабатываются; они помещаются в новый список как есть:
numbers = [1, 1, 2, 3, 4, 6, 4, 5, 7, 10, 12, 3]
even_items = [item for item in numbers if item % 2 == 0]
print(even_items)[2, 4, 6, 4, 10, 12]
Выражение в генераторе списка выше — просто item, что означает, что над элементами списка не выполняется никаких операций. Выражением может быть любое выражение Python, как и в предыдущих примерах. Например, следующий генератор списка берет все четные элементы списка, умножает каждый на десять и сохраняет результат в новом списке:
numbers = [1, 1, 2, 3, 4, 6, 4, 5, 7, 10, 12, 3]
even_items = [item * 10 for item in numbers if item % 2 == 0]
print(even_items)[20, 40, 60, 40, 100, 120]
Когда вы встречаете все более сложные генераторы списков, может быть полезно сначала читать условие. В конце концов, элементы обрабатываются только если проходят проверку, поэтому часто имеет смысл сначала понять, какие элементы проходят этап фильтрации. Иногда выражение в генераторе списка вообще не было бы возможным для всех элементов исходного списка.
Например, операция факториала определена только для неотрицательных целых чисел. Если мы не можем быть уверены, что список содержит только значения от нуля и выше, содержимое нужно отфильтровать перед передачей в функцию factorial, которую мы написали раньше:
def factorial(n: int):
""" The function calculates the factorial n! for integers above zero """
k = 1
while n >= 2:
k *= n
n -= 1
return k
if __name__ == "__main__":
numbers = [-2, 3, -1, 4, -10, 5, 1]
factorials = [factorial(number) for number in numbers if number >= 0]
print(factorials)[6, 24, 120, 1]
Как мы видели в самом первом примере генератора списка, где целые числа преобразовывались в строки, элементы нового списка не обязаны быть того же типа, что элементы исходного списка. Продолжая пример с факториалом выше, мы можем создать кортеж из каждого исходного элемента и его обработанного аналога и сохранить эти кортежи в списке, объединяя все, что изучили до сих пор, в одном выражении генератора списка:
def factorial(n: int):
""" The function calculates the factorial n! for integers above zero """
k = 1
while n >= 2:
k *= n
n -= 1
return k
if __name__ == "__main__":
numbers = [-2, 3, 2, 1, 4, -10, 5, 1, 6]
# the variable name abbreviated here so that this would be easier to read
factorials = [(n, factorial(n)) for n in numbers if n > 0 and n % 2 == 0]
print(factorials)[(2, 2), (4, 24), (6, 720)]
Разбирая пример выше, у нас есть булево выражение n > 0 and n % 2 == 0. Это означает, что из исходного списка для дальнейшей обработки принимаются только элементы, которые одновременно положительные и делятся на два.
Затем каждое из этих положительных четных чисел по очереди преобразуется в формат (n, factorial(n)). Это кортеж, где первый элемент — само число, а второй — результат, возвращенн ый функцией factorial.
Альтернативное выполнение с генераторами списков
Часто, когда у нас есть условная инструкция, мы также включаем ветку else. Поскольку условия можно использовать в генераторах списков, ветка else тоже доступна с генераторами списков. Общий синтаксис условия, используемого с генераторами списков, выглядит так:
<expression 1> if <condition> else <expression 2>
Мы уже встречали такие однострочные условия, или тернарные операторы, в части 7. Выражение выше вычисляется либо как expression 1, либо как expression 2, в зависимости от того, истинно условие или ложно.
Чтобы освежить тему: если бы нам нужно было вывести большее из двух чисел и мы хотели бы использовать только одну инструкцию print, все можно было бы уместить в одной строке:
number1 = int(input("Type in number 1:"))
number2 = int(input("Type in number 2:"))
print (number1 if number1 > number2 else number2)Объединение синтаксиса тернарного оператора с генератором списка дает следующую общую структуру:
[<expression 1> if <condition> else <expression 2> for <item> in <series>]
Это может выглядеть немного запутанно, потому что условная структ ура теперь стоит перед собственно частью генератора списка. Так синтаксис определен, по крайней мере сейчас. Если есть ветка else, условие идет первым. Если есть только if, оно идет в конец. Можете попробовать поменять их местами и посмотреть, что произойдет.
Включение оператора else означает, что мы снова обрабатываем каждый элемент исходного списка. В зависимости от того, истинно условие или ложно, к каждому элементу списка применяется либо expression 1, либо expression 2.
Следующий пример проверяет, равны ли элементы списка нулю или больше нуля. Любой такой элемент принимается как есть, но все отрицательные элементы меняют знак, так что становятся положительными. Результат — список, содержащий абсолютные значения элементов исходного списка.
numbers = [1, -3, 45, -110, 2, 9, -11]
abs_vals = [number if number >= 0 else -number for number in numbers]
print(abs_vals)[1, 3, 45, 110, 2, 9, 11]
Повторим, что происходит выше: если условие number >= 0 истинно, к элементу применяется выражение number, и результатом является сам элемент. Если условие ложно, к элементу применяется выражение -number, так что значение становится положительным.
В следующем примере у нас есть функция string_lengths, которая принимает список в качестве аргумента и возвращает другой список с длинами всех строк в исходном списке. При этом функция нормально относится к элементам списка любого типа. Если элемент — строка, она вычисляет его длину. Если элемент — что-то другое, она вставляет -1 в возвращаемый список.
def string_lengths(my_list: list):
""" The function returns the lengths of strings in a new list """
return [len(item) if type(item) == str else -1 for item in my_list]
if __name__ == "__main__":
test_list = ["hi", 3, True, "there", -123.344, "toodlepip", 2, False]
lengths = string_lengths(test_list)
print(lengths)[2, -1, -1, 5, -1, 9, -1, -1]
Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.