Python. Подвох в функции sum()
Сидите вы на работе и смотрите на дневную статистику по заявкам разных типов:
monday = {"question": 1, "problem": 3, "idea": 2}
tuesday = {"problem": 5, "idea": 1}
wednesday = {"question": 2, "problem": 2}
Задача — посчитать агрегированную статистику за все дни. Всё вроде понятно. Тут подходит тимлид и говорит, что если решите задачу однострочником, он подарит вам жёлтую резиновую уточку.
Устоять перед этим решительно невозможно.
Словари → счётчики
Сначала от словарей надо перейти к счётчикам. Воспользуемся для этого функцией map()
. Она принимает на входе функцию и последовательность (iterable), после чего применяет функцию к каждому элементу последовательности и возвращает, что получилось. Например:
>>> mapped = map(abs, [-1, -2, -3])
>>> list(mapped)
[1, 2, 3]
abs()
возвращает абсолютное значение числа, а list()
тут нужен, чтобы отработал map (сам по себе он ленивый, пока не пнёшь — не полетит).
В нашем случае последовательностью будет набор дневных статистик, а функцией — конструктор счётчика:
>>> map(Counter, [monday, tuesday, wednesday])
<map object at 0x10cae2470>
map object — это итератор, который сделает из словарей счётчики, когда нам это действительно понадобится — в момент суммирования.
Счётчики → агрегат
Окей, теперь осталось только посчитать сумму от этого добра. Как мы знаем, у счётчиков перекрыт оператор сложения, так что почему бы просто не вызвать sum()
на них?
>>> sum(map(Counter, [monday, tuesday, wednesday]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'Counter'
Эээ, что? Откуда тут int
, мы же суммируем объекты Counter
?
Оказывается, функция sum()
принимает два аргумента:
- последовательность, которую суммируем
- первое слагаемое для итоговой суммы, по умолчанию — 0
Например:
>>> sum([1, 2, 3])
6
>>> sum([1, 2, 3], 10)
16
Получается, в нашем случае sum()
пытается сложить 0 со счётчиком от monday
, и, естественно, ломается. Решение — передать в качестве первого слагаемого пустой счётчик:
sum(..., Counter())
Объединяем всё вместе:
>>> sum(map(Counter, [monday, tuesday, wednesday]), Counter())
Counter({'problem': 10, 'question': 3, 'idea': 3})
Готово!
Почему вздыхает Гвидо
Гвидо ван Россум недолюбливает map()
и, кажется, функциональщину как таковую. Вместо неё он предпочитает пользоваться comprehensions.
В нашем случае вместо map()
можно использовать такую конструкцию:
(Counter(stat) for stat in [monday, tuesday, wednesday])
Используйте с осторожностью
Ради жёлтой резиновой уточки на многое можно пойти, но всё-таки лучше использовать однострочники с осторожностью. Главный критерий хорошего кода — простота понимания. Поэтому вполне можно добавить промежуточную переменную, а то и две:
daily_stats = map(Counter, [monday, tuesday, wednesday])
empty_stat = Counter()
sum(daily_stats, empty_stat)
А если не хотите, чтобы читатель мучительно вспоминал, что за такой второй аргумент в sum()
, можно и вовсе сделать так:
daily_stats = map(Counter, [monday, tuesday, wednesday])
functools.reduce(operator.add, daily_stats)
Лично мне такой вариант даже больше нравится ツ
★ Подписывайтесь на новые заметки.