Генераторы, итераторы и последовательности 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

Синтаксис

Параметры

Примечания