Вдохновленный доблестными усилиями @ schmichael's по созданию функционального решения Python, вот моя попытка слишком далеко зайти.Я не утверждаю, что он обслуживаемый, эффективный, примерный или пригодный для использования, но он функциональный:
from itertools import imap, groupby, izip, chain
from collections import deque
from operator import itemgetter, methodcaller
from functools import partial
def shifty_csv_dicts(lines):
last = lambda seq: deque(seq, maxlen=1).pop()
parse_header = lambda header: header[1:-1].split(',')
parse_row = lambda row: row.rstrip('\n').split(',')
mkdict = lambda keys, vals: dict(izip(keys,vals))
headers_then_rows = imap(itemgetter(1), groupby(lines, methodcaller('startswith', '#')))
return chain.from_iterable(imap(partial(mkdict, parse_header(last(headers))), imap(parse_row, next(headers_then_rows))) for headers in headers_then_rows)
Хорошо, давайте распакуем это.
Основная идея заключается в том, чтобы (ab) использоватьitertools.groupby
для распознавания изменений из заголовков в строки данных.Мы используем семантику оценки аргумента для управления порядком операций.
Сначала мы сообщаем groupby
сгруппировать строки по тому, начинаются ли они с '#'
:
methodcaller('startswith', '#')
создает функцию, которая принимает строку и вызывает line.startswith('#')
(ееэквивалентно стилистически предпочтительному, но менее эффективному lambda line: line.startswith('#')
).
Итак, groupby
принимает входящую итерируемую величину lines
и чередует возврат итерируемой строки заголовка (обычно только один заголовок) иповторяемость строк данных.Он на самом деле возвращает кортеж (group_val, group_iter)
, где в этом случае group_val
- это bool
, указывающий, является ли это заголовком.Итак, мы делаем эквивалент (group_val, group_iter)[1]
для всех кортежей, чтобы выбрать итераторы: itemgetter(1)
- это просто функция, которая запускает «[1]
» для всего, что вы ей даете (снова эквивалентно, но более эффективно, чем * 1026)*).Поэтому мы используем imap
, чтобы запустить нашу itemgetter
функцию для каждого кортежа, возвращенного groupby
, чтобы выбрать итераторы заголовка / данных:
imap(itemgetter(1), groupby(lines, methodcaller('startswith', '#')))
Сначала мы вычислим это выражение и дадим ему имя, потому чтомы будем использовать его дважды позже, сначала для заголовков, затем для данных.Внешний вызов:
chain.from_iterable(... for headers in headers_then_rows)
проходит через итераторы, возвращаемые из groupby
.Мы ведем себя хитро и вызываем значение headers
, потому что какой-то другой код внутри ...
отбирает rows
, когда мы не ищем, продвигая итератор groupby
в процессе.Это выражение внешнего генератора будет всегда создавать только заголовки (помните, они изменяют: заголовки, данные, заголовки, данные ...).Хитрость заключается в том, чтобы убедиться, что заголовки используются перед строками, потому что они оба используют один и тот же базовый итератор.chain.from_iterable
просто объединяет результаты всех итераторов строк данных в один итератор, чтобы вернуть их все.
Так что же мы соединяем?Что ж, нам нужно взять (последний) заголовок, сжать его с каждой строкой значений и сделать из этого подсказки.Это:
last = lambda seq: deque(seq, maxlen=1).pop()
- несколько грязный, но эффективный хак для получения последнего элемента от итератора, в нашем случае это строка заголовка.Затем мы анализируем заголовок, обрезая начальный #
и завершающий символ новой строки, и разделяем на ,
, чтобы получить список имен столбцов:
parse_header = lambda header: header[1:-1].split(',')
Но мы хотим сделать это только один раз для каждогоИтератор строк, потому что он исчерпывает наш итератор заголовков (и мы бы не хотели копировать его в какое-то изменяемое состояние, не так ли?).Мы также должны убедиться, что итератор заголовков используется перед строками.Решение состоит в том, чтобы создать частично примененную функцию, оценивая и фиксируя заголовки в качестве первого параметра, и принимая строку в качестве второго параметра:
partial(mkdict, parse_header(last(headers)))
Функция mkdict
использует имена столбцов в качестве ключей и строкиданные как значения, чтобы сделать диктовку:
mkdict = lambda keys, vals: dict(izip(keys,vals))
Это дает нам функцию, которая замораживает первый параметр (keys
) и позволяет просто передать второй параметр (vals
): именно то, что нам нужнодля создания набора диктов с одинаковыми ключами и разными значениями.
Чтобы использовать его, мы анализируем каждую строку так, как вы ожидаете:
parse_row = lambda row: row.rstrip('\n').split(',')
, напоминая, что next(headers_then_rows)
вернетитератор строк данных из groupby
(поскольку мы уже использовали итератор заголовков):
imap(parse_row, next(headers_then_rows))
Наконец, мы отображаем нашу частично примененную функцию dict-maker на проанализированные строки:
imap(partial(...), imap(parse_row, next(headers_then_rows)))
И все они сшиты chain.from_iterable
, чтобы создать один большой, счастливый, функциональный поток изменчивых CSV-диктов.
Для справки, это, вероятно, можно упростить, и я все равно буду делать вещи @sПуть Чмайкла.Но я понял, как это понять, и попробую применить эти идеи в решении Scala.