У меня есть итерация записей, по которым я хотел бы собрать некоторую простую статистику, скажем, количество всех чисел, делящихся на два, и количество всех чисел, делящихся на три.
Моя первая альтернатива, хотя и повторяется только один раз и избегает расширения списка (и с учетом рефакторинга разделенная петля), выглядит довольно раздутой:
(вариант 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.
Он фильтрует список (или строго повторяемый), создавая новый список, содержащий только элементы, для которых указанная функция оценивается как истина.
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 должно быть довольно ясным.
Я бы определенно посмотрел на массив тупой вместо повторяющегося списка, если у вас есть только числа. Вы почти наверняка сможете делать то, что хотите, с помощью краткой арифметики над массивом.
К сожалению, на самом деле это длинная итерация довольно больших объектов; числа просто для удобства чтения :)
Ну, вы можете сделать одно понимание / выражение списка, чтобы получить набор кортежей с этим тестом статистики в них, а затем уменьшить его, чтобы получить суммы.
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 ...
Ха! Я знал, что для этого есть способ уменьшить :)
Не так кратко, как вы ищете, но более эффективно, он действительно работает с любыми итерациями, а не только с итерациями, которые вы можете повторять несколько раз, и вы можете расширить объекты для проверки, не усложняя это еще больше:
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))
«Настоящие» тесты, к сожалению, немного отличаются от этого. Их параметризация вызовет у меня только головную боль :)
Необходимость перебирать список несколько раз - это не изящно, ИМХО.
Я бы, наверное, создал функцию, которая позволяет:
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-кода?
Конечно, если я буду повторять только один раз, когда он работает и с одноразовыми итерациями, да :) Не думал, что так далеко.
Мне нравится ваше решение. Но вы говорите: «необходимость перебирать список несколько раз - не изящно». Если имеется n значений и m предикатов, вы выполняете итерацию n раз для m предикатов, так в чем же преимущество перед повторением m раз для n значений? Примечание: я новичок в Python. Ваше здоровье!
Я думаю, что причина однократной итерации в том, что не все итерации можно повторять несколько раз. Моя, к счастью, может :)
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: Я думаю, что первый подход менее удобен в обслуживании, так как вы должны контролировать инициализацию словаря в одном месте и обновлять в другом, а также использовать строки для ссылки на каждую статистику (вместо атрибутов). И я не думаю, что объектно-ориентированный подход в данном случае является излишним, поскольку вы сказали, что предикаты и объекты в вашем приложении будут сложными. На самом деле, если бы предикаты были действительно простыми, я бы даже не стал использовать словарь, один список фиксированного размера был бы вполне приемлемым. Ваше здоровье :)
Странное и интригующее использование reduce :) И да, для более сложных сценариев предпочтительнее был бы немного более объектно-ориентированный подход, но я не совсем понимаю, насколько ваша версия масштабируется лучше (с точки зрения обслуживания / повторного использования), чем исходные.
Вдохновленный 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 такие же свойства
Я бы выбрал небольшой вариант вашего (вариант 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. ваша заметка; конечно, но это выходит за рамки вопроса :)
не будет ли второй отпечаток потреблять использованный итератор и, следовательно, будет печатать 0?