21

我有一个可迭代的条目,我想收集一些简单的统计数据,比如所有可被 2 整除的数字的计数和所有可被 3 整除的数字的计数。

我的第一个选择,虽然只遍历列表一次并避免列表扩展(并牢记拆分循环重构),但看起来相当臃肿:

(备选案文 1)

r = xrange(1, 10)

twos = 0
threes = 0

for v in r:
  if v % 2 == 0:
    twos+=1
  if v % 3 == 0:
    threes+=1

print twos
print threes

这看起来相当不错,但有将表达式扩展为列表的缺点:

(备选案文 2)

r = xrange(1, 10)

print len([1 for v in r if v % 2 == 0])
print len([1 for v in r if v % 3 == 0])

我真正想要的是像这样的函数:

(替代项 3)

def count(iterable):
  n = 0
  for i in iterable:
    n += 1
  return n

r = xrange(1, 10)

print count(1 for v in r if v % 2 == 0)
print count(1 for v in r if v % 3 == 0)

但这看起来很像可以在没有函数的情况下完成的事情。最后的变体是这样的:

(替代项 4)

r = xrange(1, 10)

print sum(1 for v in r if v % 2 == 0)
print sum(1 for v in r if v % 3 == 0)

虽然最小(在我的书中可能是最优雅的),但感觉它并不能很好地表达意图。

所以,我对你的问题是:

您最喜欢哪种选择来收集这些类型的统计数据?如果您有更好的选择,请随时提供您自己的替代方案。

为了消除下面的一些混淆:

  • 实际上,我的过滤谓词比这个简单的测试更复杂。
  • 我迭代的对象比数字更大更复杂
  • 我的过滤器函数更加不同并且难以参数化为一个谓词
4

12 回答 12

20

恕我直言,必须多次遍历列表并不优雅。

我可能会创建一个允许执行以下操作的函数:

twos, threes = countmatching(xrange(1,10),
                             lambda a: a % 2 == 0,
                             lambda a: a % 3 == 0)

一个起点是这样的:

def countmatching(iterable, *predicates):
    v = [0] * len(predicates)
    for e in iterable:
        for i,p in enumerate(predicates):
            if p(e):
                v[i] += 1
    return tuple(v)

顺便说一句,“itertools recipes”有一个很像你的 alt4 的方法。

def quantify(seq, pred=None):
    "Count how many times the predicate is true in the sequence"
    return sum(imap(pred, seq))
于 2008-10-01T11:09:14.667 回答
7

替代 4!但也许您应该将代码重构为一个函数,该函数接受一个包含可除数(二和三)的参数。然后你可以有一个更好的函数名。

def methodName(divNumber, r):
  return sum(1 for v in r if v % divNumber == 0)


print methodName(2, xrange(1, 10))
print methodName(3, xrange(1, 10))
于 2008-10-01T11:04:46.737 回答
4

您可以使用该filter功能。

它过滤一个列表(或严格来说是一个可迭代的),生成一个新列表,其中仅包含指定函数评估为真的项目。

r = xrange(1, 10)

def is_div_two(n):
    return n % 2 == 0

def is_div_three(n):
    return n % 3 == 0

print len(filter(is_div_two,r))
print len(filter(is_div_three,r))

这很好,因为它允许您将统计逻辑包含在函数中,并且其意图filter应该非常清楚。

于 2008-10-01T10:51:36.457 回答
3

我会选择您的(替代 4)的一个小变体:

def count(predicate, list):
    print sum(1 for x in list if predicate(x))

r = xrange(1, 10)

count(lambda x: x % 2 == 0, r)
count(lambda x: x % 3 == 0, r)
# ...

如果您想更改 count 的作用,请在一处更改其实现。

Note: since your predicates are complex, you'll probably want to define them in functions instead of lambdas. And so you'll probably want to put all this in a class rather than the global namespace.

于 2008-10-02T16:16:40.627 回答
1

好吧,您可以做一个列表理解/表达式来获得一组带有该统计测试的元组,然后将其减少以获得总和。


r=xrange(10)
s=( (v % 2 == 0, v % 3 == 0) for v in r )
def add_tuples(t1,t2):
     return tuple(x+y for x,y in zip(t1, t2))
sums=reduce(add_tuples, s, (0,0)) # (0,0) is starting amount

print sums[0] # sum of numbers divisible by 2
print sums[1] # sum of numbers divisible by 3

使用生成器表达式等应该意味着您只会运行一次迭代器(除非 reduce 做任何奇怪的事情?)。基本上你会做地图/减少......

