How to use generators and yield in python

Генераторы

Итак, наконец-то мы добрались до самого интересного! Генераторы являются безумно интересной и полезной штукой в Python. Генератор — это особый, более изящный случай итератора.

Используя генератор, вы можете создавать итераторы, вроде того, что мы рассмотрели выше, используя более лаконичный синтаксис, и не создавая при этом отдельный класс с методами  и .

Давайте внесём немного ясности:

  1. любой генератор является итератором (но не наоборот!);
  2. следовательно, любой генератор является «ленивой фабрикой», возвращающей значения последовательности по требованию.

Вот пример итератора последовательности чисел Фибоначчи в исполнении генератора:

Ну как? Не находите, что это выглядит намного элегантнее простого итератора? Весь секрет кроется в ключевом слове . Давайте разберёмся, что к чему.

Во-первых, обратите внимание, что является обычной функцией, ничего особенного. Однако же, в ней отсутствует оператор , возвращающий значение

В данном случае возвращаемым значением функции будет генератор (то есть, по сути, итератор — фабрика значений, сохраняющая состояние между обращениями к ней).

Теперь, когда происходит вызов функции

будет создан и возвращён экземпляр генератора. К данному моменту ещё никакого кода внутри функции не выполняется и генератор ожидает вызова.

Дальше созданный экземпляр генератора передаётся в качестве аргумента функции , которая также возвращает итератор, следовательно также никакого кода функции пока что не выполняется.

И, наконец, происходит вызов  с передачей в качестве аргумента итератора, возвращённого функцией . Чтобы смогла построить объект списка на основе полученного аргумента, ей необходимо получить все значения из этого аргумента. Для этого  выполняет последовательные вызовы метода  итератора, возвращённого вызовом , который, в свою очередь, выполняет последовательные вызовы в экземпляре итератора .

При первом запросе значения итератора будет выполнен код:

После чего произойдёт вход в тело цикла и оператор вернёт первое значение. На этом работа кода внутри будет приостановлена до следующего вызова . Значение, возвращенное при помощи yield, будет передано итератору , который передаст его в , таким образом в список добавится значение.

При втором и последующих обращениях к итератору будет выполняться код с того места, где было прервано выполнение, то есть после вызова оператора . После того, как этот когда будет выполнен, выполнение цикла начнётся заново и так же, как и при первом вызове, итератор будет возвращать очередное значение и «засыпать» в аккурат после вызова .

Весь описанный процесс повторится 10 раз, до тех пор, пока не запросит у одиннадцатый элемент, на что тот ответит выбросом исключения , что будет свидетельствовать о том, что достигнут конец последовательности

Обратите внимание, что одиннадцатый элемент не будет сохранён в конечном объекте списка и память, отведённая под него, будет освобождена сборщиком мусора

What are Generators in Python?

Generators are functions that return an iterable generator object. The values from the generator object are fetched one at a time instead of the full list together and hence to get the actual values you can use a for-loop, using next() or list() method.

Using Generator function

You can create generators using generator function and using generator expression.

A generator function is like a normal function, instead of having a return value it will have a yield keyword.

To create a generator function you will have to add a yield keyword. The following examples shows how to create a generator function.

def generator():
    yield "H"
    yield "E"
    yield "L"
    yield "L"
    yield "O"

test = generator()
for i in test:
    print(i)

Output:

H
E
L
L
O

How to read the values from the generator?

You can read the values from a generator object using a list(), for-loop and using next() method.

Using : list()

A list is an iterable object that has its elements inside brackets.Using list() on a generator object will give all the values the generator holds.

def even_numbers(n):
    for x in range(n):
       if (x%2==0): 
           yield x       
num = even_numbers(10)
print(list(num))

Output:

Using : for-in

In the example, there is a function defined even_numbers() that will give you all even numbers for the n defined. The call to the function even_numbers() will return a generator object, that is used inside for-loop.

Example:

