#python #functional-programming
#python #функциональное программирование
Вопрос:
У меня есть список счетчиков:
from collections import Counter
counters = [
Counter({"coach": 1, "says": 1, "play": 1, "basketball": 1}),
Counter({"i": 2, "said": 1, "hate": 1, "basketball": 1}),
Counter({"he": 1, "said": 1, "play": 1, "basketball": 1}),
]
Я могу объединить их с помощью цикла, как показано ниже, но я бы хотел избежать цикла.
all_ct = Counter()
for ct in counters:
all_ct.update(ct)
Использование reduce
выдает ошибку:
all_ct = Counter()
reduce(all_ct.update, counters)
>>> TypeError: update() takes from 1 to 2 positional arguments but 3 were given
Есть ли способ объединить счетчики в один счетчик без использования цикла?
Комментарии:
1. «но я бы хотел избежать цикла», но почему? Почему вы хотите этого избежать?
reduce
в любом случае, вероятно, это просто цикл, менее эффективный
Ответ №1:
Вы можете использовать функцию sum .
all_ct = sum(counters, Counter())
Комментарии:
1. Вероятно, лучше избегать
sum
, из документации: «Эта функция предназначена специально для использования с числовыми значениями и может отклонять нечисловые типы»2. Согласен с juanpa, это также трудно читать и, на мой взгляд, какова цель
3. @mclslee да, хотя удобочитаемость действительно небольшая проблема, большая проблема заключается в том, что это зависит от детали реализации, которая, то
sum
есть, должна работать только для числовых типов. Просто так получилось , что он не выдает ошибку, но учтите:sum(['a','b','c'], '')
ничто не мешает Python выдавать ошибкуCounter
в будущем так же, как и для строк.4. функция sum работает очень медленно на счетчиках: (
5. @Sam да, смотрите мой ответ. Самый питонический ответ — просто использовать цикл. наивно это будет более производительно, и оно отлично читается.
Ответ №2:
Обратите внимание, счетчики реализуются __add__
для объединения счетчиков… Итак, вы могли бы использовать:
In [3]: from collections import Counter
...: counters = [
...: Counter({"coach": 1, "says": 1, "play": 1, "basketball": 1}),
...: Counter({"i": 2, "said": 1, "hate": 1, "basketball": 1}),
...: Counter({"he": 1, "said": 1, "play": 1, "basketball": 1}),
...: ]
In [4]: from operator import add
In [5]: from functools import reduce
In [6]: reduce(add, counters)
Out[6]:
Counter({'coach': 1,
'says': 1,
'play': 2,
'basketball': 3,
'i': 2,
'said': 2,
'hate': 1,
'he': 1})
Или проще:
In [7]: final = Counter()
In [8]: for c in counters:
...: final = c
...:
In [9]: final
Out[9]:
Counter({'coach': 1,
'says': 1,
'play': 2,
'basketball': 3,
'i': 2,
'said': 2,
'hate': 1,
'he': 1})
Обратите внимание, что приведенное выше более эффективно, поскольку оно использует только один dict. Если вы используете reduce(add, counters)
его, создается новый промежуточный объект счетчика на каждой итерации
Просто чтобы проиллюстрировать, что я имею в виду, в лучшем случае, когда ключи всегда повторяются, вам нужно выполнить двойную работу, используя reduce
/ sum
подход:
In [1]: from collections import Counter
...: counters = [
...: Counter({"coach": 1, "says": 1, "play": 1, "basketball": 1}),
...: Counter({"i": 2, "said": 1, "hate": 1, "basketball": 1}),
...: Counter({"he": 1, "said": 1, "play": 1, "basketball": 1}),
...: ]
In [2]: counters *= 5_000
In [3]: from functools import reduce
In [4]: from operator import add
In [5]: %%timeit
...: data = counters.copy()
...: result = Counter()
...: for c in data:
...: result = c
...:
21.2 ms ± 542 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [6]: %%timeit
...: data = counters.copy()
...: reduce(add, counters)
...:
...:
50.9 ms ± 1.73 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
И я считаю, что в худшем случае (когда каждый счетчик имеет ключи, не пересекающиеся с каждым из остальных) это приведет к снижению квадратичной производительности.
Наконец, обратите внимание, что вы можете выполнить добавление на месте, используя reduce
(не sum
), что устраняет проблему с производительностью:
In [6]: import operator
In [7]: operator.iadd?
Signature: operator.iadd(a, b, /)
Docstring: Same as a = b.
Type: builtin_function_or_method
In [8]: reduce(operator.iadd, counters, Counter())
Out[8]:
Counter({'coach': 5000,
'says': 5000,
'play': 10000,
'basketball': 15000,
'i': 10000,
'said': 10000,
'hate': 5000,
'he': 5000})
И обратите внимание, что теперь производительность соответствует явному циклу:
In [9]: %%timeit
...: data = counters.copy()
...: reduce(operator.iadd, counters, Counter())
...:
...:
22 ms ± 224 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Однако смешивание функциональных конструкций, например reduce
, с функциями, которые имеют побочные эффекты, просто… некрасиво. Лучше придерживаться императивного кода для нечистых функций.
Ответ №3:
Вам нужно заменить update() формой, которую reduce может использовать:
def static_update(x, y):
x.update(y)
return x
all_ct = Counter()
functools.reduce(static_update, counters)
Комментарии:
1. это действительно полезно! Это также быстрее, чем цикл в моих тестах. Хотел бы я понять, почему первоначальная форма обновления не сработала:(
2. @Sam непосредственная проблема заключается в том, что
all_ct.update
требуется на один параметр меньше, чемstatic_update
. Потомуall_ct.update
что это связанный метод,self
был связан в качестве первого аргумента. Но также он возвращаетNone
3. Проблема № 1.
reduce(op, [a, b, c, d])
эквивалентноop(op(op(a, b), c), d)
. Однакоupdate
возвращаетNone
, поэтому этоNone
передается следующему вызовуreduce
. Мне пришлось создать функцию, которая возвращает счетчик.4. Проблема № 2.
all_ct.update
это связанный метод.all_ct
встроен в метод. Это не вписывается в шаблон reduce, который ожидает статический метод без встроенного состояния.