Обновить счетчик из списка счетчиков без цикла?

#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, который ожидает статический метод без встроенного состояния.