Наиболее питонический способ подсчета совпадающих элементов в чем-то итерируемом - PullRequest
14 голосов
/ 01 октября 2008

У меня есть итерация записей, по которым я хотел бы собрать некоторую простую статистику, скажем, счет всех чисел, делимых на два, и счет всех чисел, делимых на три.

Моя первая альтернатива, хотя и повторяющаяся по списку только один раз и избегающая расширения списка (и помня о рефакторинге split *1004*), выглядит довольно раздутой:

(альт 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)

Но это очень похоже на то, что можно сделать без функции. Окончательный вариант таков:

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

и хотя самый маленький (и в моей книге, вероятно, самый элегантный), он не очень хорошо выражает намерение.

Итак, мой вопрос к вам:

Какая альтернатива вам больше нравится для сбора этих типов статистики? Не стесняйтесь предлагать собственную альтернативу, если у вас есть что-то лучше.

Чтобы устранить некоторую путаницу ниже:

  • На самом деле мои предикаты фильтра более сложны, чем просто этот простой тест.
  • Объекты, которые я перебираю, больше и сложнее, чем просто числа
  • Мои функции фильтра более различны и их трудно параметризировать в один предикат

Ответы [ 12 ]

17 голосов
/ 01 октября 2008

ИМХО многократно повторять список несколько раз.

Я бы, вероятно, создал функцию, которая позволяет делать:

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))
6 голосов
/ 01 октября 2008

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 голосов
/ 01 октября 2008

Вы можете использовать функцию 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 должна быть достаточно ясной.

2 голосов
/ 02 октября 2008

Я бы выбрал маленький вариант вашего (alt 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)
# ...

Если вы хотите изменить подсчет, измените его реализацию в одном месте.

Примечание: поскольку ваши предикаты сложны, вы, вероятно, захотите определить их в функциях, а не в лямбдах И поэтому вы, вероятно, захотите поместить все это в класс, а не в глобальное пространство имен.

1 голос
/ 01 октября 2008

Истинные логические значения приводятся к единичным целым числам, а ложные логические - к нулевым целым числам. Поэтому, если вы счастливы использовать scipy или numpy, создайте массив целых чисел для каждого элемента вашей последовательности, каждый из которых содержит один элемент для каждого из ваших тестов, и суммируйте по массивам. Э.Г.

>>> sum(scipy.array([c % 2 == 0, c % 3 == 0]) for c in xrange(10))
array([5, 4])
1 голос
/ 01 октября 2008

Что ж, вы могли бы сделать одно понимание / выражение списка, чтобы получить набор кортежей с этим статистическим тестом в них, а затем уменьшить его, чтобы получить суммы.


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 не делает ничего странного?). В основном вы будете делать карту / уменьшить ...

0 голосов
/ 01 октября 2008

Alt 3 по той причине, что он не использует память, пропорциональную количеству «попаданий». При таком патологическом случае, как xrange (one_trillion), многие другие предлагаемые решения потерпят неудачу.

0 голосов
/ 01 октября 2008

Вдохновленный 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)
0 голосов
/ 01 октября 2008

Идея в том, чтобы использовать сокращение, чтобы избежать повторных итераций. Кроме того, это не создает никаких дополнительных структур данных, если память является проблемой для вас. Вы начинаете со словаря со своими счетчиками ({'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

Пожалуйста, укажите на любые ошибки.

@ Хенрик: Я думаю, что первый подход менее удобен в обслуживании, так как вам нужно контролировать инициализацию словаря в одном месте и обновлять в другом, а также использовать строки для ссылки на каждый стат (вместо того, чтобы иметь атрибуты). И я не думаю, что в этом случае ОО является излишним, поскольку вы сказали, что предикаты и объекты будут сложными в вашем приложении. На самом деле, если бы предикаты были действительно простыми, я бы даже не стал использовать словарь, с одним списком фиксированного размера было бы просто замечательно. Ура:)

0 голосов
/ 01 октября 2008
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)
...