【问题标题】:Most pythonic way of counting matching elements in something iterable在可迭代的东西中计算匹配元素的最pythonic方法
【发布时间】:2008-10-01 10:37:20
【问题描述】:

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

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

(替代 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)

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

所以,我的问题是:

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

为了澄清下面的一些困惑:

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

【问题讨论】:

    标签: python list-comprehension


    【解决方案1】:

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

    我可能会创建一个允许这样做的函数:

    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))
    

    【讨论】:

    • 回复。迭代两次,它有一些事情要清楚,但除此之外,一次迭代减少的开销不会被执行的非 C 代码量吃掉吗?
    • 当然,如果我只迭代一次它也适用于一次性可迭代对象,呵呵 :) 没想到那么远。
    • 我喜欢你的解决方案。但是您说:“必须多次遍历列表并不优雅”。如果有 n 个值和 m 个谓词,则在 m 个谓词上迭代 n 次,那么与在 n 个值上迭代 m 次相比有什么优势?注意:我是 Python 新手。干杯!
    • 我认为迭代一次的原因是不是所有的可迭代对象都可以迭代多次。幸运的是,我的可以:)
    • 在 python3 中 itertools.imap 变成了只是 map。例如。 sum(map(lambda a: a > 1, [1, 2, 3])) 返回 2。
    【解决方案2】:

    Alt 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))
    

    【讨论】:

    • 不幸的是,“真实”测试与此稍有不同。参数化它们只会让我头疼:)
    【解决方案3】:

    您可以使用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 的意图应该非常清楚。

    【讨论】:

    • 第二次打印不会消耗一个用完的迭代器,因此会打印 0?
    【解决方案4】:

    我会选择你的(替代 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 的作用,请在一处更改其实现。

    注意:由于您的谓词很复杂,您可能希望在函数而不是 lambda 中定义它们。因此,您可能希望将所有这些都放在一个类中,而不是全局命名空间中。

    【讨论】:

    • 改变 count 的作用不是很常见,但创建一个名为 count 的函数有助于以一种很好的方式显示意图。关于。你的笔记;当然,但这超出了问题的范围:)
    【解决方案5】:

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

    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 做任何奇怪的事情?)。基本上你会做 map/reduce...

    【讨论】:

    • 哈!我知道有一种方法可以为此使用 reduce :)
    【解决方案6】:

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

    >>> sum(scipy.array([c % 2 == 0, c % 3 == 0]) for c in xrange(10))
    array([5, 4])
    

    【讨论】:

      【解决方案7】:

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

      【讨论】:

      • 不幸的是,它实际上是一个相当大的对象的长迭代;数字只是为了便于阅读:)
      【解决方案8】:

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

      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])
      

      【讨论】:

        【解决方案9】:
        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)
        

        【讨论】:

        • 很酷的解决方案,虽然当一个项目可以被二和三整除时,统计数据不会正确更新。
        【解决方案10】:

        受上面的 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)
        

        【讨论】:

          【解决方案11】:

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

          【讨论】:

          • 我认为 alt 4 具有相同的属性
          【解决方案12】:

          这里的想法是使用归约来避免重复迭代。此外,如果内存对您来说是个问题,这不会创建任何额外的数据结构。您从带有计数器 ({'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 并不过分,因为您说谓词和对象在您的应用程序中会很复杂。事实上,如果谓词真的很简单,我什至不会费心使用字典,一个固定大小的列表就可以了。干杯:)

          【讨论】:

          • reduce 的奇怪而有趣的使用 :) 是的,对于更复杂的场景,更倾向于使用稍微更多的 OO 方法,但我不太明白您的版本如何更好地扩展(维护/重用) ) 比原来的。
          猜你喜欢
          • 2019-04-25
          • 2015-09-11
          • 2014-11-12
          • 2012-03-12
          • 2017-12-16
          • 1970-01-01
          • 1970-01-01
          • 2019-10-19
          • 2012-07-21
          相关资源
          最近更新 更多