在生成器上进行Python聚合
我有一个生成器,每次迭代都会返回一个列表。这个列表里的每个元素要么是0,要么是1。我想统计一下总共返回了多少个元素(包括0和1),还有返回了多少个1。我尝试用reduce函数来实现,代码是这样的:
t = reduce( (lambda x,y:(y[0]+1,y[1]+x)), gen_fn(), (0,0))
上面的gen_fn()就是那个生成器,它在每次yield语句中返回列表的一部分。我想用一个元组(0,0)来初始化计数。生成器返回的元素如下:
[0, 1, 1, 0, 1]
我期望的输出结果是t = (5,3)。但是我的代码出错了,错误信息是:
TypeError: unsupported operand type(s) for +: 'int' and 'tuple'
有没有人能帮我找出问题所在?我对reduce和lambda函数不太熟悉,这让我搞不清楚哪里出错了。谢谢大家!
4 个回答
那我们换个完全不同的思路怎么样?
t = [(len(row), len(filter(lambda x: x == 1, row))) for row in gen_fn()]
你的 lambda
参数顺序搞反了;第一个参数(x
)是到目前为止的总和(一个元组),第二个参数(y
)是新的值(一个整数)。试试这样:
t = reduce((lambda x, y: (x[0]+1, x[1]+y)), gen_fn(), (0,0))
使用一个虚拟函数:
def gen_fn():
for x in [0, 1, 1, 0, 1]:
yield x
我得到的结果是 (5, 3)
。
这个来自 文档 的 reduce
的等效实现可能会让事情更清楚:
def reduce(function, iterable, initializer=None):
it = iter(iterable)
if initializer is None:
try:
initializer = next(it)
except StopIteration:
raise TypeError('reduce() of empty sequence with no initial value')
accum_value = initializer
for x in it:
accum_value = function(accum_value, x) # note value so far is first arg
return accum_value
我觉得这里最好的答案就是保持简单:
count = 0
total = 0
for item in gen_fn():
count += 1
total += item
在这里使用 reduce()
只会让你的代码变得不容易读懂。
如果你的问题是想要写出最简短的代码(同时保持懒惰求值),那么你可以用:
count, total = collections.deque(zip(itertools.count(1), itertools.accumulate(gen_fn())), maxlen=1).pop()
当然,选择这种写法而不是简单的解决方案,你肯定会觉得很疯狂。
补充:
如果生成器输出多个小部分,那就直接用 itertools.chain.from_iterable(gen_fn())
来把它们合并成一个大部分。
正如jonrsharpe所指出的,你在使用lambda
函数的参数时顺序搞反了,这和reduce
的工作方式有关。不过,如果你从生成器中得到的每个项目都是一个列表,那么在加总时可能还会有其他问题。
这个问题在于,你的y
值(生成器返回的项目)并不是一个单独的数字,而是一个列表。你需要计算这个列表的长度以及里面有多少个1
,所以你可能需要把你的lambda函数改成:
lambda x, y: (x[0]+len(y), x[1]+sum(y))