Генераторы, итераторы и последовательности Python
Введение
Примеры
итерация
Объект генератор поддерживает протокол итератора. То есть, она обеспечивает next()
метод ( __next__()
в Python 3.x), который используется для пошагового ее выполнения, и его __iter__
метод возвращает себя. Это означает, что генератор может использоваться в любой языковой конструкции, которая поддерживает универсальные итерируемые объекты.
# naive partial implementation of the Python 2.x xrange()
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# looping
for i in xrange(10):
print(i) # prints the values 0, 1, ..., 9
# unpacking
a, b, c = xrange(3) # 0, 1, 2
# building a list
l = list(xrange(10)) # [0, 1, ..., 9]
Следующая () функция
next()
встроенные удобная обертка , которая может быть использована для получения значения из любого итератора (включая генератор итератор) и установить значение по умолчанию в случае исчерпание итератора.
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# ...
Синтаксис next(iterator[, default])
по next(iterator[, default])
.Если итератор заканчивается и передается значение по умолчанию, оно возвращается. Если не было представлено никакой умолчанию StopIteration
приподнята.
Отправка объектов в генератор
В дополнение к получению значений от генератора, можно отправить объект с генератором с помощью send()
метод.
def accumulator():
total = 0
value = None
while True:
# receive sent value
value = yield total
if value is None: break
# aggregate values
total += value
generator = accumulator()
# advance until the first "yield"
next(generator) # 0
# from this point on, the generator aggregates values
generator.send(1) # 1
generator.send(10) # 11
generator.send(100) # 111
# ...
# Calling next(generator) is equivalent to calling generator.send(None)
next(generator) # StopIteration
Что здесь происходит, это следующее:
Генератор выражений
Можно создавать генераторы итераторов, используя синтаксис, похожий на понимание.
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # raises StopIteration
Если функции не обязательно нужно передавать список, вы можете сэкономить на символах (и улучшить читабельность), поместив выражение генератора в вызов функции. Скобки из вызова функции неявно делают ваше выражение выражением-генератором.
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
Кроме того, вы будете экономить на памяти , потому что вместо загрузки всего списка вы итерация ( [0, 1, 2, 3]
в приведенном выше примере), генератор позволяет Python использовать значения по мере необходимости.
Вступление
Генератор выражение подобно список, словарь и набор постижений, но заключено в круглых скобках. Скобки не обязательно должны присутствовать, когда они используются в качестве единственного аргумента для вызова функции.
expression = (x**2 for x in range(10))
Этот пример генерирует 10 первых совершенных квадратов, включая 0 (в котором x = 0).
Функции генератора похожи на обычные функции, за исключением того, что они имеют один или более yield
заявления в своем теле. Такие функции не могут return
любые значения (однако пустое return
s разрешены , если вы хотите , чтобы остановить генератор рано).
def function():
for x in range(10):
yield x**2
Эта функция генератора эквивалентна предыдущему выражению генератора, она выводит то же самое.
Примечание: все выражения генератора имеют свои собственные эквивалентные функции, но не наоборот.
Выражение генератора можно использовать без скобок, если обе скобки будут повторяться в противном случае:
sum(i for i in range(10) if i % 2 == 0) #Output: 20
any(x = 0 for x in foo) #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1) #Output: <class 'generator'>
Вместо:
sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
Но нет:
fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)
Вызов функции генератора создает объект генератора, который впоследствии может перемещаться. В отличие от других типов итераторов, объекты-генераторы могут быть пройдены только один раз.
g1 = function()
print(g1) # Out: <generator object function at 0x1012e1888>
Обратите внимание на то, что тело генератора не выполняется сразу же: при вызове function()
в примере выше, она немедленно возвращает объект генератора, не выполняя даже первый оператор печати. Это позволяет генераторам использовать меньше памяти, чем функциям, которые возвращают список, и позволяет создавать генераторы, которые создают бесконечно длинные последовательности.
По этой причине генераторы часто используются в науке о данных и других контекстах, связанных с большими объемами данных. Другое преимущество состоит в том, что другой код может немедленно использовать значения, полученные генератором, не дожидаясь полной последовательности, которая будет произведена.
Тем не менее, если вам нужно использовать значения , полученные с помощью генератора более чем один раз, и если их генерации стоит больше , чем хранение, может быть лучше хранить получены значения в list
, чем повторно генерировать последовательность. См. «Сброс генератора» ниже для более подробной информации.
Обычно объект генератора используется в цикле или в любой функции, которая требует итерации:
for x in g1:
print("Received", x)
# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81
arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Так как объекты генератора итераторы, можно итерации по их вручную с помощью next()
функции. Это вернет полученные значения одно за другим при каждом последующем вызове.
Под капотом, каждый раз , когда вы звоните next()
на генераторе, Python выполняет операторы в теле функции генератора , пока он не достигнет следующей yield
заявление. В этот момент она возвращает аргумент yield
команды, и запоминает место , где это произошло. Вызов next()
еще раз возобновить выполнение с этого момента и продолжается до следующего yield
заявления.
Если Python достигает конца функции генератора не встречая больше yield
S, A StopIteration
возбуждается исключение (это нормально, все итераторы ведут себя таким же образом).
g3 = function()
a = next(g3) # a becomes 0
b = next(g3) # b becomes 1
c = next(g3) # c becomes 2
...
j = next(g3) # Raises StopIteration, j remains undefined
Обратите внимание , что в Python 2 объекты генератор имел .next()
методы , которые могут быть использованы для перебора значений , полученных в результате вручную. В Python 3 этот метод был заменен .__next__()
стандартом для всех итераторов.
Сброс генератора
Помните , что вы можете перемещаться только по объектам , генерируемых генератором один раз. Если вы уже итерации по объектам в скрипте, любая дальнейшая попытка сделать это не даст None
.
Если вам нужно использовать объекты, сгенерированные генератором более одного раза, вы можете либо снова определить функцию генератора и использовать ее во второй раз, либо, альтернативно, вы можете сохранить выходные данные функции генератора в списке при первом использовании. Переопределение функции генератора будет хорошим вариантом, если вы имеете дело с большими объемами данных, а сохранение списка всех элементов данных займет много места на диске. И наоборот, если изначально создавать элементы дорого, вы можете предпочесть сохранить сгенерированные элементы в списке, чтобы их можно было использовать повторно.
Используя генератор, чтобы найти числа Фибоначчи
Практический вариант использования генератора - перебирать значения бесконечного ряда. Вот пример нахождения первых десяти условий последовательности Фибоначчи .
def fib(a=0, b=1):
"""Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
while True:
yield a
a, b = b, a + b
f = fib()
print(', '.join(str(next(f)) for _ in range(10)))
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Бесконечные последовательности
Генераторы могут использоваться для представления бесконечных последовательностей:
def integers_starting_from(n):
while True:
yield n
n += 1
natural_numbers = integers_starting_from(1)
Бесконечная последовательность чисел , как описано выше , также может быть получена с помощью itertools.count
.Код выше может быть написан как ниже
natural_numbers = itertools.count(1)
Вы можете использовать генераторы на бесконечных генераторах для создания новых генераторов:
multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)
Имейте в виду , что бесконечный генератор не имеет конца, поэтому передавая его в любой функции , которая будет пытаться потреблять генератор полностью будет иметь пагубные последствия:
list(multiples_of_two) # will never terminate, or raise an OS-specific error
Вместо этого, список с помощью кнопок / установите постижения с range
(или xrange
для Python <3.0):
first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)]
# [3, 6, 9, 12, 15]
или использовать itertools.islice()
, чтобы нарезать итератор к подмножеству:
from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]
Обратите внимание, что оригинальный генератор также обновляется, как и все другие генераторы, исходящие из того же «корня»:
next(natural_numbers) # yields 16
next(multiples_of_two) # yields 34
next(multiples_of_four) # yields 24
Бесконечная последовательность также может повторяться с for
-loop.Убедитесь в том , чтобы включить условный break
оператор так , что цикл будет завершить в конце концов:
for idx, number in enumerate(multiplies_of_two):
print(number)
if idx == 9:
break # stop after taking the first 10 multiplies of two
Классический пример - числа Фибоначчи
import itertools
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def nth_fib(n):
return next(itertools.islice(fibonacci(), n - 1, n))
ninety_nineth_fib = nth_fib(99) # 354224848179261915075
Вывод всех значений из другого итерируемого
Используйте yield from
, если вы хотите , чтобы получить все значения из другого Iterable:
def foob(x):
yield from range(x * 2)
yield from range(2)
list(foob(5)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]
Это работает и с генераторами.
def fibto(n):
a, b = 1, 1
while True:
if a >= n: break
yield a
a, b = b, a + b
def usefib():
yield from fibto(10)
yield from fibto(20)
list(usefib()) # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]
Сопрограммы
Генераторы могут быть использованы для реализации сопрограмм:
# create and advance generator to the first yield
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return start
# example coroutine
@coroutine
def adder(sum = 0):
while True:
x = yield sum
sum += x
# example use
s = adder()
s.send(1) # 1
s.send(2) # 3
Сопрограммы обычно используются для реализации конечных автоматов, поскольку они в первую очередь полезны для создания процедур с одним методом, для которых требуется правильное функционирование состояния. Они работают в существующем состоянии и возвращают значение, полученное по завершении операции.
Выход с рекурсией: рекурсивный список всех файлов в каталоге
Сначала импортируйте библиотеки, которые работают с файлами:
from os import listdir
from os.path import isfile, join, exists
Вспомогательная функция для чтения только файлов из каталога:
def get_files(path):
for file in listdir(path):
full_path = join(path, file)
if isfile(full_path):
if exists(full_path):
yield full_path
Еще одна вспомогательная функция для получения только подкаталогов:
def get_directories(path):
for directory in listdir(path):
full_path = join(path, directory)
if not isfile(full_path):
if exists(full_path):
yield full_path
Теперь используйте эти функции для рекурсивного получения всех файлов в каталоге и всех его подкаталогах (используя генераторы):
def get_files_recursive(directory):
for file in get_files(directory):
yield file
for subdirectory in get_directories(directory):
for file in get_files_recursive(subdirectory): # here the recursive call
yield file
Эта функция может быть упрощена с помощью yield from
:
def get_files_recursive(directory):
yield from get_files(directory)
for subdirectory in get_directories(directory):
yield from get_files_recursive(subdirectory)
Итерация по генераторам параллельно
Чтобы перебрать несколько генераторов параллельно, используйте zip
встроенную команду:
for x, y in zip(a,b):
print(x,y)
Результаты в:
1 x
2 y
3 z
В Python 2 следует использовать itertools.izip
вместо этого. Здесь мы можем видеть , что все zip
функции дают кортежи.
Обратите внимание, что zip прекратит итерацию, как только в одном из элементов будет исчерпано количество элементов. Если вы хотите , чтобы итерацию до тех пор , как самый длинный Iterable, используйте itertools.zip_longest()
.
Рефакторинг списочно-строительного кода
Предположим, у вас есть сложный код, который создает и возвращает список, начиная с пустого списка и неоднократно добавляя к нему:
def create():
result = []
# logic here...
result.append(value) # possibly in several places
# more logic...
return result # possibly in several places
values = create()
Когда нецелесообразно заменять внутреннюю логику пониманием списка, вы можете превратить всю функцию в генератор на месте, а затем собрать результаты:
def create_gen():
# logic...
yield value
# more logic
return # not needed if at the end of the function, of course
values = list(create_gen())
Если логика является рекурсивной, использовать yield from
включить все значения из рекурсивного вызова в «плоских» результате:
def preorder_traversal(node):
yield node.value
for child in node.children:
yield from preorder_traversal(child)
поиск
next
функция полезна даже без перебора. Переходя выражение генератора на next
быстрый способ для поиска первого вхождения элемента , соответствующего некоторый предикат. Процедурный код вроде
def find_and_transform(sequence, predicate, func):
for element in sequence:
if predicate(element):
return func(element)
raise ValueError
item = find_and_transform(my_sequence, my_predicate, my_func)
можно заменить на:
item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.
Для этой цели может быть желательно , чтобы создать псевдоним, например, first = next
, или функцию обертки для преобразования исключения:
def first(generator):
try:
return next(generator)
except StopIteration:
raise ValueError