itertools.product()接收空列表时应返回什么?
我觉得这是个学术性的问题,但第二个结果让我有点困惑。难道它不应该和第一个一样完全空吗?这种行为背后的原因是什么呢?
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 个回答
正如@sth已经提到的,从数学角度来看,这种行为是正确的。你只需要明白的是,list(itertools.product())
应该只有一个元素,因为一旦你知道这一点,就很清楚这个元素应该是什么:它必须是一个长度为0的元组,而这样的元组只有一个。
但是,itertools.product(l1, l2, l3, ...)
的元素数量应该是l1
、l2
、l3
等的长度相乘的结果。所以,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!
从数学的角度来看,什么都不乘的结果应该是这个操作的中性元素,也就是乘法的“单位元”。
比如在整数中,乘法的中性元素是 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)
如果 xs
或 ys
是空的,只有在 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()
而不需要为空输入引入特殊情况。