В данной статье мы рассмотрим способы создания собственных итераторов в Python и какие генераторы лучше всего для этого использовать.
Содержание статьи
- Что такое итератор?
- Зачем нужно создать итератор?
- Объектно-ориентированный итератор
- Генераторы: простой способ создания итератора
- Функции-генераторы
- Выражения-генераторы
- Выражения-генераторы или функции-генераторы?
- Лучший способ создания итератора
Что такое итератор?
Сначала давайте быстро разберемся, что такое итератор. Для более подробного объяснения посмотрите видео «Итератор и итерируемые объекты. Функции iter() и next()» от автора selfedu.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Итерабельный объект представляет собой объект, элементы которого можно перебирать в цикле или иными доступными способами о которых мы поговорим ниже.
Итератор — это объект, который выполняет фактическую итерацию.
Вы можете создать итератор из любого итерабельного объекта, вызвав встроенную функцию iter()
:
1 2 |
favorite_numbers = [6, 57, 4, 7, 68, 95] data = iter(favorite_numbers) |
Вы можете использовать встроенную функцию next
для итератора, чтобы получить следующий элемент из него (если элементов больше нет, то вы получите исключение StopIteration
).
1 2 3 4 5 |
favorite_numbers = [6, 57, 4, 7, 68, 95] my_iterator = iter(favorite_numbers) print(next(my_iterator)) # Результат: 6 print(next(my_iterator)) # Результат: 57 |
Есть еще одно правило об итераторах, которое делает все намного интереснее: итераторы также являются итераторабельными объектами, а их итератор — это они сами.
Зачем нужно создать итератор?
Итераторы позволяют создать итерабельный объект, который перебирает свои элементы по мере выполнения итерации. Это означает, что вы можете создавать ленивые итераторы, которые не определяют следующий элемент, пока вы не попросите их об этом.
Использование итератора вместо списка, множества или другой итерирабельной структуры данных иногда позволяет экономить память. Например, мы можем использовать itertools.repeat
для создания итератора, который предоставит нам 100 миллионов четверок (4
):
1 2 3 |
from itertools import repeat lots_of_fours = repeat(4, times=100_000_000) |
На моем компьютере этот итератор занимает 56 байт памяти:
1 2 3 |
import sys print(sys.getsizeof(lots_of_fours)) # 56 |
Такой же список из 100 миллионов четверок созданный более примитивным способом занимает 762.94 Мб:
1 2 3 4 |
import sys lots_of_fours = [4] * 100_000_000 print(sys.getsizeof(lots_of_fours)) # 800000064 байт |
Хотя итераторы могут экономить память, они также могут экономить время. Например, если вы хотите вывести только первую строку из 10-гигабайтного файла с логами, вы можете сделать следующее:
1 2 3 4 5 |
first_line = next(open('giant_log_file.txt')) print(first_line) # Вывод: Это первая строка из гигантского файла |
Файловые объекты в Python реализованы как итераторы. При итерации по файлу данные считываются в память по одной строке за раз. Если бы вместо этого мы использовали метод readlines
для хранения всех строк в памяти, мы могли бы исчерпать всю системную память и убить процесс.
Таким образом, итераторы могут сэкономить память, но иногда они также могут сэкономить и время.
Кроме того, у итераторов есть возможности, которых нет у других итерабельных объектов. Например, их «лень» можно использовать для создания итерабельных объектов неизвестной длины. На самом деле, можно даже создавать бесконечно длинные итераторы.
Например, метод itertools.count
создаст нам итератор, который будет выдавать каждое следующее число от 0
до «бесконечности» в зависимости когда вы завершите цикл:
1 2 3 4 |
from itertools import count for n in count(): print(n) |
Результат:
1 2 3 4 |
0 1 2 (это будет продолжаться вечность) |
Метод itertools.count
по сути является бесконечно длинным итерабельным объектом. И он реализован как итератор.
Объектно-ориентированный итератор
Итак, мы увидели, что итераторы могут экономить память, процессорное время и открывать для нас новые возможности.
Давайте создадим свои собственные итераторы. Для начала мы «изобретем» заново объект итератора itertools.count
.
Вот итератор, реализованный с помощью класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Count: """Итератор, который считает до бесконечности.""" def __init__(self, start=0): self.num = start def __iter__(self): return self def __next__(self): num = self.num self.num += 1 return num |
В этом классе есть конструктор, который инициализирует текущее число итератора на 0
(или то, что было передано в качестве начала из аргумента start
). То, что превращает этот класс в итератора, это наличие методов __iter__
и __next__
.
Когда объект передается встроенной функции str
, вызывается метод __str__
. Когда объект передается встроенной функции len
, вызывается ее метод __len__
.
1 2 3 4 5 6 7 |
numbers = [1, 2, 3] print(str(numbers), numbers.__str__()) # Вывод: ('[1, 2, 3]', '[1, 2, 3]') print(len(numbers), numbers.__len__()) # Вывод: (3, 3) |
- Передав наш объект в функцию
iter
это приведет к попытке вызвать его метод__iter__
. - Передав наш объект в функцию
next
это приведет к попытке вызвать его метод__next__
.
Предполагается, что функция iter
возвращает итератор. По этой причине метод __iter__
должен возвращать итератор. Но наш объект сам по себе является итератором, поэтому он должен возвращать самого себя. Объект Count
возвращает self
из своего метода __iter__
, так как он является собственным итератором.
Функция next
должна возвращать следующий элемент в итераторе или вызывать исключение StopIteration
, если элементов больше нет. Мы возвращаем текущее число и увеличиваем его на единицу, чтобы оно было больше во время следующего вызова метода __next__
.
Мы можем вручную перебирать объект Count
следующим образом:
1 2 3 4 5 6 |
c = Count() print(next(c)) # Вывод: 0 print(next(c)) # Вывод: 1 print(next(c)) # Вывод: 2 print(next(c)) # Вывод: 3 |
Мы также можем перебирать объект Count
, используя цикл for
, как и любой другой итерабельный объект:
1 2 |
for n in Count(): print(n) |
Результат:
1 2 3 4 |
0 1 2 (это будет продолжаться вечно) |
Такой объектно-ориентированный подход к созданию итератора — это здорово, но это не типичный способ, которым Python-программисты создают итераторы. Обычно, когда нам нужен итератор, мы создаем генератор.
Генераторы: простой способ создания итератора
Самый простой способ создания собственных итераторов в Python — это создание генератора.
В Python есть два способа создания генераторов.
Дан список чисел:
1 |
favorite_numbers = [6, 57, 4, 7, 68, 95] |
Мы можем сделать генератор, который будет лениво выдавать все квадраты этих чисел следующим образом:
1 2 3 4 5 |
def square_all(numbers): for n in numbers: yield n**2 squares = square_all(favorite_numbers) |
Или мы можем сделать такой же генератор следующим образом:
1 |
squares = (n**2 for n in favorite_numbers) |
Первый подход называется функцией-генератором, а второй — выражением-генератором.
Оба этих объекта-генератора работают одинаково. Они оба имеют тип generator
и оба являются итераторами, которые предоставляют квадраты чисел из нашего списка чисел.
1 2 3 4 5 6 7 8 |
print(type(squares)) # Вывод: <class 'generator'> print(next(squares)) # Вывод: 36 print(next(squares)) # Вывод: 3249 |
Мы поговорим об обоих этих подходах к созданию генератора, но сначала давайте обсудим терминологию.
Слово «генератор» в Python используется в разных смыслах:
- Генератор, также называемый объектом-генератором, — это итератор, тип которого —
generator
; - Функция-генератор — это специальный синтаксис, который позволяет нам создать функцию, возвращающую объект-генератор при вызове;
- Выражение-генератор — это синтаксис, напоминающий представление списков (list comprehension), которое позволяет создавать объект-генератор в одну линию кода.
Убрав эту терминологию, давайте рассмотрим каждую из этих вещей по отдельности. Сначала мы рассмотрим функции-генераторы.
Функции-генераторы
Функции-генераторы отличаются от обычных функций тем, что в них есть один или несколько операторов yield
.
Обычно при вызове функции выполняется ее код:
1 2 3 4 5 6 |
def gimme4_please(): return 4 num = gimme4_please() print(num) # Результат: 4 |
Но если в теле функции есть оператор yield
, то это уже не обычная функция. Теперь это функция-генератор, то есть при вызове она возвращает объект-генератор. Этот объект-генератор может выполняться в цикле до тех пор, пока не будет выполнен оператор yield
:
1 2 3 4 5 6 7 8 |
def gimme4_later_please(): yield 4 get4 = gimme4_later_please() print(get4) # <generator object gimme4_later_please at 0x7f78b2e7e2b0> num = next(get4) print(num) # Результат: 4 |
Одно только присутствие оператора yield
превращает функцию в функцию-генератор. Если вы видите функцию и в ней есть оператор yield
, вы работаете с чем-то иным нежели с обычной функцией. Это немного странно, но именно так работают функции-генераторы.
Хорошо, давайте рассмотрим реальный пример функции-генератора. Мы создадим функцию-генератор, которая будет делать то же самое, что и класс-итератор Count
, который мы создали ранее.
1 2 3 4 5 |
def count(start=0): num = start while True: yield num num += 1 |
Подобно классу-итератору Count
, мы можем вручную перебирать генератор, полученный в результате вызова функции count
:
1 2 3 4 |
c = count() print(next(c)) # вывод: 0 print(next(c)) # вывод: 1 |
И мы можем перебирать этот объект генератора с помощью цикла for
, как и раньше:
1 2 |
for n in count(): print(n) |
Результат:
1 2 3 4 |
0 1 2 (это будет продолжаться вечно) |
Согласитесь, что данная функция значительно короче и понятнее, чем класс Count
, который мы создали ранее.
Выражения-генераторы
Выражения-генераторы — это синтаксис, похожий на синтаксис представления списка (list comprehension), который позволяет нам создать объект-генератор.
Допустим, у нас есть представление-списка, который фильтрует пустые строки из файла и удаляет переход на новую строку в конце \n
:
1 2 3 4 5 |
lines = [ line.rstrip('\n') for line in file('esenin-berioza.txt').readlines() if line != '\n' ] |
Мы можем создать генератор вместо списка, превратив квадратные скобки в круглые скобки:
1 2 3 4 5 |
lines = ( line.rstrip('\n') for line in file('esenin-berioza.txt').readlines() if line != '\n' ) |
Точно так же, как представление списков (list comprehension) вернуло бы нам список, выражение-генератор вернет нам объект-генератор:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
print(type(lines)) # <class 'generator'> next_line = next(lines) print(next_line) # Вывод: 'Белая береза' next_line = next(lines) print(next_line) # Вывод: 'Под моим окном' next_line = next(lines) print(next_line) # Вывод: 'Принакрылась снегом,' next_line = next(lines) print(next_line) # Вывод: 'Точно серебром.' |
Выражения-генераторы используют более короткий синтаксис кода по сравнению с функциями-генераторами. Однако они не такие мощные.
Вы можете написать свою функцию-генератор в такой форме:
1 2 3 4 |
def get_a_generator(some_iterable): for item in some_iterable: if some_condition(item): yield item |
Затем вы можете заменить тело функции на выражение-генератор:
1 2 3 4 5 6 |
def get_a_generator(some_iterable): return ( item for item in some_iterable if some_condition(item) ) |
Если вы не можете написать свою функцию-генератор в такой форме, то вы не сможете создать выражение-генератор для её замены.
Обратите внимание, что мы изменили используемый пример, потому что мы не можем использовать выражение-генератор для предыдущего примера, который реализует itertools.count
который по сути является вечным циклом.
Выражения-генераторы или функции-генераторы?
Выражения-генераторы можно рассматривать как представление-списков (list comprehensions) в мире генераторов.
Если в не знакомы со представлениыем-списков, рекомендую прочитать об этом статью. В этой статье описывается путь от цикла for
к list comprehensions.
Также можно скопировать код из функции-генератора и вставить в обычную функцию которая возвращает выражение-генератор:
Выражения-генераторы являются функциями-генераторами так же, как представление-списков являются простым циклом for
с добавлением и условием.
Выражения-генераторы очень похожи на представление-списков, их даже можно называть представление-генераторов. Технически это не совсем правильное название, но если вы его произнесете, все поймут, о чем вы говорите.
Нед Батчелдер фактически предложил, чтобы мы все начали называть выражения-генераторы (generator expressions) как представление-генераторов (generator comprehensions), и я склонен согласиться, что это было бы более понятным названием.
Лучший способ создания итератора
Чтобы создать итератор, можно создать класс-итератор, функцию-генератор или выражение-генератор. Но какой способ лучше?
Выражения-генераторы очень лаконичны, но они не такие гибкие, как функции-генераторы. Функции-генераторы гибкие, но если вам нужно добавить дополнительные методы или атрибуты к объекту-итератору, то, скорее всего, придется перейти на использование класса-итератора.
Я бы рекомендовал смотреть в сторону к выражениям-генераторам так же и представление-списков (list comprehensions). Если вы выполняете простую операцию вывода или фильтрации, выражение-генератор — отличное решение. Если вы делаете что-то более сложное, вам, скорее всего, понадобится функция-генератор.
Я бы рекомендовал использовать функции-генераторы так же, как использование цикла for
для добавления данных в список. Везде, где требуется метод append
, вы зачастую увидите оператор yield
вместо него.
И я бы сказал, что класс-итератор лучше не использовать. Если вы обнаружили, что вам нужен класс-итератор, попробуйте написать функцию-генератор, которая делает то, что вам нужно, и посмотрите, как она будет работать в сравнении с классом-итератором.
Генераторы могут помочь при создании итераторов
Вы можете встретить классы-итераторы, но редко попадается хорошая возможность написать свой собственный.
Если создание собственного класса-итератора — редкость, то создание собственного итерабельного класса — не такая уж редкость. Итерабельный класс требует наличия метода __iter__
, который возвращает итератор. Поскольку генераторы — это простой способ создания итератора, мы можем использовать функцию-генератор или выражение-генератор для создания наших методов __iter__
.
Например, вот итератор, который предоставляет координаты x-y:
1 2 3 4 5 6 7 8 |
class Point: def __init__(self, x, y): self.x, self.y = x, y def __iter__(self): yield self.x yield self.y |
Обратите внимание, что при вызове класса Point
создается итерабельный объект (а не итератор). Это означает, что метод __iter__
должен возвращать итератор. Самый простой способ создать итератор — это создать функцию-генератор, что мы и сделали.
Мы вставили yield
в метод __iter__
, чтобы превратить его в функцию-генератор, и теперь класс Point
можно перебирать, как и любой другой итерабельный объект.
1 2 3 4 5 6 7 |
p = Point(1, 2) x, y = p print(x, y) # Вывод: 1 2 to_list = list(p) print(to_list) # Вывод: [1, 2] |
Функции-генераторы естественным образом подходят для создания методов __iter__
в итерабельных классах.
Генераторы — это способ создания итераторов
Словари — типичный способ создания карт в Python. Функции — типичный способ создания вызываемого объекта в Python. Аналогично, генераторы — это типичный способ создания итератора в Python.
Поэтому, когда вы думаете: «Было бы неплохо реализовать итерабельный объект, который бы лениво вычислял что-то по мере выполнения цикла», подумайте об итераторах.
А когда вы думаете о том, как создать свой собственный итератор, вспомните о функциях-генераторах и выражениях-генераторах.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»