def even_numbers(n):
    for x in range(n):
       if (x%2==0): 
           yield x       
num = even_numbers(10)
for i in num:
    print(i)

Output:

0
2
4
6
8

Using next()

The next() method will give you the next item in the list, array, or object. Once the list is empty, and if next() is called, it will give back an error with stopIteration signal. This error, from next() indicates that there are no more items in the list.

def even_numbers(n):
    for x in range(n):
       if (x%2==0): 
           yield x       
num = even_numbers(10)
print(next(num))
print(next(num))
print(next(num))
print(next(num))
print(next(num))
print(next(num))

Output:

0
2
4
6
8
Traceback (most recent call last):
  File "main.py", line 11, in <module>
    print(next(num))
StopIteration

Python Generator Expression

Simple generators can be easily created on the fly using generator expressions. It makes building generators easy.

Similar to the lambda functions which create anonymous functions, generator expressions create anonymous generator functions.

The syntax for generator expression is similar to that of a . But the square brackets are replaced with round parentheses.

The major difference between a list comprehension and a generator expression is that a list comprehension produces the entire list while the generator expression produces one item at a time.

They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.

Output


<generator object <genexpr> at 0x7f5d4eb4bf50>

We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.

Here is how we can start getting items from the generator:

When we run the above program, we get the following output:

1
9
36
100
Traceback (most recent call last):
  File "<string>", line 15, in <module>
StopIteration

Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.

Need Help?

Need help with any of this? Read BeginnersGuide/Help for mailing lists and newsgroups.

Most Python books will include an introduction to the language; see IntroductoryBooks for suggested titles.

Consult BeginnersGuide/Examples for small programs and little snippets of code that can help you learn.

Or, if you prefer to learn Python through listening to a lecture, you can attend a training course or even hire a trainer to come to your company. Consult the PythonEvents page to see if any training courses are scheduled in your area and the PythonTraining page for a list of trainers.

Teachers can join the EDU-SIG, a mailing list for discussion of Python’s use in teaching at any level ranging from K-12 up to university.

Что такое итератор (iterator)?

В Python итерируемый объект (iterable) — это любой объект, предоставляющий возможность поочерёдного прохода по своим элементам, а итератор (iterator) — это то, что выполняет реальный проход.

Вы можете получить итератор из любого итерируемого объекта в Python, используя функцию iter:

>>> iter()
<list_iterator object at 0x7f043a081da0>
>>> iter('hello')
<str_iterator object at 0x7f043a081dd8>

Если у вас есть итератор, единственное, что вы можете с ним сделать — это получить следующий элемент с помощью функции next:

>>> my_iterator = iter()
>>> next(my_iterator)
1
>>> next(my_iterator)
2

Вы получите исключение остановки итерации StopIteration, если запросите следующий элемент, но при этом все элементы будут пройдены:

