【问题标题】:itertools or hand-written generator - what is preferable?itertools 或手写生成器 - 哪个更可取?
【发布时间】:2010-10-03 12:28:43
【问题描述】:

我有许多 Python 生成器,我想将它们组合成一个新的生成器。我可以通过使用一堆yield 语句的手写生成器轻松做到这一点。

另一方面,itertools 模块是为这样的事情而设计的,在我看来,创建我需要的生成器的 Python 方法似乎是将 itertools 模块的各种迭代器插入在一起。

然而,在手头的问题中,它很快变得相当复杂(生成器需要保持一种状态 --- 例如,是否正在处理第一个或后面的项目 ---,第 i 个输出进一步取决于第 i 个输入项的 on 条件和各种输入列表在加入到生成的列表之前必须进行不同的处理。

由于可以解决我的问题的标准迭代器的组成——由于编写源代码的一维性质——几乎难以理解,我想知道使用标准 itertools 生成器是否有任何优势与手写的生成器功能(在基本和更高级的情况下)。实际上,我认为在 90% 的情况下,手写版本更容易阅读 —— 可能是因为与链式迭代器的函数式风格相比,它们更具命令性。

编辑

为了说明我的问题,这里有一个(玩具)示例:让ab 成为两个长度相同的迭代(输入数据)。 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']

这可能比我的第一个解决方案更优雅,但它看起来像是一段只写一次,以后不理解的代码。我想知道这种编写生成器的方式是否有足够的优势让人们这样做。

P.S.:我想我对函数式解决方案的部分问题是,为了尽量减少 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)。

【问题讨论】:

  • P.S.:我刚刚发现我必须坚持使用生成器功能。它可以做一件事,我不能用生成器表达式做,即捕获异常。

标签: python iterator generator


【解决方案1】:

我做了一些分析,常规生成器函数比您的第二个生成器或我的实现要快得多。

$ 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

【讨论】:

  • @Marc,您仍然没有解决真正的困惑,即您是否希望“参数是:”和 i 单独产生或作为元组产生。
  • 更正了第一段代码(我很抱歉造成混乱,但我匆忙输入了第一个生成器函数,这让我写了 while 而不是 for。)至于“yield x; yield y" 和 "(x, y)":输出将被另一个 for 循环消耗,所以就我的目的而言,输出是像 "(x, y)" 还是 "iter(( x, y))”。但是在我上面的示例中,函数 generator2 输出一个生成器,其行为与函数生成器的输出完全相同。我没有看到 generator2 输出元组(我已经在我的系统上运行了代码。)
  • @Marc。你是对的。我的错,第二个读起来很混乱,但现在我看了一下,很清楚。
  • 非常感谢您的版本生成器3(它向我展示了可以使用大量迭代器编写可读性很强的代码)和您的分析。我已经在我的计算机上重复了测试,并在时间之间获得了相同的比率。目前,我将因此坚持生成器功能,这是最容易理解的。然而,当人们也可以编写生成器函数时,这仍然留下了何时使用迭代器的问题。 (或者应该将迭代器看作是 Python 发行版的 operator 模块中的便利函数?)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-10-27
  • 2018-02-04
  • 1970-01-01
  • 2018-01-20
  • 2014-05-20
  • 2015-02-05
  • 1970-01-01
相关资源
最近更新 更多