在可迭代的事物中计算匹配元素的大多数pythonic方法

时间:2020-03-06 14:58:15  来源:igfitidea点击:

我想输入一些简单的统计数据,例如,所有数字的整数可以除以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)

尽管体积最小(在我的书中可能是最优雅的),但它并不能很好地表达其意图。

因此,我对问题是:

我们最喜欢哪种方法来收集这些类型的统计信息?如果我们有更好的东西,请随时提供自己的选择。

为了清除下面的一些混乱:

  • 实际上,我的过滤谓词比这个简单的测试还要复杂。
  • 我要遍历的对象比数字更大,更复杂
  • 我的过滤器功能更加不同,很难将其参数化为一个谓词

解决方案

我们可以使用filter函数。

它过滤一个列表(或者严格来说是一个可迭代的列表),从而生成一个仅包含指定函数求值为true的项目的新列表。

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"的意图应该很清楚。

如果我们只有数字,我肯定会看一个numpy数组,而不是一个可迭代的列表。几乎可以肯定的是,只要对数组进行一些简短的运算,就可以完成所需的操作。

好吧,我们可以执行一个列表理解/表达式以获取其中包含该stat测试的一组元组,然后将其减少以得到总和。

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不会有什么奇怪的事情?)。基本上你会做地图/减少...

它并不像我们想要的那样简洁,但是更有效,它实际上可以与任何可迭代的对象一起使用,不仅可以迭代多次,而且可以扩展要检查的内容而不会进一步使其复杂化:

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

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

不得不多次遍历列表并不是很好的恕我直言。

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

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食谱"有一个类似alt4的食谱。

def quantify(seq, pred=None):
    "Count how many times the predicate is true in the sequence"
    return sum(imap(pred, seq))

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)

这里的想法是使用减少以避免重复迭代。同样,如果内存对我们来说是一个问题,那么这不会创建任何额外的数据结构。我们可以从带有计数器的字典开始({'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:我认为第一种方法难以维护,因为我们必须在一个地方控制字典的初始化并在另一个地方进行更新,并且必须使用字符串来引用每个stat(而不是具有属性)。而且我不认为OO在这种情况下是过分的,因为我们说谓词和对象在应用程序中会很复杂。实际上,如果谓词真的很简单,我什至不用费心使用字典,单个固定大小的列表就可以了。干杯:)

受以上面向对象的启发,我也不得不尝试一下(尽管这对于我要解决的问题来说是过大的了:)

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)

布尔值强制为单位整数,布尔值强制为零整数。因此,如果我们乐于使用scipy或者numpy,请为序列中的每个元素制作一个整数数组,每个数组为每个测试包含一个元素,然后对这些数组求​​和。例如。

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

Alt 3,因为它不使用与"点击数"成正比的内存。给定像xrange(one_trillion)这样的病理情况,许多其他提供的解决方案都将严重失败。

我会选择一个小变体(替代项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)
# ...

如果要更改计数,请在一处更改其实现。

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