Документация по Python

Генераторы, итераторы и последовательности Python

В: Документация по 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

Синтаксис

Параметры

Примечания

Еще от кодкамп
Замечательно! Вы успешно подписались.
Добро пожаловать обратно! Вы успешно вошли
Вы успешно подписались на кодкамп.
Срок действия вашей ссылки истек.
Ура! Проверьте свою электронную почту на наличие волшебной ссылки для входа.
Успех! Ваша платежная информация обновлена.
Ваша платежная информация не была обновлена.