Итераторы и генераторы
Итератор – это объект, который позволяет последовательно получать элементы из коллекции
(списка, кортежа, строки) по одному за раз, при этом не загружая все сразу в память.
Итератор реализует протокол итератора с двумя методами:
– iter() – возвращает сам объект итератора
– next() – возвращает следующий элемент или вызывает исключение StopIteration, когда элементы заканчиваются.
numbers = [7, 4, 11, 3]
numbers_iter = iter(numbers)
print(next(numbers_iter))
print(next(numbers_iter))
print(next(numbers_iter))
print(next(numbers_iter))
print(next(numbers_iter)) # Вернет исключение StopIteration
Цикл for в Python автоматически использует итераторы для перебора объектов. Сначала он превращает коллекцию в итератор, а затем перебирает ее с помощью next(). Итератор – это отдельный объект со своим id. Цикл for всегда сначала превращает объект в итератор, а затем выполняет его поэлементный перебор.
Итератор можно создать классом (менее компактен, чем генератор и может загружать все элементы в память).
Генератор – это компактный способ создания итератора с помощью ключевого слова yield.
def gen_nums():
n = 0
while n <= 4:
yield n
n += 1
yield 42 # отрабатывает, когда закончил работу предыдущий yield
yield 44 # отрабатывает, когда закончил работу предыдущий yield
for num in gen_nums():
print(num)
Генератор – это объект, создающий свои элементы “лениво”, по мере необходимости. Генератор создает свои элементы в момент запроса, поэтому не хранит их в памяти. Это очень удобно при работе с большими или бесконечными последовательностями. По своей сути генератор является корутиной (см. ниже).
Функция-генератор – это обычная функция, использующая yield вместо return.
При повторном запуске она начинает работу с места последней остановки (со строки после yield).
Функция-генератор всегда возвращает объект-генератор. И больше ничего.
Объект-генератор является итератором. Следовательно, можно перебрать его элементы
с помощью next() или for…
Корутина – это функция, способная останавливать свое выполнение в любой точке, сохраняя свое состояние (локальные переменные, стек вызовов). Она может передавать управление другим корутинам и возобновлять работу с места остановки. Это дает возможность существенно облегчить код для асинхронных операций. Корутины не блокируют потоки выполнения и не конкурируют за ресурсы, как это делают потоки. Корутины не привязаны к потокам, поэтому их можно запускать в большом количестве. Корутины имеют несколько точек входа и выхода, в отличие от стандартных функций. Обычная функция имеет только одну точку входа (но может иметь несколько точек выхода – это определяется количеством return-ов).
Генераторное выражение – это синтаксическая конструкция в круглых скобках (похожа на списковые включения): (i*i for in in range(5))
Применение генераторов
Если нужно поработать с большой последовательностью, то для ее создания лучше использовать генератор:
Вместо
def get_nums(n): # n - очень большое число
for num in range(n):
nnn.append(num)
return nnn
Использовать
def get_gen_nums(n): # n - очень большое число
for num in range(n):
yield num
Но необходимо также учитывать, что именно возвращает выражение range(). Если бы оно возвращало
список (на самом деле оно возвращает итерируемый объект), то при большом аргументе это бы сводило
на нет использование генератора. Ведь этот список сначала должен быть загружен в память.
А мы ведь стремимся этого избежать. Функция-генератор масштабируема ровно в той степени, в какой это допускает ее наименее масштабируемая строка.
Работа текстовым файлом
Работа с текстовым файлом – лучший способ для построчного чтения с использованием генератора.
Плохая практика с использованием метода .readlines()
Код читает строки из файла – неоптимальный код
def matching_lines_from_file(path, pattern):
with open(path, encoding='utf-8') as handle:
for line in handle.readlines():
# handle.readlines() создает список всех строк файла - список может быть очень большим
if pattern in line:
return line
# Найдем в нашем файле все вхождения подстроки "нейрон"
print(matching_lines_from_file('file.txt', 'нейрон'))
Более рациональный подход с генератором
def matching_lines_from_file(path, pattern):
with open(path, encoding='utf-8') as handle:
for line in handle:
if pattern in line:
yield line.rstrip('\n') # Возвращает копию строки без пробелов в конце
n = matching_lines_from_file('file.txt', 'нейрос')
print(next(n), end='\n\n')
print(next(n), end='\n\n')
print(next(n), end='\n\n')
print(next(n), end='\n\n')
Можно обойти созданный генератор циклом for:
for string in n:
print(string)
Генераторное выражение
Это структура вида: (n*n for n in range(1000))
"""Разница между генератором и генераторным выражением (которое не занимает много памяти).
Задача - вывести все элементы списка, не занимая много памяти."""
NUM_SQUARES = 10*1000*1000
# Решение 1 - с генератором списка
many_squares = [n*n for n in range(NUM_SQUARES)] # Это генератор списков, возращающий огромный список
for number in many_squares: # При выполнении цикла список many_squares сначала должен быть вычислен и сохранен в памяти
print(number)
# Решение 2 - с генераторным выражением
generated_squares = (n*n for n in range(NUM_SQUARES)) # Это генераторное выражение, возвращающее генератор
for n in generated_squares:
print(n)
Генератор списка и его варианты
"""Генератор списка из двух источников
Работает подобно вложенному циклу for"""
# Порядок расположения блоков for имеет значение!
colors = ["orange", "purple", "pink"]
toys = ["bike", "basketball", "skateboard", "doll"]
print([color + " " + toy
for color in colors
for toy in toys])
# ['orange bike', 'orange basketball', 'orange skateboard', 'orange doll', 'purple bike', 'purple basketball',
# 'purple skateboard', 'purple doll', 'pink bike', 'pink basketball', 'pink skateboard', 'pink doll']
"""Еще один пример генератора списка из диапазонов.
Генератор поочередно разбирает диапазоны на элементы."""
# Порядок расположения блоков for имеет значение!
ranges = [range(1, 7), range(4, 12, 3), range(-5, 9, 4)]
print([float(num)
for subrange in ranges
for num in subrange])
# [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 4.0, 7.0, 10.0, -5.0, -1.0, 3.0, 7.0]
"""Влияние условий if в генераторе списка.
Например, использование нескольких фильтров."""
numbers = [9, -1, -4, 20, 17, -3]
odd_positives = [num for num in numbers
if num > 0
if num % 2 == 1]
print(odd_positives)
# [9, 17]
"""Генератор с использованием вспомогательной функции"""
numbers = [9, -1, -4, 20, 17, -3]
def num_is_valid(num):
return num % 2 == 0 or num % 3 == 0
print([num for num in numbers
if num_is_valid(num)]
)
# [9, -4, 20, -3]
"""Полезное использование генератора - перебор группировки трех списков разной длины."""
# Порядок расположения блоков for имеет значение!
weights = [0.2, 0.5, 0.9]
values = [27.5, 13.4]
offsets = [4.3, 7.1, 9.5]
print([(weight, value, offset)
for weight in weights
for value in values
for offset in offsets])
# [(0.2, 27.5, 4.3), (0.2, 27.5, 7.1), (0.2, 27.5, 9.5), (0.2, 13.4, 4.3), (0.2, 13.4, 7.1), (0.2, 13.4, 9.5),
# (0.5, 27.5, 4.3), (0.5, 27.5, 7.1), (0.5, 27.5, 9.5), (0.5, 13.4, 4.3), (0.5, 13.4, 7.1), (0.5, 13.4, 9.5),
# (0.9, 27.5, 4.3), (0.9, 27.5, 7.1), (0.9, 27.5, 9.5), (0.9, 13.4, 4.3), (0.9, 13.4, 7.1), (0.9, 13.4, 9.5)]
Генератор словаря, множества и кортежа
"""Генераторы словарей, множеств и кортежей - что и как"""
class Student:
"""Класс для описания студента"""
def __init__(self, name, gpa, major):
self.name = name
self.gpa = gpa
self.major = major
# Создание студентов
st_1 = Student('Jim Smith', 3.6, 'Linguistics')
st_2 = Student('Penny Gilmore', 3.9, 'Chemistry')
st_3 = Student('Ryan Spencer', 3.1, 'Physics')
st_4 = Student('Alisha Jones', 2.5, 'Journalism')
# Соберем всех студентов в список
students = [st_1, st_2, st_3, st_4] # Экземпляры класса можно сложить в список
# Генератор словаря для данных всех студентов
st_dict = {student.name: student.gpa
for student in students} # генератор словаря
print(st_dict)
print()
"""Дополнительные операции в пределах генератора словаря: преобразование элемента и фильтрация с помощью if"""
def invert_name(name):
first, last = name.split(' ', 1)
return last + ', ' + first
st_dict_filtered = {invert_name(student.name): student.gpa
for student in students
if student.gpa > 3.5}
print(st_dict_filtered)
print()
"""Генерация множества"""
st_set = {student.major for student in students} # Генератор множества
print(st_set)
print()
"""Как сгенерировать кортеж? Напрямую этого сделать нельзя."""
st_tuple = tuple(student.gpa for student in students) # Генераторное выражение подставляется в конструктор кортежа
print(st_tuple)
print()