Самый питонический способ подсчета совпадающих элементов в чем-то повторяемом

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

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

(вариант 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 есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
20
0
12 828
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 12

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

John Montgomery 01.10.2008 15:14

Я бы определенно посмотрел на массив тупой вместо повторяющегося списка, если у вас есть только числа. Вы почти наверняка сможете делать то, что хотите, с помощью краткой арифметики над массивом.

К сожалению, на самом деле это длинная итерация довольно больших объектов; числа просто для удобства чтения :)

Henrik Gustafsson 01.10.2008 14:54

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


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

Ха! Я знал, что для этого есть способ уменьшить :)

Henrik Gustafsson 01.10.2008 16:15

Не так кратко, как вы ищете, но более эффективно, он действительно работает с любыми итерациями, а не только с итерациями, которые вы можете повторять несколько раз, и вы можете расширить объекты для проверки, не усложняя это еще больше:

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

Альтернативный вариант 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))

«Настоящие» тесты, к сожалению, немного отличаются от этого. Их параметризация вызовет у меня только головную боль :)

Henrik Gustafsson 01.10.2008 16:14
Ответ принят как подходящий

Необходимость перебирать список несколько раз - это не изящно, ИМХО.

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

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

Re. повторяя дважды, у него есть кое-что для ясности, но кроме этого, не будет ли уменьшенные накладные расходы от повторения один раз быть съедены количеством выполненного не-C-кода?

Henrik Gustafsson 01.10.2008 16:13

Конечно, если я буду повторять только один раз, когда он работает и с одноразовыми итерациями, да :) Не думал, что так далеко.

Henrik Gustafsson 01.10.2008 19:30

Мне нравится ваше решение. Но вы говорите: «необходимость перебирать список несколько раз - не изящно». Если имеется n значений и m предикатов, вы выполняете итерацию n раз для m предикатов, так в чем же преимущество перед повторением m раз для n значений? Примечание: я новичок в Python. Ваше здоровье!

Sébastien RoccaSerra 02.10.2008 19:38

Я думаю, что причина однократной итерации в том, что не все итерации можно повторять несколько раз. Моя, к счастью, может :)

Henrik Gustafsson 02.10.2008 20:51

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)

Классное решение, хотя статистика некорректно обновляется, когда предмет делится и на два, и на три.

Henrik Gustafsson 01.10.2008 19:26

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

Странное и интригующее использование reduce :) И да, для более сложных сценариев предпочтительнее был бы немного более объектно-ориентированный подход, но я не совсем понимаю, насколько ваша версия масштабируется лучше (с точки зрения обслуживания / повторного использования), чем исходные.

Henrik Gustafsson 01.10.2008 19:18

Вдохновленный 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])

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

Я думаю, что у alt 4 такие же свойства

Henrik Gustafsson 01.10.2008 21:27

Я бы выбрал небольшой вариант вашего (вариант 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, измените его реализацию в одном месте.

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

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

Henrik Gustafsson 02.10.2008 20:49

Другие вопросы по теме