Настройка итерации с использованием генераторных функций

Beginner

This tutorial is from open-source community. Access the source code

Введение

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

Проблема

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

Например, обратный отсчет.

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

Есть простой способ сделать это.

Генераторы

Генератор - это функция, которая определяет итерацию.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

Например:

>>> for x in countdown(10):
...   print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1
>>>

Генератором является любая функция, которая использует оператор yield.

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

def countdown(n):
    ## Добавлено выражение print
    print('Counting down from', n)
    while n > 0:
        yield n
        n -= 1
>>> x = countdown(10)
## НЕТ ВЫРАЖЕНИЯ print
>>> x
## x - это объект-генератор
<generator object at 0x58490>
>>>

Функция выполняется только при вызове __next__().

>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.__next__()
Counting down from 10
10
>>>

yield возвращает значение, но приостанавливает выполнение функции. Функция продолжает выполнение при следующем вызове __next__().

>>> x.__next__()
9
>>> x.__next__()
8

Когда генератор в конце возвращает значение, итерация вызывает ошибку.

>>> x.__next__()
1
>>> x.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in? StopIteration
>>>

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

Упражнение 6.4: Простой генератор

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

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

>>> def filematch(filename, substr):
        with open(filename, 'r') as f:
            for line in f:
                if substr in line:
                    yield line

>>> for line in open('portfolio.csv'):
        print(line, end='')

name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
>>> for line in filematch('portfolio.csv', 'IBM'):
        print(line, end='')

"IBM",50,91.10
"IBM",100,70.44
>>>

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

Упражнение 6.5: Мониторинг потока данных в реальном времени

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

Программа stocksim.py - это программа, которая симулирует данные фондового рынка. В качестве вывода программа постоянно записывает в файл stocklog.csv данные в реальном времени. В отдельном окне командной строки перейдите в директорию с программой и запустите ее:

$ python3 stocksim.py

Если вы используете Windows, просто найдите программу stocksim.py и дважды кликните по ней, чтобы запустить. Теперь не обращайте внимания на эту программу (просто пусть она работает). В другом окне посмотрите на файл stocklog.csv, который пишет симулятор. Вы должны видеть, что каждые несколько секунд в файл добавляются новые строки текста. Опять же, просто пусть эта программа работает в фоновом режиме - она будет работать несколько часов (вам не нужно беспокоиться о ней).

Как только вышеуказанная программа запущена, напишем небольшую программу, которая откроет файл, переместит указатель в конец и будет наблюдать за новым выводом. Создайте файл follow.py и вставьте в него этот код:

## follow.py
import os
import time

f = open('stocklog.csv')
f.seek(0, os.SEEK_END)   ## Переместить указатель файла на 0 байт от конца файла

while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   ## Небольшая задержка и повторная попытка
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Если вы запустите программу, вы увидите реальный тикер фондовых цен. Под капотом этот код похож на команду Unix tail -f, которая используется для наблюдения за файлом журнала.

Примечание: Использование метода readline() в этом примере несколько необычно, так как это не обычный способ чтения строк из файла (обычно вы бы просто использовали цикл for). Однако в этом случае мы используем его, чтобы постоянно проверять конец файла, чтобы понять, были ли добавлены новые данные (readline() либо возвращает новые данные, либо пустую строку).

Упражнение 6.6: Использование генератора для генерации данных

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

Измените код в упражнении 6.5 так, чтобы чтение файла выполнялось с помощью генераторной функции follow(filename). Дополните код так, чтобы он работал следующим образом:

>>> for line in follow('stocklog.csv'):
          print(line, end='')

... Здесь должны появиться строки вывода...

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

if __name__ == '__main__':
    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Упражнение 6.7: Мониторинг вашего портфеля

Измените программу follow.py так, чтобы она следила за потоком данных о акциях и выводила тикер, показывающий информацию только о тех акциях, которые находятся в портфеле. Например:

if __name__ == '__main__':
    import report

    portfolio = report.read_portfolio('portfolio.csv')

    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if name in portfolio:
            print(f'{name:>10s} {price:>10.2f} {change:>10.2f}')

Примечание: Для того чтобы это работало, ваша класс Portfolio должен поддерживать оператор in. См. упражнение 6.3 и убедитесь, что вы реализуете оператор __contains__().

Обсуждение

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

Резюме

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