>>> next(my_iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

И удобно, и несколько запутанно, все итераторы тоже итерируемы. Это означает, что вы можете получить итератор из итератора (то есть он вернет себя сам). То есть вы также можете перебирать итератор:

>>> my_iterator = iter()
>>> 

Важно отметить, что у итераторов есть состоянием. То есть, как только вы получили элемент из итератора, он удаляется из итератора (а не из источника данных)

Поэтому после того, как вы пройдете по всем элементам один раз, итератор будет пуст:

>>> my_iterator = iter()
>>> 

>>> 
[]

В Python 3 функции enumerate, zip, reversed и ряд других встроенных функций возвращают итераторы:

>>> enumerate(numbers)
<enumerate object at 0x7f04384ff678>
>>> zip(numbers, numbers)
<zip object at 0x7f043a085cc8>
>>> reversed(numbers)
<list_reverseiterator object at 0x7f043a081f28>

Генераторы (будь то из функции генератора или выражения генератора generator expressions) являются одним из наиболее простых способов создания собственных итераторов:

>>> numbers = 
>>> squares = (n**2 for n in numbers)
>>> squares
<generator object <genexpr> at 0x7f043a0832b0>

Я часто говорю, что итераторы — это ленивые одноразовые итерации. Они «ленивы», потому что у них есть возможность вычислять элементы, только тогда когда вы проходите через них. И они «одноразовые», потому что как только вы «получили (consumed)» элемент из итератора, он исчезнет навсегда. Термин «исчерпан (exhausted)» часто используется для полностью использованного итератора.

Это было краткое изложение того, что такое итераторы. Если вы раньше не сталкивались с итераторами, я бы порекомендовал почитать о них больше, прежде чем продолжить. Я написал статью, в которой объясняются итераторы, и я выступил с докладом Loop Better, о котором я упоминал ранее, в ходе которого я углубился в итераторы.

Python yield Example

Let’s say we have a function that returns a list of random numbers.

from random import randint

def get_random_ints(count, begin, end):
    print("get_random_ints start")
    list_numbers = []
    for x in range(0, count):
        list_numbers.append(randint(begin, end))
    print("get_random_ints end")
    return list_numbers


print(type(get_random_ints))
nums = get_random_ints(10, 0, 100)
print(nums)

Output:

<class 'function'>
get_random_ints start
get_random_ints end

It works great when the “count” value is not too large. If we specify count as 100000, then our function will use a lot of memory to store that many values in the list.

In that case, using yield keyword to create a generator function is beneficial. Let’s convert the function to a generator function and use the generator iterator to retrieve values one by one.

def get_random_ints(count, begin, end):
    print("get_random_ints start")
    for x in range(0, count):
        yield randint(begin, end)
    print("get_random_ints end")


nums_generator = get_random_ints(10, 0, 100)
print(type(nums_generator))
for i in nums_generator:
    print(i)

Output:

<class 'generator'>
get_random_ints start
70
15
86
8
79
36
37
79
40
78
get_random_ints end
  • Notice that the type of nums_generator is generator.
  • The first print statement is executed only once when the first element is retrieved from the generator.
  • Once, all the items are yielded from the generator function, the remaining code in the generator function is executed. That’s why the second print statement is getting printed only once and at the end of the for loop.

Rejected Ideas

Some ideas were discussed but rejected.

Suggestion: There should be some way to prevent the initial call to
__next__(), or substitute it with a send() call with a specified
value, the intention being to support the use of generators wrapped so
that the initial __next__() is performed automatically.

Resolution: Outside the scope of the proposal. Such generators should
not be used with yield from.

Suggestion: If closing a subiterator raises StopIteration with a
value, return that value from the close() call to the delegating
generator.

The motivation for this feature is so that the end of a stream of
values being sent to a generator can be signalled by closing the
generator. The generator would catch GeneratorExit, finish its
computation and return a result, which would then become the return
value of the close() call.

Resolution: This usage of close() and GeneratorExit would be
incompatible with their current role as a bail-out and clean-up
mechanism. It would require that when closing a delegating generator,
after the subgenerator is closed, the delegating generator be resumed
instead of re-raising GeneratorExit. But this is not acceptable,
because it would fail to ensure that the delegating generator is
finalised properly in the case where close() is being called for
cleanup purposes.

Signalling the end of values to a consumer is better addressed by
other means, such as sending in a sentinel value or throwing in an
exception agreed upon by the producer and consumer. The consumer can
then detect the sentinel or exception and respond by finishing its
computation and returning normally. Such a scheme behaves correctly
in the presence of delegation.

Suggestion: If close() is not to return a value, then raise an
exception if StopIteration with a non-None value occurs.

Resolution: No clear reason to do so. Ignoring a return value is not
considered an error anywhere else in Python.

Use of Python Generators

There are several reasons that make generators a powerful implementation.

1. Easy to Implement

Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.

The above program was lengthy and confusing. Now, let’s do the same using a generator function.

Since generators keep track of details automatically, the implementation was concise and much cleaner.

2. Memory Efficient

A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.

Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.

3. Represent Infinite Stream

Generators are excellent mediums to represent an infinite stream of data. Infinite streams cannot be stored in memory, and since generators produce only one item at a time, they can represent an infinite stream of data.

The following generator function can generate all the even numbers (at least in theory).

4. Pipelining Generators

Multiple generators can be used to pipeline a series of operations. This is best illustrated using an example.

Suppose we have a generator that produces the numbers in the Fibonacci series. And we have another generator for squaring numbers.

If we want to find out the sum of squares of numbers in the Fibonacci series, we can do it in the following way by pipelining the output of generator functions together.

Output

4895

This pipelining is efficient and easy to read (and yes, a lot cooler!).

Sub-proposal: decorator to explicitly request current behaviour

Nick Coghlan suggested that the situations where the current
behaviour is desired could be supported by means of a decorator:

from itertools import allow_implicit_stop

@allow_implicit_stop
def my_generator():
    ...
    yield next(it)
    ...

Which would be semantically equivalent to:

def my_generator():
    try:
        ...
        yield next(it)
        ...
    except StopIteration
        return

but be faster, as it could be implemented by simply permitting the
StopIteration to bubble up directly.

Single-source Python 2/3 code would also benefit in a 3.7+ world,
since libraries like six and python-future could just define their own
version of «allow_implicit_stop» that referred to the new builtin in
3.5+, and was implemented as an identity function in other versions.

Asynchronous Generator Object

The object is modeled after the standard Python generator object.
Essentially, the behaviour of asynchronous generators is designed
to replicate the behaviour of synchronous generators, with the only
difference in that the API is asynchronous.

The following methods and properties are defined:

  1. agen.__aiter__(): Returns agen.

  2. agen.__anext__(): Returns an awaitable, that performs one
    asynchronous generator iteration when awaited.

  3. agen.asend(val): Returns an awaitable, that pushes the
    val object in the agen generator. When the agen has
    not yet been iterated, val must be None.

    Example:

    async def gen():
        await asyncio.sleep(0.1)
        v = yield 42
        print(v)
        await asyncio.sleep(0.2)
    
    g = gen()
    
    await g.asend(None)      # Will return 42 after sleeping
                             # for 0.1 seconds.
    
    await g.asend('hello')   # Will print 'hello' and
                             # raise StopAsyncIteration
                             # (after sleeping for 0.2 seconds.)
    
  4. agen.athrow(typ, ]): Returns an awaitable, that
    throws an exception into the agen generator.

    Example:

    async def gen():
        try:
            await asyncio.sleep(0.1)
            yield 'hello'
        except ZeroDivisionError:
            await asyncio.sleep(0.2)
            yield 'world'
    
    g = gen()
    v = await g.asend(None)
    print(v)                # Will print 'hello' after
                            # sleeping for 0.1 seconds.
    
    v = await g.athrow(ZeroDivisionError)
    print(v)                # Will print 'world' after
                            $ sleeping 0.2 seconds.
    
  5. agen.aclose(): Returns an awaitable, that throws a
    GeneratorExit exception into the generator. The awaitable can
    either return a yielded value, if agen handled the exception,
    or agen will be closed and the exception will propagate back
    to the caller.

  6. agen.__name__ and agen.__qualname__: readable and writable
    name and qualified name attributes.

  7. agen.ag_await: The object that agen is currently awaiting
    on, or None. This is similar to the currently available
    gi_yieldfrom for generators and cr_await for coroutines.

  8. agen.ag_frame, agen.ag_running, and agen.ag_code:
    defined in the same way as similar attributes of standard generators.

