需要一种快速计算方法并在一次传递中对可迭代求和

时间:2014-02-12 01:04:50

标签: python python-3.x

任何人都可以帮助我吗?我正试图想出一种计算方法

>>> sum_widths = sum(col.width for col in cols if not col.hide)

并且还计算此总和中的项目数,而不必在cols上进行两次传递。

看起来难以置信但是在扫描了std-lib(内置函数,itertools,functools等)之后,我甚至找不到一个可以计算迭代中成员数量的函数。我找到了函数itertools.count,这听起来像我想要的,但它实际上只是一个欺骗性命名的range函数。

经过一番思考后我想出了以下内容(这很简单,除了它的迟钝之外,缺少库函数可能是可以原谅的):

>>> visable_col_count = sum(col is col for col in cols if not col.hide)

但是,使用这两个函数需要两次迭代,这只是错误的方式。

作为替代方案,以下功能可以满足我的需求:

>>> def count_and_sum(iter):
>>>     count = sum = 0
>>>     for item in iter:
>>>         count += 1
>>>         sum += item
>>>     return count, sum

这个问题是它需要100倍的长度(根据timeit)作为生成器表达形式的总和。

如果有人能想出一个能做我想做的简单的单行程,请告诉我(使用Python 3.3)。

修改1

这里有很多好主意,伙计们。感谢所有回复的人。我需要一段时间才能消化所有这些答案,但我会尝试选择一个来检查。

修改2

我在我的两个简单建议(count_and_sum函数和2个单独的sum函数)上重复了时间,并发现我的原始时间已经过时,可能是由于自动调度的备份过程在的背景。

我也在这里给出了大部分优秀的建议作为答案,所有建议都采用相同的模型。分析这些答案对我来说是一种教育:dequeenumeratereduce的新用途以及countaccumulate的第一次使用。谢谢大家!

以下是使用我正在开发的用于显示的软件的结果(来自我的慢速上网本):

┌───────────────────────────────────────────────────────┐
│                 Count and Sum Timing                  │
├──────────────────────────┬───────────┬────────────────┤
│          Method          │Time (usec)│Time (% of base)│
├──────────────────────────┼───────────┼────────────────┤
│count_and_sum (base)      │        7.2│            100%│
│Two sums                  │        7.5│            104%│
│deque enumerate accumulate│        7.3│            101%│
│max enumerate accumulate  │        7.3│            101%│
│reduce                    │        7.4│            103%│
│count sum                 │        7.3│            101%│
└──────────────────────────┴───────────┴────────────────┘

(我没有把复杂的时间和折叠方法视为过于模糊,但无论如何都要感谢。)

由于所有这些方法的时序差别很小,我决定使用count_and_sum函数(带有显式for循环)作为最具可读性,显性和简单(Python Zen)和它也恰好是最快的!

