Как написать свой итератор python

Некоторое время назад я написал статью о протоколе итераторов, который поддерживает цикл for в Python. Одна вещь, которую я пропустил в этой статье, это то, как создавать свои собственные итераторы.

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

Что такое итератор?

Сначала давайте быстро рассмотрим, что такое итератор. Для более подробного объяснения посмотрите мой доклад Loop Better или прочитайте статью на основе этого доклада.

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

Итератор (iterator) – это объект, который выполняет фактический проход по элементам.

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

>>> favorite_numbers = [6, 57, 4, 7, 68, 95]
>>> iter(favorite_numbers)
<list_iterator object at 0x7fe8e5623160>

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

>>> favorite_numbers = [6, 57, 4, 7, 68, 95]
>>> my_iterator = iter(favorite_numbers)
>>> next(my_iterator)
6
>>> next(my_iterator)
57

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

Зачем нужны итераторы?

Итераторы позволяют вам сделать поочередно перебрать элементы, которые будут вычисляется по мере их поступления. Использование итератора вместо списка list, набора set или другой итерируемой структуры данных может иногда позволить нам сэкономить память. Например, мы можем использовать itertools.repeat, чтобы создать итерируемый объект с большим количество элементов:

>>> from itertools import repeat
>>> lots_of_fours = repeat(4, times=100_000_000)

Этот итератор занимает всего 56 байт памяти на моей машине:

>>> import sys
>>> sys.getsizeof(lots_of_fours)
56

Эквивалентный список из 400 миллионов элементов занимает гораздо больше памяти:

>>> lots_of_fours = [4] * 100_000_000
>>> import sys
>>> sys.getsizeof(lots_of_fours)
800000064

Хотя итераторы могут экономить память, они также могут экономить время. Например, если вы хотите распечатать только первую строку 10-гигабайтного файла логов, вы можете легко сделать так:

>>> print(next(open('giant_log_file.txt')))
This is the first line in a giant file

Файловые объекты в Python реализованы то же как итераторы. При прохождению по файлу данные считываются в память по одной строке за раз. Если бы мы вместо этого использовали метод readlines для хранения всех строк в памяти, мы могли бы быстро израсходовать все системную память.

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

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

Например, утилита itertools.count предоставит нам итератор, который будет возвращать каждый раз число от 0 и выше:

>>> from itertools import count
>>> for n in count():
...     print(n)
...
0
1
2
(это будет продолжаться бесконечно)

Создание итератора: объектно-ориентированный путь

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

Давайте создадим наш собственный итератор. А начнем мы с созданием объекта итератора такого же как itertools.count.

Вот итератор Count, реализованный с использованием класса:

class Count:

    """Iterator that counts upward forever."""

    def __init__(self, start=0):
        self.num = start

    def __iter__(self):
        return self

    def __next__(self):
        num = self.num
        self.num += 1
        return num

Этот класс имеет конструктор, который инициализирует наше текущее число в 0 (или что-либо переданное в качестве start). То, что делает этот класс в качестве итератора, это методы __iter__ и __next__.

Когда итерируемый объект передается во встроенную функцию str, вызывается его метод __str__. Когда объект передается во встроенную функции len, вызывается его метод __len__.

>>> numbers = [1, 2, 3]
>>> str(numbers), numbers.__str__()
('[1, 2, 3]', '[1, 2, 3]')
>>> len(numbers), numbers.__len__()
(3, 3)

Встроенной функции iter для итерируемого объекта попытается вызвать его метод __iter__. Встроенная функции next для объекта попытается вызвать его метод __next__.

Предполагается, что функция iter возвращает итератор. Поэтому наша функция __iter__ должна возвращать итератор. Но наш объект сам является итератором, поэтому должен вернуть себя. Поэтому наш объект Count возвращает self из своего метода __iter__.

Функция next должна возвращать следующий элемент в нашем итераторе или вызывать исключение StopIteration, когда элементов больше нет. Мы возвращаем текущий номер и увеличиваем его, чтобы он был больше во время следующего вызова __next__.

Мы можем вручную запустить наш класс итератора Count следующим образом:

>>> c = Count()
>>> next(c)
0
>>> next(c)
1

Мы также можем использовать объект Count в цикле for, как с любым другим итератором:

>>> for n in Count():
...     print(n)
...
0
1
2
(это будет продолжаться бесконечно)

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

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

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

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

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

>>> favorite_numbers = [6, 57, 4, 7, 68, 95]

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

>>> 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), который позволяет создавать встроенный объект генератора.

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

Функции генератора

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

Обычно, когда вы вызываете функцию, ее код сразу выполняется:

>>> def gimme4_please():
...     print("Let me go get that number for you.")
...     return 4
...
>>> num = gimme4_please()
Let me go get that number for you.
>>> num
4

Но если в функции есть оператор yield, она больше не является типичной функцией. Теперь это функция генератора, то есть она будет возвращать объект генератора при вызове. Этот объект генератора может быть зациклен при выполнение, пока не будет достигнут оператор yield:

>>> def gimme4_later_please():
...     print("Let me go get that number for you.")
...     yield 4
...
>>> get4 = gimme4_later_please()
>>> get4
<generator object gimme4_later_please at 0x7f78b2e7e2b0>
>>> num = next(get4)
Let me go get that number for you.
>>> num
4

Простое присутствие оператора yield превращает функцию в функцию генератора. Это немного странно, но именно так работают функции генератора.

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

def count(start=0):
    num = start
    while True:
        yield num
        num += 1

Так же, как и наш класс итератора Count, мы можем вручную зациклить генератор, который мы получаем после вызова count:

>>> c = count()
>>> next(c)
0
>>> next(c)
1

И мы можем зациклить этот объект генератора, используя цикл for, как и раньше:

>>> for n in count():
...     print(n)
...
0
1
2
...

Но эта функция значительно короче, чем наш класс Count, который мы создали ранее.

Выражение генератор

Выражение генератор – это синтаксис, похожий на генератор списков , который позволяет нам создавать объект генератора.

Допустим, у нас есть генератор списка, который фильтрует пустые строки из файла и удаляет новые строки с конца:

lines = [
    line.rstrip('n')
    for line in poem_file
    if line != 'n'
]

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

ines = (
    line.rstrip('n')
    for line in poem_file
    if line != 'n'
)

Подобно тому, как наш генератор списков возвращает нам список, наше выражение генератора возвращает нам объект генератора:

>>> type(lines)
<class 'generator'>
>>> next(lines)
' This little bag I hope will prove'
>>> next(lines)
'To be not vainly made--'

Выражения генератора используют более короткий встроенный синтаксис по сравнению с функциями генератора. Хотя они не такие мощные.

Вы можете написать свою функцию генератора в такой форме:

def get_a_generator(some_iterable):
    for item in some_iterable:
        if some_condition(item):
            yield item

Затем вы можете заменить его на выражение генератора:

def get_a_generator(some_iterable):
    return (
        item
        for item in some_iterable
        if some_condition(item)
    )

Выражения генератора против функции генератора

Вы можете думать о выражениях генератора как о списочном понимании мира генератора.

Если вы не знакомы с генераторами списков (list comprehension), я рекомендую прочитать мою статью list comprehensions in Python. В этой статье я отмечаю, что вы можете копировать и вставлять свой код из цикла for в list comprehension.

Вы также можете скопировать и вставить свой код из функции генератора в функцию, которая возвращает выражение генератора:

Выражения генератора относятся к функциям генератора, а генераторы списков (list comprehension) – к простому циклу for с добавлением и условием.

Выражения генератора настолько похожи на выражения, что вы даже можете испытать соблазн сказать «generator comprehension» по аналогии с list comprehension вместо выражения генератора. В русском языке нет устойчивого перевода названия generator comprehension. Хотя технически это не правильное имя, но если вы скажете это, все поймут, о чем вы говорите. Нед Бэтчелдер фактически предложил, чтобы мы все начали называть выражения генератора как generator comprehensions , и я склонен согласиться с тем, потому что это будет более понятное имя.

Так какой же способ лучший что бы создать итератор?

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

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

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

Я бы порекомендовал использовать функции генератора так же, как вы используете цикл for для создания списка. Везде, где вы видите метод append, вы часто можете использовать вместо этого выражение yield.

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

Генераторы тоже могут помочь при создании итерируемых объектов

Вы часто встретите классы итераторов, но на самом деле не так часть встречается хорошая возможность написать свои собственные.

Хотя возможность создать свой собственный класс итераторов встречается не так уж часто, создать собственный итеративный класс совсем не сложно. Итеративные классы требуют метода __iter__, который возвращает итератор. Поскольку генераторы – это простой способ создать итератор, мы можем использовать функцию генератора или выражение генератора для создания своих методов __iter__.

Например, вот итерируемый объект, который предоставляет координаты x-y:

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __iter__(self):
        yield self.x
        yield self.y

Обратите внимание, что наш класс Point здесь создает итерацию при вызове (не итератор). Это означает, что наш метод __iter__ должен возвращать итератор. Самый простой способ создать итератор – создать функцию генератора, так что это именно то, что мы и сделали.

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

