На этом шаге мы рассмотрим общий принцип решения этой задачи.
Вы хотите обрабатывать данные итеративно, в стиле обрабатывающего данные канала (похожего на канал Unix - он же конвейер). Например, у вас есть
огромный объем данных для обработки, который просто не поместится в память целиком.
Генераторы хорошо подходят для реализации обрабатывающих каналов. Предположим, что у вас есть огромный каталог с файлами логов, который вы хотите обработать:
foo/ access-log-012007.gz access-log-022007.gz access-log-032007.gz . . . . access-log-012008.gz bar/ access-log-092007.bz2 . . . . access-log-022008.bz2
Предположим, каждый файл содержит такие строки данных:
124.115.6.12 - - [10/Jul/2012:00:18:50 -0500] "GET /robots.txt ..." 200 71 210.212.209.67 - - [10/Jul/2012:00:18:51 -0500] "GET /ply/ ..." 200 11875 210.212.209.67 - - [10/Jul/2012:00:18:51 -0500] "GET /favicon.ico ..." 404 369 61.135.216.105 - - [10/Jul/2012:00:20:04 -0500] "GET /blog/atom.xml ..." 304 565 . . . .
Чтобы обработать эти файлы, вы могли бы создать коллекцию небольших генераторов, которые будут выполнять специфические замкнутые в себе задачи:
import os import fnmatch import gzip import bz2 import re def gen_find(filepat, top): ''' Находит все имена файлов в дереве каталогов, которые совпадают с шаблоном маски оболочки ''' for path, dirlist, filelist in os.walk(top): for name in fnmatch.filter(filelist, filepat): yield os.path.join(path,name) def gen_opener(filenames): ''' Открывает последовательность имен файлов, которая раз за разом производит файловый объект. Файл закрывается сразу же после перехода к следующему шагу итерации. ''' for filename in filenames: if filename.endswith('.gz'): f = gzip.open(filename, 'rt') elif filename.endswith('.bz2'): f = bz2.open(filename, 'rt') else: f = open(filename, 'rt') yield f f.close() def gen_concatenate(iterators): ''' Объединяет цепочкой последовательность итераторов в одну последовательность. ''' for it in iterators: yield from it def gen_grep(pattern, lines): ''' Ищет шаблон регулярного выражения в последовательности строк ''' pat = re.compile(pattern) for line in lines: if pat.search(line): yield line
Теперь вы можете легко совместить эти функции для создания обрабатывающего канала. Например, чтобы найти все файлы логов, которые содержат слово python, вы можете поступить так:
lognames = gen_find('access-log*', 'www') files = gen_opener(lognames) lines = gen_concatenate(files) pylines = gen_grep('(?i)python', lines) for line in pylines: print(line)
Если вы хотите еще расширить функциональность канала, то можете скармливать данные выражениям-генераторам. Например, эта версия находит количество переданных байтов и подсчитывает общую сумму:
lognames = gen_find('access-log*', 'www') files = gen_opener(lognames) lines = gen_concatenate(files) pylines = gen_grep('(?i)python', lines) bytecolumn = (line.rsplit(None,1)[1] for line in pylines) bytes = (int(x) for x in bytecolumn if x != '-') print('Total', sum(bytes))
Обработка данных в "каналообразной" манере отлично работает для решения широкого спектра задач: парсинга, чтения из источников данных в реальном времени, периодического опрашивания и т. д.
Для понимания представленного выше кода главное - уловить, что инструкция yield действует как своего рода производитель данных для цикла for, который действует как потребитель данных. Когда генераторы соединены, каждый yield скармливает один элемент данных следующему этапу канала, который потребляет его, совершая итерацию. В последнем примере функция sum() управляет всей программой, вытягивая один элемент за другим из канала (конвейера) генераторов.
Приятная особенность этого подхода заключается в том, что каждый генератор является маленьким и замкнутым на себе, поэтому их легко писать и поддерживать. Во многих случаях они получаются настолько универсальными, что могут быть переиспользованы в других контекстах. Получающийся код, который "склеивает" компоненты вместе, тоже обычно читается как простой для понимания рецепт.
Есть небольшая тонкость с использованием функции gen_concatenate(). Ее назначение - конкатенировать входные последовательности в одну длинную последовательность строк. itertools.chain() выполняет похожую функцию, но требует, чтобы все объединяемые итерируемые объекты были определены в качестве аргументов. В случае этого конкретного рецепта такой подход потребовал бы инструкции типа lines = itertools.chain(*files), которая заставила бы генератор gen_opener() быть полностью потребленным. Поскольку генератор производит последовательность открытых файлов, которые немедленно закрываются на следующем шаге итерации, chain() использовать нельзя. Показанное решение позволяет решить эту проблему.
Также в функции gen_concatenate() используется yield from для делегирования субгенератору. Объявление yield from it просто заставляет gen_concatenate() выдать все значения, произведенные генератором it. Это описано далее, в следующем шаге.
И последнее: стоит отметить, что "конвейерный" ("канальный") подход не сработает для всех на свете задач обработки данных. Иногда вам просто необходимо работать со всеми данными сразу. Однако даже в этом случае использование каналов генераторов может стать путем логического разбиения задачи.
Дэвид Бизли подробно написал об этих приемах в обучающей презентации "Трюки с генераторами для системных программистов".
http://www.dabeaz.com/generators/.
Если вам нужны дополнительные примеры, обратитесь к ней.
На следующем шаге мы рассмотрим превращение вложенной последовательности в плоскую.