【问题标题】:Why does itertools.product run through all elements at initialization?为什么 itertools.product 在初始化时会遍历所有元素?
【发布时间】:2020-02-04 08:29:59
【问题描述】:

我假设itertools.product 会同时生成一个元素。我现在注意到这不是真的。 简单的概念证明:

Class A:
  def __init__(self, n):
    self.source = iter(range(n))

  def __iter__(self):
    return self

  def __next__(self):
    val = next(self.source)
    print("I am at:", val)
    return val

现在如果我这样做:

from itertools import product
l = product(A(3), A(3))
print("Here")
next(l)

我希望有输出:

>'Here'
>'I am at 0'
>'I am at 0'

但我有

>'I am at 0'
>'I am at 1'
>'I am at 2'
>'I am at 0'
>'I am at 1'
>'I am at 2'
>'Here'

我错过了什么吗?

【问题讨论】:

    标签: iteration generator python-3.7


    【解决方案1】:

    要回答您的问题,我们需要查看itertools.product 的实现:

    def product(*args, repeat=1):
        pools = [tuple(pool) for pool in args] * repeat
        result = [[]]
        for pool in pools:
            result = [x+[y] for x in result for y in pool]
        for prod in result:
            yield tuple(prod)
    

    here你找到了真正的C实现,但是要回答这个问题,参考python就足够了(见底部的EXTRA段)。

    关注这行代码:

    pools = [tuple(pool) for pool in args] * repeat
    

    这样,两个迭代器的所有元素(取入输入)都转化为一个元组列表(仅在您第一次调用next()时),此时它们才真正被创建。

    回到您的代码,当您第一次调用next(l) 时,会创建迭代器的所有元素。在您的示例中,将使用以下元素创建 polls 列表:

    # pools: [(0, 1, 2), (0, 1, 2)]
    

    这就是你得到这些输出的原因。


    至于print("Here"),要了解为什么要先打印它,您需要了解生成器的工作原理:

    itertool.product() 返回一个生成器对象。生成器在受到第一个next() 刺激之前不会执行功能代码。随后,每次调用next() 都允许您计算下一个元素,只执行一次包含关键字yield 的循环。

    Here 你会找到很好的资源来更好地理解 python 生成器是如何工作的。


    为什么“itertools”选择将元组列表保存在内存中?

    因为笛卡尔积必须多次计算同一个元素,而迭代器不能只使用一次。


    额外

    在 C 中,元组 pools 的列表与 python 等效,正如您从这段代码中看到的那样,它被急切地评估。每个可迭代参数首先转换为一个元组:

    pools = PyTuple_New(npools);
    if (pools == NULL)
        goto error;
    
    for (i=0; i < nargs ; ++i) {
        PyObject *item = PyTuple_GET_ITEM(args, i);
        PyObject *pool = PySequence_Tuple(item);
        if (pool == NULL)
            goto error;
        PyTuple_SET_ITEM(pools, i, pool);
        indices[i] = 0;
    }
    for ( ; i < npools; ++i) {
        PyObject *pool = PyTuple_GET_ITEM(pools, i - nargs);
        Py_INCREF(pool);
        PyTuple_SET_ITEM(pools, i, pool);
        indices[i] = 0;
    }
    

    【讨论】:

    • 这不是它真正实现的方式,那你怎么能确定你的答案呢?
    • itertools.product 文档中有实现。如果您查看我的答案,还有一个指向文档的链接,您可以在其中查看代码。但是你可以在这里找到它:docs.python.org/3/library/itertools.html#itertools.product
    • “这个函数大致相当于下面的代码,只是实际的实现不会在内存中建立中间结果”——我在这里缺少什么?
    • 当然,itertools 是用 C 实现的(我在答案中添加了链接)。就这个问题而言,代码是 C 等价的,相当于他们在文档中给出的 python。元组列表以相同的方式“完全”创建。
    • 当然,我很高兴我也能为您的回答做出贡献;)
    【解决方案2】:

    我想指出,虽然class A 的两个实例都调用了__next__ 方法(直到遇到StopIteration),但itertools.product 迭代器仍然是惰性求值的,随后调用next。请注意:

    > '我在 0'

    > '我在 1'

    > '我在 2'

    > '我在 0'

    > '我在 1'

    > '我在 2'

    > '这里'

    只是首先为第一个传递的实例彻底调用next 的结果,然后是第二个传递的实例。这在调用product(A(2), A(3)) 时更容易看到,结果是:

    > '我在 0'

    > '我在 1'

    > '我在 0'

    > '我在 1'

    > '我在 2'

    combinationspermutations 观察到相同的行为。事实上,用“itertools.product 是否懒惰地评估其论点?”来寻找如此有见地的问题。把我带到this SO question,这也回答了你的问题。参数不会被延迟评估:

    因为product 有时需要多次遍历一个可迭代对象,如果将参数保留为只能使用一次的迭代器,这是不可能的。

    【讨论】:

    • 这很有意义。我认为@Massifox 的答案更广泛,所以如果在赏金结束时我没有得到其他答案,我会接受他的。但你真的很有帮助!
    猜你喜欢
    • 2016-12-18
    • 2020-04-12
    • 1970-01-01
    • 1970-01-01
    • 2012-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多