Введение
В этом разделе рассматривается, как можно настроить итерацию с использованием генераторной функции.
Проблема
Предположим, что вы хотите создать собственный пользовательский паттерн итерации.
Например, обратный отсчет.
>>> 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, чтобы улучшить свои навыки.