Want to contribute?

  • Python is a product of the Python Software Foundation, a non-profit organization that holds the copyright. Donations to the PSF are tax-deductible in the USA, and you can donate via credit card or PayPal.

  • To report a bug in the Python core, use the Python Bug Tracker.

  • To contribute a bug fix or other patch to the Python core, read the Python Developer’s Guide for more information about Python’s development process.

  • To contribute to the official Python documentation, join the Documentation SIG, write to docs@python.org , or use the Issue Tracker to contribute a documentation patch.

  • To announce your module or application to the Python community, use comp.lang.python.announce. See for more information.

  • To propose changes to the Python core, post your thoughts to comp.lang.python. If you have an implementation, follow the Python Patch Guidelines.

  • If you have a question are not sure where to report it, check out the WhereDoIReportThis? page.

Такой умный yield

Так, в принципе, с несколькими точками выхода мы разобрались. А как же с несколькими точками входа? Неужели — это все, что мы можем сделать с генератором? Оказывается, нет.

Со времен старичка Python 2.5 у объекта генератора появилось еще несколько методов: , и . И это привело, можно сказать, к революции в области Data Flow Programming. С помощью теперь можно извне заставить генератор остановиться на следующем обращении к нему, а с помощью — заставить его бросить исключение:

А самое вкусное, как оказалось, спрятано в методе . Он позволяет отправить данные в генератор перед вызовом следующего блока кода!

