Шаг 74.
Python: сборник рецептов.
Итераторы и генераторы. Создание каналов для обработки данных

    На этом шаге мы рассмотрим общий принцип решения этой задачи.

Задача

    Вы хотите обрабатывать данные итеративно, в стиле обрабатывающего данные канала (похожего на канал 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/.

    Если вам нужны дополнительные примеры, обратитесь к ней.

    На следующем шаге мы рассмотрим превращение вложенной последовательности в плоскую.




Предыдущий шаг Содержание Следующий шаг