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

Синтаксис

Параметры

Примечания