Мэппер и Редуктор с итераторами и генераторами - Hadoop Python

Давайте немного усовершенствуем код мэппера и редуктора отсюда:

1) mapper.py

#!/usr/bin/env python
"""Более сложный  Mapper, использующий Итераторы и генераторы Питона"""

import sys

# эта функция вернёт своебразный "массив массивов"
def read_input(file):
    for line in file:
        # разбивает строку на слова
        yield line.split() # возвращает генератор

"""Точка входа в мэппер с разделителем в виде таба в качестве разделителя по-умолчанию."""
def main(separator='\t'):
    # Передаём в качестве "файла" стандартный поток ввода
    data = read_input(sys.stdin)
    for words in data:
        # Пишем результаты в стандартный поток вывода
        # what we output here will be the input for the Выход мэппера будет входом редуктора
        # Reduce step, i.e. the input for reducer.py
        #
        # отделяем слово  табом от формального
# ("формального" так как сумма ещё не подсчитана - это будет сделано в редукторе)
# числа вхождений равного 1 
        for word in words:
            print '%s%s%d' % (word, separator, 1)

if __name__ == "__main__":
    main()

2) reducer.py

#!/usr/bin/env python

from itertools import groupby
from operator import itemgetter
import sys

# читаем выход мэппера
# и разбиваем каждую пару на ключ-значение
# по знаку табуляции
def read_mapper_output(file, separator='\t'):
    for line in file:
        yield line.rstrip().split(separator, 1)

def main(separator='\t'):
    # будем читать из стандартного потока ввода
    data = read_mapper_output(sys.stdin, separator=separator)
    # groupby groups multiple word-count pairs by word,
    # and creates an iterator that returns consecutive keys and their group:
    #   current_word - string containing a word (the key)
    #   group - iterator yielding all ["<current_word>", "<count>"] items
    # с помощью  groupby группирует пары "слово-число вхождений" 
# по значению слова и создаёт итератор
#
#
#
    for current_word, group in groupby(data, itemgetter(0)):
        try:
            total_count = sum(int(count) for current_word, count in group)
            print "%s%s%d" % (current_word, separator, total_count)
        except ValueError:
            # count was not a number, so silently discard this item
            pass

if __name__ == "__main__":
    main()

humanmashine's picture

Этот код:

data = read_mapper_output(sys.stdin, separator=separator)

Возвращает генератор, который при каждой итерации будет возвращать список из двух строк полученных расзделением строки. считанной из файла, оттталкиваясь от разделителя (сепаратора). Как я понял из предыдущих примеров, выглядить будет так: ['foo', '1'].

groupby(data, itemgetter(0))

Это выражение вернёт генератор, который будет при каждой итерации возвращать кортедж, первое значение - это сгруппированное значение в коллекции без повторений, тоесть если у нас такая коллекция: [['foo', '1'], ['foo', '1'], ['foo', '1']], то результат будет (['foo', '1'], [['foo', '1'], ['foo', '1'], ['foo', '1']]), причём жирный список, это утрированное представление значений генератора, по настоящему вместо жирного списка будет генератор и чтобы получить эти значения, надо его "проитерировать" (скажем применить функцияю list()).

Слудея вышесказанному, следующий цикл:

for current_word, group in groupby(data, itemgetter(0)):

Будет перебирать все слова в data, но уже без повторений, а сами повторения можно выбрать из group - так как это генератор, из которого можно достать ранее рассмотренные "жирные" значения. Собственно в цикле мы с помощью генератора списков:

[int(count) for current_word, count in group]

который формирует список чисел, находим сумму элементов списка, используя встроенную функцию sum().

vedro-compota's picture

м?)

_____________
матфак вгу и остальная классика =)

vedro-compota's picture

будет при каждой итерации возвращать кортедж...... то есть если у нас такая коллекция: [['foo', '1'], ['foo', '1'], ['foo', '1']], то результат будет (['foo', '1'], [['foo', '1'], ['foo', '1'], ['foo', '1']])

то есть первое значение этого кортежа - это тоже кортеж (пара - ['foo', '1'])
тогда получается, что в current_word попадёт не слово ....
выходит что кортеж такой: ('foo', [['foo', '1'], ['foo', '1'], ['foo', '1']]) ?

_____________
матфак вгу и остальная классика =)

humanmashine's picture

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

vedro-compota's picture

ок) в любом случае - ответ был отличный (в конце я пропишу комментарии в первом сообщений)
ещё уточнение:

for current_word, group 

у нас эта строка лихо пишет значения сразу в две переменные.
а может ли сразу в три, четыре и т.д.?

_____________
матфак вгу и остальная классика =)

humanmashine's picture

Этот вопрос можно выделить в отдельную тему. Тему присваивания. Дело в том, что в Python можно присваивать элементы картежа (или другой коллекции, скажем списка) отдельным переменным (Упаковка и распаковка). Это учень удобно для передачи и получений значений при работе с функциями.
Тут лучше пример:

a, b = ("trolala", "btrolb")  # в итоге a = "trolala", соответственно b = "btrolb"
a, b, c = [1, 2, 5]
print(c)  # выведет 5

def fun_func():
    a = "so"
    b = 4
    res = str(b) + a
    return res, a, b  #  возвращает кортеж из трёх элементов

a, b, c = fun_func()  # a = "so", b = 4, c = "4so"
print(c)  # выведет "4so"

sm = fun_func() 
print(sm)  # выведет кортеж ("so", 4, "4so")
vedro-compota's picture

В отдельную тему вынес. правильно ли я понимаю, что если попытаться присвоить результат из fun_func() двум переменным. то будет брошена ошибка?
типа:

def fun_func():
    a = "so"
    b = 4
    res = str(b) + a
    return res, a, b  #  возвращает кортеж из трёх элементов

sm, b = fun_func() # несогласованность числа значений??

наверное, это можно сразу как примечание написать там.

_____________
матфак вгу и остальная классика =)

humanmashine's picture

Правильно)))