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() принимает два аргумента:

  1. последовательность, которую суммируем
  2. первое слагаемое для итоговой суммы, по умолчанию — 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)

Лично мне такой вариант даже больше нравится ツ

Заметка из телеграм-канала «Oh My Py»