我希望我能接受这些令人惊讶的答案中的一个是正确的,但它们虽然或多或少都是模糊的,但它们都是同样好的,所以我只是向所有人投票并接受我自己的答案为正确(count_and_sum函数因为那是我正在使用的。

那是什么呢?“应该有一个 - 最好只有一个 - 明显的方式去做。”

13 个答案:

答案 0 :(得分:101)

使用复数

z = [1, 2, 4, 5, 6]
y = sum(x + 1j for x in z)
sum_z, count_z = y.real, int(y.imag)
print sum_z, count_z
18.0 5

答案 1 :(得分:49)

我不知道速度,但这有点漂亮:

>>> from itertools import accumulate
>>> it = range(10)
>>> max(enumerate(accumulate(it), 1))
(10, 45)

答案 2 :(得分:29)

改编DSM的答案。使用deque(... maxlen=1)来节省内存使用。

import itertools 
from collections import deque 
deque(enumerate(itertools.accumulate(x), 1), maxlen=1)

ipython中的计时代码:

import itertools , random
from collections import deque 

def count_and_sum(iter):
     count = sum = 0
     for item in iter:
         count += 1
         sum += item
     return count, sum

X = [random.randint(0, 10) for _ in range(10**7)]
%timeit count_and_sum(X)
%timeit deque(enumerate(itertools.accumulate(X), 1), maxlen=1)
%timeit (max(enumerate(itertools.accumulate(X), 1)))

结果:现在比OP的方法更快

1 loops, best of 3: 1.08 s per loop
1 loops, best of 3: 659 ms per loop
1 loops, best of 3: 1.19 s per loop

答案 3 :(得分:23)

以下是一些可能感兴趣的时间数据:

import timeit

setup = '''
import random, functools, itertools, collections

x = [random.randint(0, 10) for _ in range(10**5)]

def count_and_sum(it):
    c, s = 0, 0
    for i in it:
        c += 1
        s += i
    return c, s

def two_pass(it):
    return sum(i for i in it), sum(True for i in it)

def functional(it):
    return functools.reduce(lambda pair, x: (pair[0]+1, pair[1]+x), it, [0, 0])

def accumulator(it):
    return max(enumerate(itertools.accumulate(it), 1))

def complex(it):
    cpx = sum(x + 1j for x in it)
    return cpx.real, int(cpx.imag)

def dequed(it):
    return collections.deque(enumerate(itertools.accumulate(it), 1), maxlen=1)

'''

number = 100
for stmt in ['count_and_sum(x)',
             'two_pass(x)',
             'functional(x)',
             'accumulator(x)',
             'complex(x)',
             'dequed(x)']:
    print('{:.4}'.format(timeit.timeit(stmt=stmt, setup=setup, number=number)))

结果:

3.404 # OP's one-pass method
3.833 # OP's two-pass method
8.405 # Timothy Shields's fold method
3.892 # DSM's accumulate-based method
4.946 # 1_CR's complex-number method
2.002 # M4rtini's deque-based modification of DSM's method

鉴于这些结果,我不确定OP如何通过一次通过方法看到100倍减速。即使数据看起来与随机整数列表完全不同,也不应该发生。

此外,M4rtini的解决方案看起来很明显。


为了澄清,这些结果在CPython 3.2.3中。有关与PyPy3的比较,请参阅James_pic's answer,其中显示了某些方法的JIT编译的一些重要收益(也在a comment by M4rtini中提到。

答案 4 :(得分:20)

作为senshin答案的后续行动,值得注意的是,性能差异很大程度上是由于CPython实现中的怪癖导致某些方法比其他方法慢(例如,for循环在CPython中相对较慢)。我认为在PyPy(使用PyPy3 2.1 beta)中尝试完全相同的测试会很有趣,它具有不同的性能特征。在PyPy中,结果如下:

0.6227 # OP's one-pass method
0.8714 # OP's two-pass method
1.033 # Timothy Shields's fold method
6.354 # DSM's accumulate-based method
1.287 # 1_CR's complex-number method
3.857 # M4rtini's deque-based modification of DSM's method

在这种情况下,OP的一次通过方法最快。这是有道理的,因为它可以说是最简单的(至少从编译器的角度来看),并且PyPy可以通过内联方法调用来消除许多开销,而CPython则不能。

为了比较,我机器上的CPython 3.3.2给出了以下内容:

1.651 # OP's one-pass method
1.825 # OP's two-pass method
3.258 # Timothy Shields's fold method
1.684 # DSM's accumulate-based method
3.072 # 1_CR's complex-number method
1.191 # M4rtini's deque-based modification of DSM's method

答案 5 :(得分:5)

你可以使用类似于

的技巧来计算总和
>>> from itertools import count
>>> cnt = count()
>>> sum((next(cnt), x)[1] for x in range(10) if x%2)
25
>>> next(cnt)
5

但是,使用for循环可能会更具可读性

答案 6 :(得分:4)

您可以使用:

from itertools import count

lst = range(10)
c = count(1)
tot = sum(next(c) and x for x in lst if x % 2)
n = next(c)-1
print(n, tot)

# 5 25

这是一种黑客行为,但效果非常好。

答案 7 :(得分:3)

我不知道Python语法是什么,但你可能会使用折叠。像这样:

(count, total) = fold((0, 0), lambda pair, x: (pair[0] + 1, pair[1] + x))

想法是使用(0,0)的种子,然后在每个步骤中将第一个组件加1,将当前数加到第二个组件。

为了进行比较,您可以将sum实现为折叠,如下所示:

total = fold(0, lambda t, x: t + x)

答案 8 :(得分:3)

1_CR的复数解决方案很可爱,但过于苛刻。它起作用的原因是复数是2元组,它按元素求和。 numpy数组也是如此,我认为使用它们会更加清晰:

import numpy as np
z = [1, 2, 4, 5, 6]
y = sum(np.array([x, 1]) for x in z)
sum_z, count_z = y[0], y[1]
print sum_z, count_z
18 5

答案 9 :(得分:3)

需要考虑的其他事项:如果可以确定最小可能的计数,我们可以让高效的内置sum完成部分工作:

from itertools import islice

def count_and_sum(iterable):
    # insert favorite implementation here

def count_and_sum_with_min_count(iterable, min_count):
    iterator = iter(iterable)
    slice_sum = sum(islice(iterator, None, min_count))
    rest_count, rest_sum = count_and_sum(iterator)
    return min_count + rest_count, slice_sum + rest_sum

例如,使用deque方法,序列为10000000项,min_count为5000000,时间结果为:

count_and_sum: 1.03
count_and_sum_with_min_count: 0.63

答案 10 :(得分:2)

这个怎么样? 它似乎有效。

from functools import reduce

class Column:
    def __init__(self, width, hide):
        self.width = width
        self.hide = hide

lst = [Column(10, False), Column(100, False), Column(1000, True), Column(10000, False)]

print(reduce(lambda acc, col: Column(col.width + acc.width, False) if not col.hide else acc, lst, Column(0, False)).width)

答案 11 :(得分:2)

你可能只需要总和&今天算了,但明天谁知道你需要什么!

这是一个易于扩展的解决方案:

def fold_parallel(itr, **fs):
    res = {
        k: zero for k, (zero, f) in fs.items()
    }

    for x in itr:
        for k, (_, f) in fs.items():
            res[k] = f(res[k], x)

    return res

from operator import add

print(fold_parallel([1, 2, 3],
    count = (0, lambda a, b: a + 1),
    sum = (0, add),
))
# {'count': 3, 'sum': 6}

答案 12 :(得分:2)

感谢所有出色的答案,但我决定使用我原来的count_and_sum函数,如下所示:

>>> cc, cs = count_and_sum(c.width for c in cols if not c.hide) 

正如我对原始问题的编辑中所解释的那样,这是最快且最易读的解决方案。