于 2008-10-01T10:53:51.893 回答
1

真布尔值被强制为单位整数,而假布尔值被强制为零整数。因此,如果您乐于使用 scipy 或 numpy,请为序列的每个元素创建一个整数数组,每个数组包含每个测试的一个元素,并对数组求和。例如

>>> sum(scipy.array([c % 2 == 0, c % 3 == 0]) for c in xrange(10))
array([5, 4])
于 2008-10-01T16:47:13.433 回答
0

如果您只有数字,我肯定会查看numpy数组而不是可迭代列表。几乎可以肯定,您可以在数组上使用一些简洁的算术来做您想做的事情。

于 2008-10-01T10:53:12.113 回答
0

不像您正在寻找的那样简洁,但更有效,它实际上适用于任何可迭代,而不仅仅是您可以循环多次的可迭代,并且您可以扩展要检查的内容而不会进一步复杂化:

r = xrange(1, 10)

counts = {
   2: 0,
   3: 0,
}

for v in r:
    for q in counts:
        if not v % q:
            counts[q] += 1
        # Or, more obscure:
        #counts[q] += not v % q

for q in counts:
    print "%s's: %s" % (q, counts[q])
于 2008-10-01T10:56:46.770 回答
0
from itertools import groupby
from collections import defaultdict

def multiples(v):
    return 2 if v%2==0 else 3 if v%3==0 else None
d = defaultdict(list)

for k, values in groupby(range(10), multiples):
    if k is not None:
        d[k].extend(values)
于 2008-10-01T11:26:34.873 回答
0

这里的想法是使用归约来避免重复迭代。此外,如果内存对您来说是个问题,这不会创建任何额外的数据结构。您从带有计数器 ( {'div2': 0, 'div3': 0}) 的字典开始,并在迭代过程中递增它们。

def increment_stats(stats, n):
    if n % 2 == 0: stats['div2'] += 1
    if n % 3 == 0: stats['div3'] += 1
    return stats

r = xrange(1, 10)
stats = reduce(increment_stats, r, {'div2': 0, 'div3': 0})
print stats

如果你想计算比除数更复杂的东西,使用更面向对象的方法(具有相同的优点)是合适的,封装统计数据提取的逻辑。

class Stats:

    def __init__(self, div2=0, div3=0):
        self.div2 = div2
        self.div3 = div3

    def increment(self, n):
        if n % 2 == 0: self.div2 += 1
        if n % 3 == 0: self.div3 += 1
        return self

    def __repr__(self):
        return 'Stats(%d, %d)' % (self.div2, self.div3)

r = xrange(1, 10)
stats = reduce(lambda stats, n: stats.increment(n), r, Stats())
print stats

请指出任何错误。

@Henrik:我认为第一种方法的可维护性较差,因为您必须在一个地方控制字典的初始化并在另一个地方进行更新,并且必须使用字符串来引用每个统计信息(而不是具有属性)。而且我认为在这种情况下 OO 并不过分,因为您说谓词和对象在您的应用程序中会很复杂。事实上,如果谓词真的很简单,我什至不会费心使用字典,一个固定大小的列表就可以了。干杯:)

于 2008-10-01T13:32:41.733 回答
0

受上面 OO-stab 的启发,我也不得不尝试一个(尽管这对于我要解决的问题来说有点矫枉过正:)

class Stat(object):
  def update(self, n):
    raise NotImplementedError

  def get(self):
    raise NotImplementedError


class TwoStat(Stat):
  def __init__(self):
    self._twos = 0

  def update(self, n):
    if n % 2 == 0: self._twos += 1

  def get(self):
    return self._twos


class ThreeStat(Stat):
  def __init__(self):
    self._threes = 0

  def update(self, n):
    if n % 3 == 0: self._threes += 1

  def get(self):
    return self._threes


class StatCalculator(object):
  def __init__(self, stats):
    self._stats = stats

  def calculate(self, r):
    for v in r:
      for stat in self._stats:
        stat.update(v)
    return tuple(stat.get() for stat in self._stats)


s = StatCalculator([TwoStat(), ThreeStat()])

r = xrange(1, 10)
print s.calculate(r)
于 2008-10-01T15:35:20.267 回答
0

Alt 3,因为它不使用与“命中”数成正比的内存。给定一个像 xrange(one_trillion) 这样的病态案例,许多其他提供的解决方案都会严重失败。

于 2008-10-01T16:59:00.037 回答