itertools.product()接收空列表时应返回什么?

14 投票
2 回答
4992 浏览
提问于 2025-04-16 00:39

我觉得这是个学术性的问题,但第二个结果让我有点困惑。难道它不应该和第一个一样完全空吗?这种行为背后的原因是什么呢?

from itertools import product

one_empty = [ [1,2], [] ]
all_empty = []

print [ t for t in product(*one_empty) ]  # []
print [ t for t in product(*all_empty) ]  # [()]

更新

感谢大家的回答,信息量很大。

维基百科对空的笛卡尔积的讨论给出了明确的说明:

没有集合的笛卡尔积... 是一个只包含空元组的单元素集合。

这里有一些代码,你可以用来理解sth的回答

from itertools import product

def tproduct(*xss):
    return ( sum(rs, ()) for rs in product(*xss) )

def tup(x):
    return (x,)

xs = [ [1, 2],     [3, 4, 5]       ]
ys = [ ['a', 'b'], ['c', 'd', 'e'] ]

txs = [ map(tup, x) for x in xs ]  # [[(1,), (2,)], [(3,), (4,), (5,)]]
tys = [ map(tup, y) for y in ys ]  # [[('a',), ('b',)], [('c',), ('d',), ('e',)]]

a = [ p for p in tproduct( *(txs + tys) )                   ]
b = [ p for p in tproduct( tproduct(*txs), tproduct(*tys) ) ]

assert a == b

2 个回答

4

正如@sth已经提到的,从数学角度来看,这种行为是正确的。你只需要明白的是,list(itertools.product())应该只有一个元素,因为一旦你知道这一点,就很清楚这个元素应该是什么:它必须是一个长度为0的元组,而这样的元组只有一个。

但是,itertools.product(l1, l2, l3, ...)的元素数量应该是l1l2l3等的长度相乘的结果。所以,itertools.product()的元素数量应该是空乘积的大小,而网上有很多资料可以证明空乘积是1。

我想指出的是,这个定义在实际应用中也是正确的,和数学上的定义一样;也就是说,这个定义在边界情况下最有可能“正常工作”。举个例子,假设你想生成所有长度为n的字符串,这些字符串由十进制数字组成,且第一个数字不能为零。你可能会这样做:

import itertools

def decimal_strings(n):
    """Generate all digit strings of length n that don't start with 0."""
    for lead_digit in '123456789':
        for tail in itertools.product('0123456789', repeat=n-1):
            yield lead_digit + ''.join(tail)

n = 1时,这应该产生什么结果呢?在这种情况下,你实际上是用一个空乘积(repeat = 0)来调用itertools.product。如果它什么都不返回,那么上面内层for循环的内容就不会被执行,所以decimal_strings(1)会是一个空的迭代器;这几乎肯定不是你想要的结果。但是,由于itertools.product('0123456789', repeat=0)返回一个元组,你就得到了预期的结果:

>>> list(decimal_strings(1))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

(当然,当n = 0时,这个函数会正确地抛出一个ValueError。)

所以简而言之,这个定义在数学上是合理的,而且在很多情况下也是你想要的。这绝对不是Python的bug!

11

从数学的角度来看,什么都不乘的结果应该是这个操作的中性元素,也就是乘法的“单位元”。

比如在整数中,乘法的中性元素是 1,因为 1 ⋅ a = a 对于所有整数 a 都成立。所以,空的整数乘积应该是 1。在实现一个返回数字列表乘积的 Python 函数时,这个结果自然就会出现:

def iproduct(lst):
  result = 1
  for i in lst:
    result *= i
  return result

为了让这个算法计算出正确的结果,result 需要初始化为 1。这样,当函数在一个空列表上调用时,就会返回 1

这个返回值对于这个函数来说也是很合理的。一个好的乘积函数不应该在你先连接两个列表再计算元素的乘积,或者先计算两个单独列表的乘积再相乘这两种情况下有区别:

iproduct(xs + ys) == iproduct(xs) * iproduct(ys)

如果 xsys 是空的,只有在 iproduct([]) == 1 的情况下,这种情况才成立。

现在我们来看看更复杂的 product() 函数,处理迭代器。在这里,从数学的角度来看,product([]) 也应该返回这个操作的中性元素,但它不是 [],因为 product([], xs) == [],而对于中性元素来说,product([], xs) == xs 应该成立。不过,[()] 也不是中性元素:

>>> list(product([()], [1,2,3]))
[((), 1), ((), 2), ((), 3)]

实际上,product() 并不是一个很好用的数学乘积,因为上面的等式并不成立:

product(*(xs + ys)) != product(product(*xs), product(*ys))

每次调用 product 都会生成一个额外的元组层,这种情况是无法避免的,所以根本不存在真正的中性元素。不过,[()] 非常接近,它不会增加或减少任何元素,只是给每个元素添加一个空元组。

[()] 实际上是这个稍微调整过的乘积函数的中性元素,这个函数只对元组列表进行操作,但每次调用时不会增加额外的元组层:

def tproduct(*xss):
  # the parameters have to be lists of tuples
  return (sum(rs, ()) for rs in product(*xss))

对于这个函数,上面的乘积等式是成立的:

def tup(x): return (x,)
txs = [map(tup, x) for x in xs]
tys = [map(tup, y) for y in ys]
tproduct(*(txs + tys)) == tproduct(tproduct(*txs), tproduct(*tys))

通过将输入列表打包成元组的额外预处理步骤,tproduct() 给出的结果与 product() 一样,但从数学的角度来看表现得更好。而且它的中性元素是 [()]

所以,[()] 作为这种列表乘法的中性元素是有一定道理的。即使它并不完全适用于 product(),但对于这个函数来说是个不错的选择,因为它允许定义 tproduct() 而不需要为空输入引入特殊情况。

撰写回答