Я не зря назвал здесь генератор словом coroutine. Корутина, или сопрограмма, — это как раз и есть та самая штука, о которой шепчутся программисты в переговорках офисов, обсуждая gevent, tornado и прочий eventlet. Более конкретно можно почитать в Википедии, а я, пожалуй, напишу о том, что корутины в таком вот виде чаще всего используют в Python для анализа потоков данных, реализуя кооперативную многозадачность.

Дело в том, что по вызову yield (как, в общем случае, и return) происходит передача управления. Сопрограмма сама решает, когда перенаправить flow в другое место (например, в другую сопрограмму). И это позволяет строить красивые разветвленные деревья обработки потоков данных, реализовывать MapReduce, возможно прокидывать текущие байты через сокет на другую ноду. Более того, сопрограммы могут быть фактически реализованы абсолютно на любом языке, равно как и утилиты командной строки в Linux, которые я приводил в пример в самом начале.

Случаи применения

Генераторы идеально подходят для чтения большого количества больших файлов, поскольку они выдают данные по одному фрагменту за раз, независимо от размера входного потока. Они также могут привести к более чистому коду путем разделения процесса итерации на более мелкие компоненты.

Пример 1

def emit_lines(pattern=None):
    lines = []
    for dir_path, dir_names, file_names in os.walk('test/'):
        for file_name in file_names:
            if file_name.endswith('.py'):
                for line in open(os.path.join(dir_path, file_name)):
                    if pattern in line:
                        lines.append(line)
    return lines

Эта функция просматривает набор файлов в указанном каталоге. Она открывает каждый файл и затем просматривает каждую строку, чтобы проверить соответствие шаблону.

Это прекрасно работает с небольшим количеством небольших файлов. Но что, если мы имеем дело с очень большими файлами? А что, если их много? К счастью, функция Python open() достаточно эффективна и не загружает весь файл в память. Но что, если наш список совпадений намного превышает доступную память на нашей машине?

Таким образом, вместо обработки нехватки места (при больших списках) и времени (почти бесконечный объем потока данных) при обработке больших объемов данных генераторы являются идеальным решением для использования, поскольку они выдают данные по одному за раз (вместо создание промежуточных списков).

Давайте посмотрим на версию вышеупомянутой проблемы для генератора и попытаемся понять, почему генераторы подходят для таких случаев использования с использованием конвейеров обработки.

Мы разделили весь наш процесс на три разных компонента:

  • Генерация множества имен файлов
  • Генерация всех строк из всех файлов
  • Фильтрация строк на основе сопоставления с образцом
def generate_filenames():
    """
    generates a sequence of opened files
    matching a specific extension
    """
    for dir_path, dir_names, file_names in os.walk('test/'):
        for file_name in file_names:
            if file_name.endswith('.py'):
                yield open(os.path.join(dir_path, file_name))

