itertools или рукописный генератор - что предпочтительнее? - PullRequest
7 голосов
/ 03 октября 2010

У меня есть несколько генераторов Python, которые я хочу объединить в новый генератор.Я легко могу сделать это с помощью написанного от руки генератора, используя кучу операторов yield.

С другой стороны, модуль itertools создан для таких вещей, и мне кажется, чтоПитонный способ создания генератора, который мне нужен, - это объединить различные итераторы этого itertools модуля.

Однако в рассматриваемой задаче это вскоре становится довольно сложным (генератор должен поддерживать своего рода состояние--- например, обрабатываются ли первый или более поздние элементы ---, i-тый вывод дополнительно зависит от условий на i-тых элементах ввода, и различные списки ввода должны обрабатываться по-разному, прежде чем они будут присоединены ксгенерированный список.

Поскольку состав стандартных итераторов, которые могли бы решить мою проблему, - из-за одномерной природы записи исходного кода - почти непостижим, интересно, есть ли какие-либо преимуществаиспользование стандартных генераторов itertools по сравнению с функциями, написанными от руки (в основном и более продвинутомSES).На самом деле, я думаю, что в 90% случаев рукописные версии намного легче читать - вероятно, из-за их более императивного стиля по сравнению с функциональным стилем цепочки итераторов.

РЕДАКТИРОВАТЬ

Чтобы проиллюстрировать мою проблему, вот (игрушечный) пример: пусть a и b будут двумя итерациями одинаковой длины (входные данные).Элементы a состоят из целых чисел, элементы b сами являются итерируемыми, отдельные элементы которых являются строками.Вывод должен соответствовать выводу следующей функции генератора:

from itertools import *
def generator(a, b):
    first = True
    for i, s in izip(a, b):
        if first:
            yield "First line"
            first = False
        else:
            yield "Some later line"
        if i == 0:
            yield "The parameter vanishes."
        else:
            yield "The parameter is:"
            yield i
        yield "The strings are:"
        comma = False
        for t in s:
            if comma:
                yield ','
            else:
                comma = True
            yield t

Если я записываю ту же программу в функциональном стиле, используя выражения генератора и модуль itertools, я получаю что-то вроде:

from itertools import *
def generator2(a, b):
    return (z for i, s, c in izip(a, b, count())
            for y in (("First line" if c == 0 else "Some later line",),
                      ("The parameter vanishes.",) if i == 0
                      else ("The parameter is:", i),
                      ("The strings are:",),
                      islice((x for t in s for x in (',', t)), 1, None))
            for z in y)

ПРИМЕР

>>> a = (1, 0, 2), ("ab", "cd", "ef")
>>> print([x for x in generator(a, b)])
['First line', 'The parameter is:', 1, 'The strings are:', 'a', ',', 'b', 'Some later line', 'The parameter vanishes.', 'The strings are:', 'c', ',', 'd', 'Some later line', 'The parameter is:', 2, 'The strings are:', 'e', ',', 'f']
>>> print([x for x in generator2(a, b)])
['First line', 'The parameter is:', 1, 'The strings are:', 'a', ',', 'b', 'Some later line', 'The parameter vanishes.', 'The strings are:', 'c', ',', 'd', 'Some later line', 'The parameter is:', 2, 'The strings are:', 'e', ',', 'f']

Это, возможно, более элегантно, чем мое первое решение, но выглядит как произведение "один раз не пойми позже"кода.Мне интересно, имеет ли этот способ написания моего генератора достаточно преимуществ, чтобы его можно было сделать.

PS: Я полагаю, что часть моей проблемы с функциональным решением заключается в том, чтобы минимизировать количество ключевых слов в Python,некоторые ключевые слова, такие как «for», «if» и «else», были переработаны для использования в выражениях, так что их размещение в выражении требует привыкания (упорядоченность в выражении генератора z for x in a for y in x for z in y выглядит, по крайней мере для меня, менееестественно, чем порядок в классической for петле: for x in a: for y in x: for z in y: yield z).

1 Ответ

7 голосов
/ 03 октября 2010

Я выполнил некоторое профилирование, и функция обычного генератора работает намного быстрее, чем ваш второй генератор или моя реализация.

$ python -mtimeit -s'import gen; a, b = gen.make_test_case()' 'list(gen.generator1(a, b))'
10 loops, best of 3: 169 msec per loop

$ python -mtimeit -s'import gen; a, b = gen.make_test_case()' 'list(gen.generator2(a, b))'
10 loops, best of 3: 489 msec per loop

$ python -mtimeit -s'import gen; a, b = gen.make_test_case()' 'list(gen.generator3(a, b))'
10 loops, best of 3: 385 msec per loop

Это также оказывается наиболее читабельным, так что я думаю, что я пойду с этим. При этом я все еще опубликую свое решение, потому что я думаю, что это более чистый пример функционального программирования, которое вы можете сделать с помощью itertools (хотя, очевидно, все еще не оптимально, я чувствую, что оно должно быть в состоянии курить обычную функцию генератора. Я взломаю это)

def generator3(parameters, strings):
    # replace strings with a generator of generators for the individual charachters
    strings = (it.islice((char for string_char in string_ for char in (',', string_char)), 1, None)
               for string_ in strings)

    # interpolate strings with the notices
    strings = (it.chain(('The strings are:',), string_) for string_ in strings)

    # nest them in tuples so they're ate the same level as the other generators
    separators = it.chain((('First line',),), it.cycle((('Some later line',),)))

    # replace the parameters with the appropriate tuples
    parameters = (('The parameter is:', p) if p else ('The parameter vanishes.',)
                  for p in parameters)

    # combine the separators, parameters and strings
    output = it.izip(separators, parameters, strings)

    # flatten it twice and return it
    output = it.chain.from_iterable(output)
    return it.chain.from_iterable(output)   

для справки, контрольный пример:

def make_test_case():
    a = [i % 100 for i in range(10000)]
    b = [('12345'*10)[:(i%50)+1] for i in range(10000)]
    return a, b
...