>>> p = Point(1, 2)
>>> x, y = p
>>> print(x, y)
1 2
>>> list(p)
[1, 2]

Функции генератора естественным образом подходят для создания методов __iter__ в ваших итерируемых классах.

Генераторы – это способ создания итераторов.

Словари являются типичным способом составления мапирования в Python. Функции – это типичный способ создания вызываемого объекта в Python. Аналогично, генераторы являются типичным способом создания итератора в Python.

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

И когда вы думаете о том, как создать свой собственный итератор, подумайте о функциях генератора и выражениях генератора.

Оригинал: How to make an iterator in Python

Была ли вам полезна эта статья?

I wrote an article sometime ago on the iterator protocol that powers Python’s for loops.
One thing I left out of that article was how to make your own iterators.

In this article I’m going to discuss why you’d want to make your own iterators and then show you how to do so.

    What is an iterator?

    First let’s quickly address what an iterator is.
    For a much more detailed explanation, consider watching my Loop Better talk or reading the article based on the talk.

    An iterable is anything you’re able to loop over.

    An iterator is the object that does the actual iterating.

    You can get an iterator from any iterable by calling the built-in iter function on the iterable.

    1
    2
    3
    
    >>> favorite_numbers = [6, 57, 4, 7, 68, 95]
    >>> iter(favorite_numbers)
    <list_iterator object at 0x7fe8e5623160>
    

    You can use the built-in next function on an iterator to get the next item from it (you’ll get a StopIteration exception if there are no more items).

    1
    2
    3
    4
    5
    6
    
    >>> favorite_numbers = [6, 57, 4, 7, 68, 95]
    >>> my_iterator = iter(favorite_numbers)
    >>> next(my_iterator)
    6
    >>> next(my_iterator)
    57
    

    There’s one more rule about iterators that makes everything interesting: iterators are also iterables and their iterator is themselves.
    I explain the consequences of that more fully in that Loop Better talk I mentioned above.

    Why make an iterator?

    Iterators allow you to make an iterable that computes its items as it goes.
    Which means that you can make iterables that are lazy, in that they don’t determine what their next item is until you ask them for it.

    Using an iterator instead of a list, set, or another iterable data structure can sometimes allow us to save memory.
    For example, we can use itertools.repeat to create an iterable that provides 100 million 4’s to us:

    1
    2
    
    >>> from itertools import repeat
    >>> lots_of_fours = repeat(4, times=100_000_000)
    

    This iterator takes up 56 bytes of memory on my machine:

    1
    2
    3
    
    >>> import sys
    >>> sys.getsizeof(lots_of_fours)
    56
    

    An equivalent list of 100 million 4’s takes up many megabytes of memory:

    1
    2
    3
    4
    
    >>> lots_of_fours = [4] * 100_000_000
    >>> import sys
    >>> sys.getsizeof(lots_of_fours)
    800000064
    

    While iterators can save memory, they can also save time.
    For example if you wanted to print out just the first line of a 10 gigabyte log file, you could do this:

    1
    2
    
    >>> print(next(open('giant_log_file.txt')))
    This is the first line in a giant file
    

    File objects in Python are implemented as iterators.
    As you loop over a file, data is read into memory one line at a time.
    If we instead used the readlines method to store all lines in memory, we might run out of system memory.

    So iterators can save us memory, but iterators can sometimes save us time also.

    Additionally, iterators have abilities that other iterables don’t.
    For example, the laziness of iterators can be used to make iterables that have an unknown length.
    In fact, you can even make infinitely long iterators.

    For example, the itertools.count utility will give us an iterator that will provide every number from 0 upward as we loop over it:

    1
    2
    3
    4
    5
    6
    7
    8
    
    >>> from itertools import count
    >>> for n in count():
    ...     print(n)
    ...
    0
    1
    2
    (this goes on forever)
    

    That itertools.count object is essentially an infinitely long iterable.
    And it’s implemented as an iterator.

    Making an iterator: the object-oriented way

    So we’ve seen that iterators can save us memory, save us CPU time, and unlock new abilities to us.

    Let’s make our own iterators.
    We’ll start be re-inventing the itertools.count iterator object.

    Here’s an iterator implemented using a class:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    class Count:
    
        """Iterator that counts upward forever."""
    
        def __init__(self, start=0):
            self.num = start
    
        def __iter__(self):
            return self
    
        def __next__(self):
            num = self.num
            self.num += 1
            return num
    

    This class has an initializer that initializes our current number to 0 (or whatever is passed in as the start).
    The things that make this class usable as an iterator are the __iter__ and __next__ methods.

    When an object is passed to the str built-in function, its __str__ method is called.
    When an object is passed to the len built-in function, its __len__ method is called.

    1
    2
    3
    4
    5
    
    >>> numbers = [1, 2, 3]
    >>> str(numbers), numbers.__str__()
    ('[1, 2, 3]', '[1, 2, 3]')
    >>> len(numbers), numbers.__len__()
    (3, 3)
    

    Calling the built-in iter function on an object will attempt to call its __iter__ method.
    Calling the built-in next function on an object will attempt to call its __next__ method.

    The iter function is supposed to return an iterator.
    So our __iter__ function must return an iterator.
    But our object is an iterator, so should return ourself.
    Therefore our Count object returns self from its __iter__ method because it is its own iterator.

    The next function is supposed to return the next item in our iterator or raise a StopIteration exception when there are no more items.
    We’re returning the current number and incrementing the number so it’ll be larger during the next __next__ call.

    We can manually loop over our Count iterator class like this:

    1
    2
    3
    4
    5
    
    >>> c = Count()
    >>> next(c)
    0
    >>> next(c)
    1
    

    We could also loop over our Count object like using a for loop, as with any other iterable:

    1
    2
    3
    4
    5
    6
    7
    
    >>> for n in Count():
    ...     print(n)
    ...
    0
    1
    2
    (this goes on forever)
    

    This object-oriented approach to making an iterator is cool, but it’s not the usual way that Python programmers make iterators.
    Usually when we want an iterator, we make a generator.

    Generators: the easy way to make an iterator

    The easiest ways to make our own iterators in Python is to create a generator.

    There are two ways to make generators in Python.

    Given this list of numbers:

    1
    
    >>> favorite_numbers = [6, 57, 4, 7, 68, 95]
    

    We can make a generator that will lazily provide us with all the squares of these numbers like this:

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

    Or we can make the same generator like this:

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

    The first one is called a generator function and the second one is called a generator expression.

    Both of these generator objects work the same way.
    They both have a type of generator and they’re both iterators that provide squares of the numbers in our numbers list.

    1
    2
    3
    4
    5
    6
    
    >>> type(squares)
    <class 'generator'>
    >>> next(squares)
    36
    >>> next(squares)
    3249
    

    We’re going to talk about both of these approaches to making a generator, but first let’s talk about terminology.

    The word “generator” is used in quite a few ways in Python:

    • A generator, also called a generator object, is an iterator whose type is generator
    • A generator function is a special syntax that allows us to make a function which returns a generator object when we call it
    • A generator expression is a comprehension-like syntax that allows you to create a generator object inline

    With that terminology out of the way, let’s take a look at each one of these things individually.
    We’ll look at generator functions first.

    Generator functions

    Generator functions are distinguished from plain old functions by the fact that they have one or more yield statements.

    Normally when you call a function, its code is executed:

    1
    2
    3
    4
    5
    6
    7
    8
    
    >>> def gimme4_please():
    ...     print("Let me go get that number for you.")
    ...     return 4
    ...
    >>> num = gimme4_please()
    Let me go get that number for you.
    >>> num
    4
    

    But if the function has a yield statement in it, it isn’t a typical function anymore.
    It’s now a generator function, meaning it will return a generator object when called.
    That generator object can be looped over to execute it until a yield statement is hit:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    >>> def gimme4_later_please():
    ...     print("Let me go get that number for you.")
    ...     yield 4
    ...
    >>> get4 = gimme4_later_please()
    >>> get4
    <generator object gimme4_later_please at 0x7f78b2e7e2b0>
    >>> num = next(get4)
    Let me go get that number for you.
    >>> num
    4
    

    The mere presence of a yield statement turns a function into a generator function.
    If you see a function and there’s a yield, you’re working with a different animal.
    It’s a bit odd, but that’s the way generator functions work.

    Okay let’s look at a real example of a generator function.
    We’ll make a generator function that does the same thing as our Count iterator class we made earlier.

    1
    2
    3
    4
    5
    
    def count(start=0):
        num = start
        while True:
            yield num
            num += 1
    

    Just like our Count iterator class, we can manually loop over the generator we get back from calling count:

    1
    2
    3
    4
    5
    
    >>> c = count()
    >>> next(c)
    0
    >>> next(c)
    1
    

    And we can loop over this generator object using a for loop, just like before:

    1
    2
    3
    4
    5
    6
    7
    
    >>> for n in count():
    ...     print(n)
    ...
    0
    1
    2
    (this goes on forever)
    

    But this function is considerably shorter than our Count class we created before.

    Generator expressions

    Generator expressions are a list comprehension-like syntax that allow us to make a generator object.

    Let’s say we have a list comprehension that filters empty lines from a file and strips newlines from the end:

    1
    2
    3
    4
    5
    
    lines = [
        line.rstrip('n')
        for line in poem_file
        if line != 'n'
    ]
    

    We could create a generator instead of a list, by turning the square brackets of that comprehension into parenthesis:

    1
    2
    3
    4
    5
    
    lines = (
        line.rstrip('n')
        for line in poem_file
        if line != 'n'
    )
    

    Just as our list comprehension gave us a list back, our generator expression gives us a generator object back:

    1
    2
    3
    4
    5
    6
    
    >>> type(lines)
    <class 'generator'>
    >>> next(lines)
    ' This little bag I hope will prove'
    >>> next(lines)
    'To be not vainly made--'
    

    Generator expressions use a shorter inline syntax compared to generator functions.
    They’re not as powerful though.

    If you can write your generator function in this form:

    1
    2
    3
    4
    
    def get_a_generator(some_iterable):
        for item in some_iterable:
            if some_condition(item):
                yield item
    

    Then you can replace it with a generator expression:

    1
    2
    3
    4
    5
    6
    
    def get_a_generator(some_iterable):
        return (
            item
            for item in some_iterable
            if some_condition(item)
        )
    

    If you *can’t write your generator function in that form, then you can’t create a generator expression to replace it.

    Note that we’ve changed the example we’re using because we can’t use a generator expression for our previous example (our example that re-implements itertools.count).

    Generator expressions vs generator functions

    You can think of generator expressions as the list comprehensions of the generator world.

    If you’re not familiar with list comprehensions, I recommend reading my article on list comprehensions in Python.
    I note in that article that you can copy-paste your way from a for loop to a list comprehension.

    You can also copy-paste your way from a generator function to a function that returns a generator expression:

    Generator expressions are to generator functions as list comprehensions are to a simple for loop with an append and a condition.

    Generator expressions are so similar to comprehensions, that you might even be tempted to say generator comprehension instead of generator expression.
    That’s not technically the correct name, but if you say it everyone will know what you’re talking about.
    Ned Batchelder actually proposed that we should all start calling generator expressions generator comprehensions and I tend to agree that this would be a clearer name.

    So what’s the best way to make an iterator?

    To make an iterator you could create an iterator class, a generator function, or a generator expression.
    Which way is the best way though?

    Generator expressions are very succinct, but they’re not nearly as flexible as generator functions.
    Generator functions are flexible, but if you need to attach extra methods or attributes to your iterator object, you’ll probably need to switch to using an iterator class.

    I’d recommend reaching for generator expressions the same way you reach for list comprehensions.
    If you’re doing a simple mapping or filtering operation, a generator expression is a great solution.
    If you’re doing something a bit more sophisticated, you’ll likely need a generator function.

    I’d recommend using generator functions the same way you’d use for loops that append to a list.
    Everywhere you’d see an append method, you’d often see a yield statement instead.

    And I’d say that you should almost never create an iterator class.
    If you find you need an iterator class, try to write a generator function that does what you need and see how it compares to your iterator class.

    Generators can help when making iterables too

    You’ll see iterator classes in the wild, but there’s rarely a good opportunity to write your own.

    While it’s rare to create your own iterator class, it’s not as unusual to make your own iterable class.
    And iterable classes require a __iter__ method which returns an iterator.
    Since generators are the easy way to make an iterator, we can use a generator function or a generator expression to create our __iter__ methods.

    For example here’s an iterable that provides x-y coordinates:

    1
    2
    3
    4
    5
    6
    
    class Point:
        def __init__(self, x, y):
            self.x, self.y = x, y
        def __iter__(self):
            yield self.x
            yield self.y
    

    Note that our Point class here creates an iterable when called (not an iterator).
    That means our __iter__ method must return an iterator.
    The easiest way to create an iterator is by making a generator function, so that’s just what we did.

    We stuck yield in our __iter__ to make it into a generator function and now our Point class can be looped over, just like any other iterable.

    1
    2
    3
    4
    5
    6
    
    >>> p = Point(1, 2)
    >>> x, y = p
    >>> print(x, y)
    1 2
    >>> list(p)
    [1, 2]
    

    Generator functions are a natural fit for creating __iter__ methods on your iterable classes.

    Generators are the way to make iterators

    Dictionaries are the typical way to make a mapping in Python.
    Functions are the typical way to make a callable object in Python.
    Likewise, generators are the typical way to make an iterator in Python.

    So when you’re thinking “it sure would be nice to implement an iterable that lazily computes things as it’s looped over,” think of iterators.

    And when you’re considering how to create your own iterator, think of generator functions and generator expressions.

    Practice making an iterator right now

    You won’t learn new Python skills by reading, you’ll learn them by writing code.

    If you’d like to practice making an iterator right now, sign up for Python Morsels using the form below and I’ll immediately give you an exercise to practice making an iterator.

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

    Во многих современных языках программирования используют такие сущности как итераторы. Основное их назначение – это упрощение навигации по элементам объекта, который, как правило, представляет собой некоторую коллекцию (список, словарь и т.п.). Язык Python, в этом случае, не исключение и в нем тоже есть поддержка итераторов. Итератор представляет собой объект перечислитель, который для данного объекта выдает следующий элемент, либо бросает исключение, если элементов больше нет.

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

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

    >>> num_list = [1, 2, 3, 4, 5]
    >>> for i in num_list:
        print(i)
    1
    2
    3
    4
    5
    

    Как уже было сказано, объекты, элементы которых можно перебирать в цикле for, содержат в себе объект итератор, для того, чтобы его получить необходимо использовать функцию iter(), а для извлечения следующего элемента из итератора – функцию next().

    >>> itr = iter(num_list)
    >>> print(next(itr))
    1
    >>> print(next(itr))
    2
    >>> print(next(itr))
    3
    >>> print(next(itr))
    4
    >>> print(next(itr))
    5
    >>> print(next(itr))
    Traceback (most recent call last):
      File "<pyshell#12>", line 1, in <module>
        print(next(itr))
    StopIteration
    

    Как видно из приведенного выше примера вызов функции next(itr) каждый раз возвращает следующий элемент из списка, а когда эти элементы заканчиваются, генерируется исключение StopIteration.

    Создание собственных итераторов

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

    class SimpleIterator:
        def __init__(self, limit):
            self.limit = limit
            self.counter = 0
    
        def __next__(self):
            if self.counter < self.limit:
                self.counter += 1
                return 1
            else:
                raise StopIteration
    
    s_iter1 = SimpleIterator(3)
    print(next(s_iter1))
    print(next(s_iter1))
    print(next(s_iter1))
    print(next(s_iter1))
    

    В нашем примере при четвертом вызове функции next() будет выброшено исключение StopIterationЕсли мы хотим, чтобы с данным объектом можно было работать в цикле for, то в класс SimpleIterator нужно добавить метод __iter__(), который возвращает итератор, в данном случае этот метод должен возвращать self.

    class SimpleIterator:
        def __iter__(self):
            return self
    
        def __init__(self, limit):
            self.limit = limit
            self.counter = 0
    
        def __next__(self):
            if self.counter < self.limit:
                self.counter += 1
                return 1
            else:
                raise StopIteration
    
    s_iter2 = SimpleIterator(5)
    for i in s_iter2:
        print(i)
    

    Генераторы

    Генераторы позволяют значительно упростить работу по конструированию итераторов. В предыдущих примерах, для построения итератора и работы с ним, мы создавали отдельный класс. Генератор – это функция, которая будучи вызванной в функции next() возвращает следующий объект согласно алгоритму ее работы. Вместо ключевого слова return в генераторе используется yield. Проще всего работу генератор посмотреть на примере. Напишем функцию, которая генерирует необходимое нам количество единиц.

    def simple_generator(val):
       while val > 0:
           val -= 1
           yield 1
    
    gen_iter = simple_generator(5)
    print(next(gen_iter))
    print(next(gen_iter))
    print(next(gen_iter))
    print(next(gen_iter))
    print(next(gen_iter))
    print(next(gen_iter))
    

    Данная функция будет работать точно также, как класс SimpleIterator из предыдущего примера.

    Ключевым моментом для понимания работы генераторов является то, при вызове yield функция не прекращает свою работу, а “замораживается” до очередной итерации, запускаемой функцией next(). Если вы в своем генераторе, где-то используете ключевое слово return, то дойдя до этого места будет выброшено исключение StopIteration, а если после ключевого слова return поместить какую-либо информацию, то она будет добавлена к описанию StopIteration.

    P.S.

    Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
    Книга: Pandas. Работа с данными

    <<< Python. Урок 14. Классы и объекты    Python. Урок 16. Установка пакетов в Python>>>

    В этой статье, посвященной итераторам и их применению в языке Python, мы также рассмотрим создание своих собственных методов __iter__() и __next__(), соберем свой собственный итератор, рассмотрим всю пользу итераторов и закрепим наши знания на примерах.

    Итак, начинаем наш туториал по итераторам в Python.

    2. Кто такие эти ваши итераторы?

    Итератор в языке программирования Python — это объект, который вы можете перебирать. То есть он возвращает по одному объекту за раз. Итератор Python, неявно реализован в таких конструкциях, как циклы for, comprehension’ах и генераторах Python. Функции iter() и next() как раз и образуют протокол итератора.

    Если мы можем получить итератор от объекта в Python, значит он итерабельный. Например, списки Python, кортежи и строки.

    3. А как создать итератор?

    Чтобы собрать итератор python3, мы используем функции iter() и next(). Давайте начнем с iter(), чтобы создать итератор.

    Сначала мы создаем список, который содержит все четные числа от 2 до 10.

    evens=[2,4,6,8,10]

    Затем мы применяем функцию iter() к этому списку Python, чтобы создать объект итератора. Мы храним его в переменной evenIterator.

    evenIterator=iter(evens)
    evenIterator
    <list_iterator object at 0x05E35410>

    Помните, итератор можно получить не только для списка, кортежи и даже множества отлично подойдут.

    iter((1,3,2))
    <tuple_iterator object at 0x05E35550>

    Теперь, чтобы получить доступ к первому элементу, мы применяем функцию next() к объекту итератора Python.

    next(evenIterator)
    2
    next(evenIterator)
    4
    next(evenIterator)
    6
    next(evenIterator)
    8
    next(evenIterator)
    10

    Мы достигли конца списка. Когда мы вызываем его еще раз, мы провоцируем ошибку StopIteration (исключение). Интерпретатор сразу же выбрасывает его.

    next(evenIterator)
    Traceback (most recent call last):
    File “<pyshell#442>”, line 1, in <module>
    next(evenIterator)
    StopIteration

    С методом iter() разобрались, посмотрим на __next__()

    Итак, вы можете пройтись по итератору в Python, используя метод __next __() вместо next(). (Ну, мало ли…)

    nums=[1,2,3]
    numIter=iter(nums)
    numIter.__next__()
    1
    >>> next(numIter)
    2
    >>> numIter.__next__()
    3
    >>> numIter.__next__()
    Traceback (most recent call last):
    File “<pyshell#448>”, line 1, in <module>
    numIter.__next__()
    StopIteration

    Мы можем увидеть этот метод с помощью функции dir().

    >>> dir(numIter)
    [‘__class__’, ‘__delattr__’, ‘__dir__’, ‘__doc__’, ‘__eq__’, ‘__format__’, ‘__ge__’, ‘__getattribute__’, ‘__gt__’, ‘__hash__’, ‘__init__’, ‘__init_subclass__’, ‘__iter__’, ‘__le__’, ‘__length_hint__’, ‘__lt__’, ‘__ne__’, ‘__new__’, ‘__next__’, ‘__reduce__’, ‘__reduce_ex__’, ‘__repr__’, ‘__setattr__’, ‘__setstate__’, ‘__sizeof__’, ‘__str__’, ‘__subclasshook__’]

    4. Цикл for для итераторов Python

    Вы также можете использовать цикл for в Python для итерации по какому-нибудь итерируемому объекту, например, по списку Python или по кортежу.

    for i in 'Python':
        print(i)
    P
    y
    t
    h
    o
    n

    Но как это на самом деле реализовано? Давайте взглянем.

    iter_obj=iter('Python')
    while True:
        try:
            i=next(iter_obj)
            print(i)
        except StopIteration
             break
    P
    y
    t
    h
    o
    n

    Это была простая версия. Вот как на самом деле реализован вышеприведенный цикл for.

    5. Наконец создаем свой собственный итератор

    Теперь вы знаете, как использовать итератор с функциями iter() и next(). Но мы не остановимся на этом. Теперь мы начнем с самого нуля.

    Мы реализуем следующий класс для создания итератора в Python для квадратов чисел от 1 до максимального указанного.

    class PowTwo:
    
        def __init__(self, max=0):
            self.max = max
    
        def __iter__(self):
            self.n = 0
            return self
    
        def __next__(self):
            if self.n <= self.max:
                result = 2 ** self.n
                self.n += 1
                return result
            else:
                raise StopIteration

    Здесь __init __() принимает значение max. Затем мы создаем объект «a» класса PowTwo с аргументом 4. Затем мы создаем итератор, используя iter(). Далее мы используем функцию next(), чтобы получать элементы один за другим.

    a=PowTwo(4)
    i=iter(a)
    next(i)
    1
    >>> next(i)
    2
    >>> next(i)
    4
    >>> next(i)
    8
    >>> next(i)
    16
    >>> next(i)
    Traceback (most recent call last):
    File “<pyshell#484>”, line 1, in <module>
    next(i)
    File “<pyshell#476>”, line 13, in __next__
    raise StopIteration
    StopIteration

    В качестве альтернативы вы можете использовать методы __iter __() и __next __() для этого объекта.

    j=a.__iter__()
    j.__next__()
    1
    >>> j.__next__()
    2
    >>> j.__next__()
    4
    >>> j.__next__()
    8
    >>> j.__next__()
    16
    >>> j.__next__()
    Traceback (most recent call last):
    File “<pyshell#491>”, line 1, in <module>
    j.__next__()
    File “<pyshell#476>”, line 13, in __next__
    raise StopIteration
    StopIteration

    Функция iter() вызывает метод __iter __() внутри себя.

    6. Бесконечный итератор

    В Python действительно возможно создать итератор, который никогда не исчерпывается. Функция iter() может принимать другой аргумент, называемый «страж». Этот страж является точкой выхода и работает следующим образом: как только значение, возвращаемое итератором равно значению стража, итератор заканчивается.

    Мы знаем, что функция int() без параметра внутри возвращает 0.

    >>> int()

    Теперь мы вызываем iter() с двумя аргументами — int и 1.

    >>> a=iter(int,1)

    Этот итератор Python никогда не исчерпает себя, он бесконечен. Это потому, что 0 никогда не равен 1. Серьезно, никогда.

    >>> next(a)
    >>> next(a)
    >>> next(a)
    >>> next(a)
    >>> next(a)

    И так далее.

    Чтобы создать бесконечный итератор Python с использованием класса, рассмотрим следующий пример.

    class Even:
            def __iter__(self):
                self.num=2
                return self
            def __next__(self):
                num=self.num
                self.num+=2
                return num             
    >>> e=Even()
    >>> i=iter(e)
    >>> next(i)
    2
    >>> next(i)
    4
    >>> next(i)
    6
    >>> next(i)
    8

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

    7. Преимущества итераторов языка Python

    Итератор в python экономит ресурсы. Чтобы получить все элементы, в памяти одновременно хранится только один элемент. В отличие от итератора, список должен хранить все значения одновременно.

    8. Делаем выводы

    В этой статье мы узнали об итераторах Python. Разве они не веселые и супер удобные? Итератор использует всего две функции — iter() и next(). Тем не менее, мы можем сделать наш собственный итератор в Python при помощи класса. Наконец, мы рассмотрели также бесконечные итераторы.

    Кроме того, если у вас есть какие-либо вопросы/сомнения, не стесняйтесь задавать их в поле для комментариев.

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

    Содержание статьи

    • Что такое итератор?
    • Зачем нужно создать итератор?
    • Объектно-ориентированный итератор
    • Генераторы: простой способ создания итератора
    • Функции-генераторы
    • Выражения-генераторы
    • Выражения-генераторы или функции-генераторы?
    • Лучший способ создания итератора

    Что такое итератор?

    Сначала давайте быстро разберемся, что такое итератор. Для более подробного объяснения посмотрите видео «Итератор и итерируемые объекты. Функции iter() и next()» от автора selfedu.

    Есть вопросы по Python?

    На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!

    Telegram Чат & Канал

    Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!

    Паблик VK

    Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!

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

    Итератор — это объект, который выполняет фактическую итерацию.

    Вы можете создать итератор из любого итерабельного объекта, вызвав встроенную функцию iter():

    favorite_numbers = [6, 57, 4, 7, 68, 95]

    data = iter(favorite_numbers)

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

    favorite_numbers = [6, 57, 4, 7, 68, 95]

    my_iterator = iter(favorite_numbers)

    print(next(my_iterator)) # Результат: 6

    print(next(my_iterator)) # Результат: 57

    Есть еще одно правило об итераторах, которое делает все намного интереснее: итераторы также являются итераторабельными объектами, а их итератор — это они сами.

    Зачем нужно создать итератор?

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

    Использование итератора вместо списка, множества или другой итерирабельной структуры данных иногда позволяет экономить память. Например, мы можем использовать itertools.repeat для создания итератора, который предоставит нам 100 миллионов четверок (4):

    from itertools import repeat

    lots_of_fours = repeat(4, times=100_000_000)

    На моем компьютере этот итератор занимает 56 байт памяти:

    import sys

    print(sys.getsizeof(lots_of_fours)) # 56

    Такой же список из 100 миллионов четверок созданный более примитивным способом занимает 762.94 Мб:

    import sys

    lots_of_fours = [4] * 100_000_000

    print(sys.getsizeof(lots_of_fours)) # 800000064 байт

    Хотя итераторы могут экономить память, они также могут экономить время. Например, если вы хотите вывести только первую строку из 10-гигабайтного файла с логами, вы можете сделать следующее:

    first_line = next(open(‘giant_log_file.txt’))

    print(first_line)

    # Вывод: Это первая строка из гигантского файла

    Файловые объекты в Python реализованы как итераторы. При итерации по файлу данные считываются в память по одной строке за раз. Если бы вместо этого мы использовали метод readlines для хранения всех строк в памяти, мы могли бы исчерпать всю системную память и убить процесс.

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

    Кроме того, у итераторов есть возможности, которых нет у других итерабельных объектов. Например, их «лень» можно использовать для создания итерабельных объектов неизвестной длины. На самом деле, можно даже создавать бесконечно длинные итераторы.

    Например, метод itertools.count создаст нам итератор, который будет выдавать каждое следующее число от 0 до «бесконечности» в зависимости когда вы завершите цикл:

    from itertools import count

    for n in count():

        print(n)

    Результат:

    0

    1

    2

    (это будет продолжаться вечность)

    Метод itertools.count по сути является бесконечно длинным итерабельным объектом. И он реализован как итератор.

    Объектно-ориентированный итератор

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

    Давайте создадим свои собственные итераторы. Для начала мы «изобретем» заново объект итератора itertools.count.

    Вот итератор, реализованный с помощью класса:

    class Count:

        «»«Итератор, который считает до бесконечности.»«»

        def __init__(self, start=0):

            self.num = start

        def __iter__(self):

            return self

        def __next__(self):

            num = self.num

            self.num += 1

            return num

    В этом классе есть конструктор, который инициализирует текущее число итератора на 0 (или то, что было передано в качестве начала из аргумента start). То, что превращает этот класс в итератора, это наличие методов __iter__ и __next__.

    Когда объект передается встроенной функции str, вызывается метод __str__. Когда объект передается встроенной функции len, вызывается ее метод __len__.

    numbers = [1, 2, 3]

    print(str(numbers), numbers.__str__())

    # Вывод: (‘[1, 2, 3]’, ‘[1, 2, 3]’)

    print(len(numbers), numbers.__len__())

    # Вывод: (3, 3)

    • Передав наш объект в функцию iter это приведет к попытке вызвать его метод __iter__.
    • Передав наш объект в функцию next это приведет к попытке вызвать его метод __next__.

    Предполагается, что функция iter возвращает итератор. По этой причине метод __iter__ должен возвращать итератор. Но наш объект сам по себе является итератором, поэтому он должен возвращать самого себя. Объект Count возвращает self из своего метода __iter__, так как он является собственным итератором.

    Функция next должна возвращать следующий элемент в итераторе или вызывать исключение StopIteration, если элементов больше нет. Мы возвращаем текущее число и увеличиваем его на единицу, чтобы оно было больше во время следующего вызова метода __next__.

    Мы можем вручную перебирать объект Count следующим образом:

    c = Count()

    print(next(c)) # Вывод: 0

    print(next(c)) # Вывод: 1

    print(next(c)) # Вывод: 2

    print(next(c)) # Вывод: 3

    Мы также можем перебирать объект Count, используя цикл for, как и любой другой итерабельный объект:

    for n in Count():

        print(n)

    Результат:

    0

    1

    2

    (это будет продолжаться вечно)

    Такой объектно-ориентированный подход к созданию итератора — это здорово, но это не типичный способ, которым Python-программисты создают итераторы. Обычно, когда нам нужен итератор, мы создаем генератор.

    Генераторы: простой способ создания итератора

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

    В Python есть два способа создания генераторов.

    Дан список чисел:

    favorite_numbers = [6, 57, 4, 7, 68, 95]

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

    def square_all(numbers):

        for n in numbers:

            yield n**2

    squares = square_all(favorite_numbers)

    Или мы можем сделать такой же генератор следующим образом:

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

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

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

    print(type(squares))

    # Вывод: <class ‘generator’>

    print(next(squares))

    # Вывод: 36

    print(next(squares))

    # Вывод: 3249

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

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

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

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

    Функции-генераторы

    Функции-генераторы отличаются от обычных функций тем, что в них есть один или несколько операторов yield.

    Обычно при вызове функции выполняется ее код:

    def gimme4_please():

        return 4

    num = gimme4_please()

    print(num) # Результат: 4

    Но если в теле функции есть оператор yield, то это уже не обычная функция. Теперь это функция-генератор, то есть при вызове она возвращает объект-генератор. Этот объект-генератор может выполняться в цикле до тех пор, пока не будет выполнен оператор yield:

    def gimme4_later_please():

        yield 4

    get4 = gimme4_later_please()

    print(get4)  # <generator object gimme4_later_please at 0x7f78b2e7e2b0>

    num = next(get4)

    print(num)  # Результат: 4

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

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

    def count(start=0):

        num = start

        while True:

            yield num

            num += 1

    Подобно классу-итератору Count, мы можем вручную перебирать генератор, полученный в результате вызова функции count:

    c = count()

    print(next(c))  # вывод: 0

    print(next(c))  # вывод: 1

    И мы можем перебирать этот объект генератора с помощью цикла for, как и раньше:

    for n in count():

        print(n)

    Результат:

    0

    1

    2

    (это будет продолжаться вечно)

    Согласитесь, что данная функция значительно короче и понятнее, чем класс Count, который мы создали ранее.

    Выражения-генераторы

    Выражения-генераторы — это синтаксис, похожий на синтаксис представления списка (list comprehension), который позволяет нам создать объект-генератор.

    Допустим, у нас есть представление-списка, который фильтрует пустые строки из файла и удаляет переход на новую строку в конце n:

    lines = [

        line.rstrip(‘n’)

        for line in file(‘esenin-berioza.txt’).readlines()

        if line != ‘n’

    ]

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

    lines = (

        line.rstrip(‘n’)

        for line in file(‘esenin-berioza.txt’).readlines()

        if line != ‘n’

    )

    Точно так же, как представление списков (list comprehension) вернуло бы нам список, выражение-генератор вернет нам объект-генератор:

    print(type(lines)) # <class ‘generator’>

    next_line = next(lines)

    print(next_line) # Вывод: ‘Белая береза’

    next_line = next(lines)

    print(next_line) # Вывод: ‘Под моим окном’

    next_line = next(lines)

    print(next_line) # Вывод: ‘Принакрылась снегом,’

    next_line = next(lines)

    print(next_line) # Вывод: ‘Точно серебром.’

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

    Вы можете написать свою функцию-генератор в такой форме:

    def get_a_generator(some_iterable):

        for item in some_iterable:

            if some_condition(item):

                yield item

    Затем вы можете заменить тело функции на выражение-генератор:

    def get_a_generator(some_iterable):

        return (

            item

            for item in some_iterable

            if some_condition(item)

        )

    Если вы не можете написать свою функцию-генератор в такой форме, то вы не сможете создать выражение-генератор для её замены.

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

    Выражения-генераторы или функции-генераторы?

    Выражения-генераторы можно рассматривать как представление-списков (list comprehensions) в мире генераторов.

    Если в не знакомы со представлениыем-списков, рекомендую прочитать об этом статью. В этой статье описывается путь от цикла for к list comprehensions.

    Также можно скопировать код из функции-генератора и вставить в обычную функцию которая возвращает выражение-генератор:

    Итераторы в Python

    Выражения-генераторы являются функциями-генераторами так же, как представление-списков являются простым циклом for с добавлением и условием.

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

    Нед Батчелдер фактически предложил, чтобы мы все начали называть выражения-генераторы (generator expressions) как представление-генераторов (generator comprehensions), и я склонен согласиться, что это было бы более понятным названием.

    Лучший способ создания итератора

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

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

    Я бы рекомендовал смотреть в сторону к выражениям-генераторам так же и представление-списков (list comprehensions). Если вы выполняете простую операцию вывода или фильтрации, выражение-генератор — отличное решение. Если вы делаете что-то более сложное, вам, скорее всего, понадобится функция-генератор.

    Я бы рекомендовал использовать функции-генераторы так же, как использование цикла for для добавления данных в список. Везде, где требуется метод append, вы зачастую увидите оператор yield вместо него.

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

    Генераторы могут помочь при создании итераторов

    Вы можете встретить классы-итераторы, но редко попадается хорошая возможность написать свой собственный.

    Если создание собственного класса-итератора — редкость, то создание собственного итерабельного класса — не такая уж редкость. Итерабельный класс требует наличия метода __iter__, который возвращает итератор. Поскольку генераторы — это простой способ создания итератора, мы можем использовать функцию-генератор или выражение-генератор для создания наших методов __iter__.

    Например, вот итератор, который предоставляет координаты x-y:

    class Point:

        def __init__(self, x, y):

            self.x, self.y = x, y

        def __iter__(self):

            yield self.x

            yield self.y

    Обратите внимание, что при вызове класса Point создается итерабельный объект (а не итератор). Это означает, что метод __iter__ должен возвращать итератор. Самый простой способ создать итератор — это создать функцию-генератор, что мы и сделали.

    Мы вставили yield в метод __iter__, чтобы превратить его в функцию-генератор, и теперь класс Point можно перебирать, как и любой другой итерабельный объект.

    p = Point(1, 2)

    x, y = p

    print(x, y) # Вывод: 1 2

    to_list =  list(p)

    print(to_list) # Вывод: [1, 2]

    Функции-генераторы естественным образом подходят для создания методов __iter__ в итерабельных классах.

    Генераторы — это способ создания итераторов

    Словари — типичный способ создания карт в Python. Функции — типичный способ создания вызываемого объекта в Python. Аналогично, генераторы — это типичный способ создания итератора в Python.

    Поэтому, когда вы думаете: «Было бы неплохо реализовать итерабельный объект, который бы лениво вычислял что-то по мере выполнения цикла», подумайте об итераторах.

    А когда вы думаете о том, как создать свой собственный итератор, вспомните о функциях-генераторах и выражениях-генераторах.

    Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.

    E-mail: vasile.buldumac@ati.utm.md

    Образование
    Universitatea Tehnică a Moldovei (utm.md)

    • 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
    • 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»

    В этой статье вы узнаете, что такое итераторы, зачем они нужны в Python и как создать пользовательский итератор с помощью протокола итератора.

    Что такое итератор

    Итератор — это объект, у которого есть:

    • метод __iter__ , который возвращает сам объект;
    • метод __next__, который возвращает следующий элемент. Если все элементы уже возвращены, метод вызывает исключение StopIteration.

    Методы __iter__ и __next__ называются протоколом итератора.

    Python позволяет использовать итераторы в циклах for, списковых включениях и других встроенных функциях, в том числе map, filter, reduce и zip.

    Пример итератора

    Ниже — пример класса-итератора Square, который возвращает квадраты чисел.

    class Square:
        def __init__(self, length):
            self.length = length
            self.current = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.current >= self.length:
                raise StopIteration
    
            self.current += 1
            return self.current ** 2

    Как это работает

    1. Инициализируем атрибуты length и current в методе __init__. Атрибут length определяет количество квадратных чисел, которые должен вернуть класс. А атрибут current отслеживает текущее целое число.
    2. Создаем метод __iter__, который возвращает объект self.
    3. Создаем метод __next__, который возвращает квадрат следующего числа. Если количество возвращенных квадратов чисел больше length, метод __next__ вызывает исключение StopIteration.

    Используем итератор

    Давайте используем только что созданный итератор Square из примера. Ниже показано, как это сделать в цикле for:

    square = Square(5)
    
    for sq in square:
        print(sq)

    Как это работает

    1. Создаем новый экземпляр класса Square.
    2. Используем цикл for для перебора элементов итератора Square.

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

    Если вы попытаетесь использовать итератор, который уже исчерпан, вы получите исключение StopIteration. Например:

    next(square) # ошибка StopIteration

    Кроме того, итератор нельзя перезапустить, потому что у него есть только метод __next__, который возвращает следующий элемент из коллекции.

    Что нужно запомнить

    • Итератор — это объект, реализующий методы __iter__ и __next__.
    • Итератор нельзя повторно использовать после возврата всех элементов.

    Урок 43. Асинхронное программирование в python. Корутины. Asyncio.

    Итераторы

    Во многих современных языках программирования используют такие сущности как итераторы. Основное их назначение – это
    упрощение навигации по элементам объекта, который, как правило, представляет собой некоторую коллекцию (список, словарь
    и т.п.). Язык Python, в этом случае, не исключение и в нем тоже есть поддержка итераторов. Итератор представляет собой
    объект перечислитель, который для данного объекта выдает следующий элемент, либо бросает исключение, если элементов
    больше нет.

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

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

    num_list = [1, 2, 3, 4, 5]
    for i in num_list:
        print(i)
    1
    2
    3
    4
    5

    Как уже было сказано, объекты, элементы которых можно перебирать в цикле for, содержат в себе объект итератор, для
    того, чтобы его получить необходимо использовать функцию iter(), а для извлечения следующего элемента из итератора –
    функцию
    next().

    itr = iter(num_list)
    print(next(itr))
    1
    print(next(itr))
    2
    print(next(itr))
    3
    print(next(itr))
    4
    print(next(itr))
    5
    print(next(itr))
    # Traceback(most recent call last):
    # File "<pyshell#12>", line1, in < module > print(next(itr))
    # StopIteration

    Как видно из приведенного выше примера вызов функции next(itr) каждый раз возвращает следующий элемент из списка, а
    когда эти элементы заканчиваются, генерируется исключение StopIteration.

    Последовательности и итерируемые объекты

    По-сути, вся разница, между последовательностями и итерируемыми объектами(Не итераторы), заключается в том, что в
    последовательностях элементы упорядочены.

    Так, последовательностями являются: списки, кортежи и даже строки.

    numbers = [1, 2, 3, 4, 5]
    letters = ('a', 'b', 'c')
    characters = 'habristhebestsiteever'
    numbers[1]
    2
    letters[2]
    'c'
    characters[11]
    's'
    characters[0:4]
    'habr'

    Итерируемые объекты же, напротив, не упорядочены, но, тем не менее, могут быть использованы там, где требуется итерация:
    цикл for, генераторные выражения, списковые включения — как примеры.

    # Can't be indexed
    unordered_numbers = {1, 2, 3}
    unordered_numbers[1]
    # Traceback(most recent call last):
    # File "<stdin>", line 1, in < module >
    # TypeError: 'set' object is not subscriptable
    
    users = {'males': 23, 'females': 32}
    users[1]
    # Traceback(most recent call last):
    # File "<stdin>", line 1, in < module >
    # KeyError: 1
    
    # Can be used as sequence
    [number ** 2 for number in unordered_numbers]
    [1, 4, 9]
    
    for user in users:
        print(user)
    
    males
    females

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

    Итераторы

    Как мы могли убедиться, цикл for не использует индексы. Вместо этого он использует так называемые итераторы.

    Итераторы — это такие штуки, которые, очевидно, можно итерировать :)
    Получить итератор мы можем из любого итерируемого объекта.

    Что бы сделать это явно нужно вызвать метод iter()

    set_of_numbers = {1, 2, 3}
    list_of_numbers = [1, 2, 3]
    string_of_numbers = '123'
    iter(set_of_numbers)
    # < set_iterator object at 0x7fb192fa0480 >
    iter(list_of_numbers)
    # < list_iterator object at 0x7fb193030780 >
    iter(string_of_numbers)
    # < str_iterator object at 0x7fb19303d320 >

    Что бы получить следующий объект из итератора нужно вызвать метод next()

    iterator = iter('123')
    next(iterator)
    '1'
    next(iterator)
    '2'
    next(iterator)
    '3'
    next(iterator)
    # Traceback(most recent call last):
    # File "<pyshell#12>", line1, in < module > print(next(itr))
    # StopIteration

    Как работает for

    Цикл for вызывает метод iter() и к полученному объекту применит метод next() пока не встретит
    исключение StopIteration

    Это называется протокол итерации. На самом деле он применяется не только в цикле for, но и в генераторном выражении
    и даже при распаковке и «звёздочке»:

    coordinates = [1, 2, 3]
    x, y, z = coordinates
    
    numbers = [1, 2, 3, 4, 5]
    a, b, *rest = numbers
    
    print(rest)
    [3, 4, 5]

    В случае, если мы передаём в iter итератор, то получаем тот же самый итератор

    >>> numbers = [1,2,3,4,5]
    >>> iter1 = iter(numbers)
    >>> iter2 = iter(iter1)
    >>> next(iter1)
    1
    >>> next(iter2)
    2
    >>> iter1 is iter2
    True
    

    Подытожим.

    Итерируемый объект — это что-то, что можно итерировать.

    Итератор — это сущность порождаемая функцией iter, с помощью которой происходит итерирование итерируемого объекта.

    Итератор не имеет индексов и может быть использован только один раз.

    Итераторы повсюду

    Мы уже видели много итераторов в Python. Я уже упоминал о том, что генераторы — это тоже итераторы. Многие встроенные
    функции является итераторами.

    Так, например, enumerate:

    >>> numbers = [1,2,3]
    >>> enumerate_var = enumerate(numbers)
    >>> enumerate_var
    <enumerate object at 0x7ff975dfdd80>
    >>> next(enumerate_var)
    (0, 1)
    

    А так же zip:

    >>> letters = ['a','b','c']
    >>> z = zip(letters, numbers)
    >>> z
    <zip object at 0x7ff975e00588>
    >>> next(z)
    ('a', 1)
    

    И даже open:

    >>> f = open('foo.txt')
    >>> next(f)
    'barn'
    >>> next(f)
    'bazn'
    

    В Python очень много итераторов, и, как уже упоминалось выше, они откладывают выполнение работы до того момента, как мы
    запрашиваем следующий элемент с помощью next. Так называемое, «ленивое» выполнение.

    Создание своих итераторов

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

    class SimpleIterator:
        def __init__(self, limit):
            self.limit = limit
            self.counter = 0
    
        def __next__(self):
            if self.counter < self.limit:
                self.counter += 1
                return 1
            raise StopIteration
    
    
    s_iter1 = SimpleIterator(3)
    print(next(s_iter1))
    print(next(s_iter1))
    print(next(s_iter1))
    print(next(s_iter1))

    В нашем примере при четвертом вызове функции next() будет выброшено исключение StopIteration. Если мы хотим, чтобы с
    данным объектом можно было работать в цикле for, то в класс SimpleIterator нужно добавить метод __iter__(),
    который возвращает итератор, в данном случае этот метод должен возвращать self.

    class SimpleIterator:
        def __iter__(self):
            return self
    
        def __init__(self, limit):
            self.limit = limit
            self.counter = 0
    
        def __next__(self):
            if self.counter < self.limit:
                self.counter += 1
                return 1
            raise StopIteration
    
    
    s_iter2 = SimpleIterator(5)
    for i in s_iter2:
        print(i)

    Выражение итератора

    Объект созданный при помощи list comprehension тоже является итератором.

    iterator = [i for i in range(10)]

    Генераторы

    Генераторы — это тоже итераторы

    Return VS Yield

    Ключевое слово return — это финальная инструкция в функции. Она предоставляет способ для возвращения значения. При
    возвращении весь локальный стек очищается. И новый вызов начнется с первой инструкции.

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

    Генератор vs. Функция

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

    Генератор использует yield для отправления значения пользователю, а у функции для этого есть return;

    • При использовании генератора вызовов yield может быть больше чем один;

    • Вызов yield останавливает исполнение и возвращает итератор, а return всегда выполняется последним;

    • Вызов метода next() приводит к выполнению функции генератора;

    • Локальные переменные и состояния сохраняются между последовательными вызовами метода next();

    • Каждый дополнительный вызов next() вызывает исключение StopIteration, если нет следующих элементов для обработки.

    Дальше пример функции генератора с несколькими yield.

    def testGen():
        x = 2
        print('Первый yield')
        yield x
    
        x *= 1
        print('Второй yield')
        yield x
    
        x *= 1
        print('Последний yield')
        yield x
    
    
    # Вызов генератора
    iter = testGen()
    
    # Вызов первого yield
    next(iter)
    
    # Вызов второго yield
    next(iter)
    
    # Вызов последнего yield
    next(iter)

    Вывод:

    Первый yield
    Второй yield
    Последний yield
    

    Генераторы тоже реализуют протокол итератора:

    Если генератор встречает return, то в этот момент, генерируется исключение StopIteration

    Если функция завершается без return, то после последней строки вызывается return без параметров, что и
    вызовет StopIteration в следующем примере:

    >>> def custom_range(number):
    ...     index = 0 
    ...     while index < number:
    ...         yield index
    ...         index += 1
    ... 
    >>> range_of_four = custom_range(4)
    >>> next(range_of_four)
    0
    >>> next(range_of_four)
    1
    >>> next(range_of_four)
    2
    >>> next(range_of_four)
    3
    >>> next(range_of_four)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    

    Когда использовать генератор?

    Есть много ситуаций, когда генератор оказывается полезным. Вот некоторые из них:

    • Генераторы помогают обрабатывать большие объемы данных. Они позволяют производить так называемые ленивые вычисления.

    • Подобным образом происходит потоковая обработка. Генераторы можно устанавливать друг за другом и использовать их как
      Unix-каналы.

    • Генераторы позволяют настроить одновременное исполнение

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

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

    Зачем использовать генераторы?

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

    Удобные для программистов

    Генератор кажется сложной концепцией, но его легко использовать в программах. Это хорошая альтернатива итераторам.

    Рассмотрим следующий пример реализации арифметической прогрессии с помощью класса итератора.

    Создание арифметической прогрессии с помощью класса итератора:

    class AP:
        def __init__(self, a1, d, size):
            self.ele = a1
            self.diff = d
            self.len = size
            self.count = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.count >= self.len:
                raise StopIteration
            elif self.count == 0:
                self.count += 1
                return self.ele
            else:
                self.count += 1
                self.ele += self.diff
                return self.ele
    
    
    for ele in AP(1, 2, 10):
        print(ele)

    Ту же логику куда проще написать с помощью генератора.

    Генерация арифметической прогрессии с помощью функции генератора:

    def ap(a1, d, size):
        count = 1
        while count <= size:
            yield a1
            a1 += d
            count += 1
    
    
    for ele in ap(1, 2, 10):
        print(ele)

    Экономия памяти

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

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

    Обработка больших данных

    Генераторы полезны при обработке особенно больших объемов данных, например, Big Data. Они работают как бесконечный поток
    данных.

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

    Следующий код теоретически может выдать все простые числа.

    Найдем все простые числа с помощью генератора:

    def find_prime():
        num = 1
        while True:
            if num > 1:
                for i in range(2, num):
                    if not num % i:
                        break
                else:
                    yield num
            num += 1
    
    
    for ele in find_prime():
        print(ele)

    Последовательность генераторов

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

    Цепочка нескольких операций с использованием pipeline генератора:

    def find_prime():
        num = 1
        while num < 100:
            if num > 1:
                for i in range(2, num):
                    if not num % i:
                        break
                else:
                    yield num
            num += 1
    
    
    def find_odd_prime(seq):
        for num in seq:
            if not num % 2:
                yield num
    
    
    a_pipeline = find_odd_prime(find_prime())
    
    for a_ele in a_pipeline:
        print(a_ele)

    В примере ниже связаны две функции. Первая находит все простые числа от 1 до 100, а вторая — выбирает нечетные.

    yield from

    Есть специальная конструкция yield from она нужна для:

    # Обычный yield
    def numbers_range(n):
        for i in range(n):
            yield i
    
    
    # yield from
    def numbers_range(n):
        yield from range(n)

    yield from принимает в качестве параметра итератор.

    Напоминаю, генератор это тоже итератор.

    А значит yield from может принимать другой генератор:

    def subgenerator():
        yield 'World'
    
    
    def generator():
        yield 'Hello'
        yield from subgenerator()  # Запрашиваем значение из субгенератора
        yield '!'
    
    
    for i in generator():
        print(i, end=' ')

    Это важнейшее свойство мы и будем использовать далее

    Генераторные выражения и особенности генераторов

    В случае использования генераторного выражения, мы не храним значения, а значит что мы можем использовать только 1 раз:

    gen = (x for x in range(0, 100 * 10000))
    100 in gen
    True
    100 in gen
    False

    Корутины

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

    О стандарте можно почитать тут PEP 342.

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

    def calc():
        history = []
        while True:
            x = yield
            if x == 'h':
                print(history)
                continue
            print(x)
            history.append(x)
    
    
    c = calc()
    
    next(c)  # Необходимая инициация. Можно написать c.send(None)
    c.send(1)  # Выведет 1
    c.send(100)  # Выведет 100
    c.send(666)  # Выведет 666
    c.send('h')  # Выведет [1, 100, 666]
    c.close()  # Закрываем генератор, данные сотрутся, генератор необходимо будет создавать заново.

    Пример с передачей более чем одного параметра

    def calc():
        history = []
        while True:
            x, y = (yield)
            if x == 'h':
                print(history)
                continue
            result = x + y
            print(result)
            history.append(result)
    
    
    c = calc()
    
    next(c)  # Необходимая инициация. Можно написать c.send(None)
    c.send((1, 2))  # Выведет 3
    c.send((100, 30))  # Выведет 130
    c.send((666, 0))  # Выведет 666
    c.send(('h', 0))  # Выведет [3, 130, 666]
    c.close()  # Закрывем генератор, данные сотрутся, генератор необходимо будет создавать заново.

    send, throw, close

    В python 2.5 добавили в генераторы возможность отправлять данные и эксепшены.

    • send — передача данных в корутину. send(None) — равносильно next

    • throw — передача исключения в корутину. Например, GeneratorExit, для выхода из корутины

    • close — для «закрытия» корутины, и очистки локальной памяти корутины

    Корутин как декоратор

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

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

    def coroutine(f):
        def wrap(*args, **kwargs):
            gen = f(*args, **kwargs)
            gen.send(None)
            return gen
    
        return wrap
    
    
    @coroutine
    def calc():
        history = []
        while True:
            x, y = (yield)
            if x == 'h':
                print(history)
                continue
            result = x + y
            print(result)
            history.append(result)

    Asyncio

    Начиная с Python 3.4 существует новый модуль asyncio, который вводит API для обобщенного асинхронного программирования.
    Мы можем использовать корутины с этим модулем для простого и понятного выполнения асинхронного кода. Мы можем
    использовать корутины вместе с модулем asyncio для простого выполнения асинхронных операций. Пример из официальной
    документации:

    import asyncio
    import datetime
    import random
    
    
    @asyncio.coroutine
    def display_date(num, loop):
        end_time = loop.time() + 50.0
        while True:
            print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
            if (loop.time() + 1.0) >= end_time:
                break
            yield from asyncio.sleep(random.randint(0, 5))
    
    
    loop = asyncio.get_event_loop()
    
    asyncio.ensure_future(display_date(1, loop))
    asyncio.ensure_future(display_date(2, loop))
    
    loop.run_forever()

    Мы создали функцию display_date(num, loop) которая принимает два аргумента, первый номер, а второй цикл событий, после
    чего наша корутина печатает текущее время. После чего используется ключевое слово yield from для ожидания результата
    выполнения asyncio.sleep – которая является корутиной, которая выполняется через указанное количество секунд (пауза
    выполнения), мы в своем коде передаем в эту функцию случайное количество секунд. После чего мы используем
    asyncio.ensure_future для планирования выполнения корутины в цикле событий. После чего мы указываем, что цикл событий
    должен работать бесконечно долго.

    Если мы посмотрим на вывод программы, то увидим, что две функции выполняются одновременно. Когда мы
    используем yield from, цикл обработки событий знает, что он будет какое-то время занят, поэтому он приостанавливает
    выполнение функции и запускает другую. Таким образом, две функции работают одновременно (но не параллельно, поскольку
    цикл обработки событий является однопоточным).

    Стоит отметить, yield from – это синтаксический сахар для for x in asyncio.sleep(random.randint(0, 5)): yield x
    который делает код чище и проще.

    Этот декоратор был удалён в python 3.8

    Встроенные корутины

    Помните, мы все еще используем функции на основе генератора? В Python 3.5 мы получили новые встроенные корутины, которые
    используют синтаксис async / await. Предыдущая функция может быть написана так:

    import asyncio
    import datetime
    import random
    
    
    async def display_date(num, loop):
        end_time = loop.time() + 10.0
        while True:
            print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
            if (loop.time() + 1.0) >= end_time:
                break
            await asyncio.sleep(random.randint(0, 5))
    
    
    loop = asyncio.get_event_loop()
    
    asyncio.ensure_future(display_date(1, loop))
    asyncio.ensure_future(display_date(2, loop))
    
    loop.run_forever()

    Фактически изменены были только строки 6 и 12, для определения встроенной корутины определение функции помечается
    ключевым словом async, а вместо yield from используется await.

    Корутины на генераторах и встроенные корутины

    Функционально нет никакой разницы между корутинами на генераторах и встроенными корутинами, кроме различия в синтаксисе.
    Кроме того не допускается смешивания их синтаксисов. То есть нельзя использовать await внутри корутин на генераторах или
    yield / yeild from внутри встроенных корутин.

    Несмотря на различия, мы можем организовывать взаимодействия между ними. Нам просто нужно добавить декоратор
    @types.coroutine к старым генераторам. Тогда мы можем использовать старый генератор из встроенных корутин и наоброт.

    Пример для python 3.6:

    import asyncio
    import datetime
    import random
    import types
    
    
    @types.coroutine
    def my_sleep_func():
        yield from asyncio.sleep(random.randint(0, 5))
    
    
    async def display_date(num, loop):
        end_time = loop.time() + 50.0
        while True:
            print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
            if (loop.time() + 1.0) >= end_time:
                break
            await my_sleep_func()
    
    
    loop = asyncio.get_event_loop()
    
    asyncio.ensure_future(display_date(1, loop))
    asyncio.ensure_future(display_date(2, loop))
    
    loop.run_forever()

    Asyncio. Loop, run, create_task, gather, etc.

    loop

    loop — один набор событий, до версии python 3.7 любые корутины запускались исключительно внутри loop

    Давайте рассмотрим пример, где отдельная корутина вычисляет факториал последовательно (сначала 2, потом 3, потом 4 итд),
    и делает паузу на одну секунду, перед следующим вычислением

    import asyncio
    
    
    async def factorial(name, number):
        f = 1
        for i in range(2, number + 1):
            print(f"Task {name}: Compute factorial({i})...")
            await asyncio.sleep(1)
            f *= i
        print(f"Task {name}: factorial({number}) = {f}")
    
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(factorial('A', 4))

    Обратите внимание, этот код будет работать на python 3.6+

    run

    То же самое для python 3.7+ будет выглядеть так:

    import asyncio
    
    
    async def factorial(name, number):
        f = 1
        for i in range(2, number + 1):
            print(f"Task {name}: Compute factorial({i})...")
            await asyncio.sleep(1)
            f *= i
        print(f"Task {name}: factorial({number}) = {f}")
    
    
    asyncio.run(factorial('A', 4))  # Добавлено в 3.7

    create_tasks

    Рассмотрим код, в котором основная корутина запускает две других.

    import asyncio
    import time
    
    
    async def say_after(delay, what):
        await asyncio.sleep(delay)
        print(what)
    
    
    async def main():
        print(f"started at {time.strftime('%X')}")
    
        await say_after(1, 'hello')
        await say_after(2, 'world')
    
        print(f"finished at {time.strftime('%X')}")
    
    
    asyncio.run(main())

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

    async def main():
        task1 = asyncio.create_task(
            say_after(1, 'hello'))
    
        task2 = asyncio.create_task(
            say_after(2, 'world'))
    
        print(f"started at {time.strftime('%X')}")
    
        # Подождите, пока обе задачи не будут выполнены (должны пройти
        # около 2 секунд.)
        await task1
        await task2

    Попытка запустить асинхронный метод синхронно не приведёт ни к чему, это просто не будет работать.

    import asyncio
    
    
    async def nested():
        return 42
    
    
    async def main():
        # Ничего не произойдет, если мы просто вызовем "nested()".
        # Объект корутины создан, но не await,
        # так что *не будет работать вообще*.
        nested()
    
        # Let's do it differently now and await it:
        print(await nested())  # will print "42".
    
    
    asyncio.run(main())

    gather

    Что если нам необходимо запустить асинхронно несколько одинаковых задач с разными параметрами? Нам поможет gather.

    Вернёмся к коду с факториалами

    import asyncio
    
    
    async def factorial(name, number):
        f = 1
        for i in range(2, number + 1):
            print(f"Task {name}: Compute factorial({i})...")
            await asyncio.sleep(1)
            f *= i
        print(f"Task {name}: factorial({number}) = {f}")
    
    
    async def main():
        # Запланировать дерево вызовов *конкурентно*:
        await asyncio.gather(
            factorial("A", 2),
            factorial("B", 3),
            factorial("C", 4),
        )
    
    
    asyncio.run(main())
    
    # Ожидаемый вывод:
    #
    #     Task A: Compute factorial(2)...
    #     Task B: Compute factorial(2)...
    #     Task C: Compute factorial(2)...
    #     Task A: factorial(2) = 2
    #     Task B: Compute factorial(3)...
    #     Task C: Compute factorial(3)...
    #     Task B: factorial(3) = 6
    #     Task C: Compute factorial(4)...
    #     Task C: factorial(4) = 24

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

    import asyncio
    
    
    async def factorial(name, number):
        f = 1
        for i in range(2, number + 1):
            print(f"Task {name}: Compute factorial({i})...")
            await asyncio.sleep(1)
            f *= i
        print(f"Task {name}: factorial({number}) = {f}")
        return f
    
    
    async def main():
        # Запланировать дерево вызовов *конкурентно*:
        res = await asyncio.gather(
            factorial("A", 4),
            factorial("B", 3),
            factorial("C", 2),
        )
        print(res)
    
    
    asyncio.run(main())

    Вы можете быть уверены в том, что в переменную res результаты придут именно в том порядке в котором вы их запросили, в
    примере результат всегда будет [24, 6, 2], никакой неожиданности

    Это далеко не все методы и подробности корутин, за всеми деталями
    в доку

    Aiohttp.

    Как мы помним одним из основных преимуществ использования асинхронности это возможность отправки параллельных http
    запросов, не дожидаясь результатов других. К сожалению используя корутиновый подход вместе с классическим requests,
    запросы будут выполнены синхронно, т.к. сами запросы не являются awaitable объектами, и результат будет таким же как
    если бы вы использовали обычный слип, а не асинхронных, соседние корутины будут ждать остальные, что бы такого не было,
    существует специальный пекедж aiohttp, его необходимо устанавливать через pip:

    pip install aiohttp

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

    import aiohttp
    import asyncio
    
    
    async def main():
        async with aiohttp.ClientSession() as session:
            async with session.get('http://httpbin.org/get') as resp:
                print(resp.status)
                print(await resp.text())
    
    
    asyncio.run(main())

    Объединив знания можно приступать к практике и сделать те же задачи, что и на прошлом занятии, но теперь при помощи
    корутин

    ПрактикаДомашка:

    1. Написать функцию, которая будет делать запросы на https://google.com, https://amazon.com, https://microsoft.com.
      Оценить время выполнения.
    • 1.1 сделать по 5 запросов на каждый сайт, получить время.
    1. Написать функцию, которая возводит числа 2, 3 и 5, в 1000000 степень. Оценить время выполнения, сделать выводы.

    Понравилась статья? Поделить с друзьями:
  • Как написать свой индикатор tradingview
  • Как написать свой игровой движок на python
  • Как написать свой законопроект
  • Как написать свой емейл правильно
  • Как написать свой емейл для электронной почты