def cat_files(files):
    """
    takes in an iterable of filenames
    """
    for fname in files:
        for line in fname:
            yield line

def grep_files(lines, pattern=None):
    """
    takes in an iterable of lines
    """
    for line in lines:
        if pattern in line:
            yield line


py_files = generate_filenames()
py_file = cat_files(py_files)
lines = grep_files(py_file, 'python')
for line in lines:
    print (line)

В приведенном выше фрагменте мы не используем никаких дополнительных переменных для формирования списка строк, вместо этого мы создаем конвейер, который подает свои компоненты через процесс итерации по одному элементу за раз. grep_files принимает объект-генератор всех строк файлов *.py. Точно так же cat_file вставляет в объект генератора все имена файлов в каталоге. Таким образом весь конвейер склеивается с помощью итераций.

Пример 2

Генераторы отлично работают и для рекурсивного парсинга веб-страниц:

import requests
import re


def get_pages(link):
    links_to_visit = []
    links_to_visit.append(link)
    while links_to_visit:
        current_link = links_to_visit.pop(0)
        page = requests.get(current_link)
        for url in re.findall('<a href="(+)">', str(page.content)):
            if url == '/':
                url = current_link + url
            pattern = re.compile('https?')
            if pattern.match(url):
                links_to_visit.append(url)
        yield current_link


webpage = get_pages('http://sample.com')
for result in webpage:
    print(result)

Здесь мы просто выбираем по одной странице за раз, а затем выполняем какое-то действие на странице. Как бы это выглядело без генератора? Либо выборка и обработка должны происходить в одной и той же функции (что приводит к высокосвязанному коду, который трудно протестировать), либо нам нужно получить все ссылки перед обработкой одной страницы.

Генераторы: простой способ сделать итератор

Самый простой способ создать свои собственные итераторы в Python — это создать генератор.

Есть два способа сделать генераторы в Python.

Возьмем в качестве источника данных этот список номеров:

>>> favorite_numbers = 

Мы можем легко создать генератор, который будет лениво возвращать нам все квадраты этих чисел, например:

>>> def square_all(numbers):
...     for n in numbers:
...         yield n**2
...
>>> squares = square_all(favorite_numbers)

Или мы можем сделать такой же генератор, как этот:

>>> squares = (n**2 for n in favorite_numbers)

Первая называется функцией генератора, а вторая называется выражением генератора.

Оба этих объекта-генератора работают одинаково. Оба имеют тип генератора и оба являются итераторами, которые возвращают квадраты чисел из нашего списка чисел.

>>> type(squares)
<class 'generator'>
>>> next(squares)
36
>>> next(squares)
3249

Мы рассмотрим обоих этих подходах к созданию генератора подробнее, но сначала поговорим о терминологии.

Слово «генератор» используется в Python довольно часто:

  • Генератор, также называемый объект генератор, является итератором, тип которого является generator.
  • Функция генератора — это специальный синтаксис, который позволяет нам создать функцию, которая возвращает объект генератора, когда мы его вызываем.
  • Выражение генератора — это синтаксис, похожий на синтаксис генератора списков (comprehension list), который позволяет создавать встроенный объект генератора.

С учетом этой терминологии давайте рассмотрим каждую из этих вещей в отдельности. Сначала рассмотрим функции генератора.

Optimisations

Using a specialised syntax opens up possibilities for optimisation
when there is a long chain of generators. Such chains can arise, for
instance, when recursively traversing a tree structure. The overhead
of passing __next__() calls and yielded values down and up the
chain can cause what ought to be an O(n) operation to become, in the
worst case, O(n**2).

A possible strategy is to add a slot to generator objects to hold a
generator being delegated to. When a __next__() or send()
call is made on the generator, this slot is checked first, and if it
is nonempty, the generator that it references is resumed instead. If
it raises StopIteration, the slot is cleared and the main generator is
resumed.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector