Коллекции Python.Счетчик` с нецелыми значениями

#python #python-collections

#python #python-коллекции

Вопрос:

collections.Counter чрезвычайно полезен для сбора счетчиков и работы с ними как с объектами. Вы можете делать такие вещи, как:

 >>> Counter({'a': 2, 'b': 5})   Counter({'a': 3, 'c': 7})
Counter({'c': 7, 'a': 5, 'b': 5})
  

Это, по сути, группирует элементы по их ключам и суммирует значения каждой группы.

Каков минимальный способ повторного использования Counter функциональности с нецелыми значениями?

Для этих значений было бы определено дополнение как операция «уменьшения» групповых значений, которую я уже хочу: например, строки и списки (которые оба __add__ определены как конкатенация).

Как есть, мы получаем:

 >>> Counter({'a': 'hello ', 'b': 'B'})   Counter({'a': 'world', 'c': 'C'})
Traceback (most recent call last):
    ...
TypeError: '>' not supported between instances of 'str' and 'int'
>>> Counter({'a': [1, 2], 'b': [3, 4]})   Counter({'a': [1, 1], 'c': [5, 6, 7]})
Traceback (most recent call last):
    ...
TypeError: '>' not supported between instances of 'list' and 'int'
  

В коде collections.Counter существует жестко закодированное предположение, что значения являются целыми числами, и поэтому такие вещи, как self.get(k, 0) и count > 0 , разбросаны повсюду. Поэтому кажется, что создание подклассов Counter было бы не намного меньшей работой, чем переписывание моего собственного специализированного (или общего) пользовательского класса (возможно, с использованием collections.defaultdict ).

Вместо этого кажется, что перенос значений (например, str и list ), чтобы иметь возможность работать с 0, как если бы это был пустой элемент, может быть элегантным подходом.

Комментарии:

1. в чем ваш вопрос?

2. Как следует из его названия — Counter используется для подсчета , поэтому не имеет предопределенного поведения для работы со строками. Как вы сами упомянули, вы могли бы получить то, что ищете, работая с defaultdict s вместо этого

3. Как Counter({'a': 'hello ', 'b': 'B'}) это вообще будет выглядеть? Какими будут значения?

4. «Каков минимальный способ повторного использования функциональности счетчика», какая функциональность счетчика? Возможность «добавлять» похожие элементы вместе? Получение «наиболее распространенных» элементов? Добавление, вычитание и / или сравнение счетчиков? Как это будет работать при объединении строк или списков?

Ответ №1:

Я бы не назвал «определение добавления к целым числам для типов коллекций» более элегантным, чем просто переписывание Counter для выполнения необходимой вам работы. Вам нужно поведение, не связанное с подсчетом, вам нужен класс, который не ориентирован на подсчет вещей.

По сути, Counter это не подходит для вашего варианта использования; у вас нет счетчиков. Что elements означает, когда вам не хватает количества для умножения каждого ключа? most_common может работать так, как написано, но это не будет иметь ничего общего с частотами.

В 95% случаев я бы просто использовал collections.defaultdict(list) (или любое другое значение по умолчанию), а в остальных 5% я бы использовал Counter в качестве модели и реализовал свою собственную версию (без поведения, зависящего от количества).

Ответ №2:

Я бы предложил два решения:

Один переносит сами значения, хотя здесь рассматриваются только примеры в вопросе: Другие Counter операции не являются:

 >>> class ZeroAsEmptyMixin:
...     _empty_val = None
...
...     def __gt__(self, other):
...         if isinstance(other, int) and other == 0:
...             other = self._empty_val
...         return super().__gt__(other)
...
...     def __add__(self, other):
...         if isinstance(other, int) and other == 0:
...             other = self._empty_val
...         return self.__class__(super().__add__(other))
...
...
>>> class mystr(ZeroAsEmptyMixin, str):
...     _empty_val = str()
...
...
>>> class mylist(ZeroAsEmptyMixin, list):
...     _empty_val = list()
...
...
>>>
>>> Counter({'a': mystr('hello '), 'b': mystr('B')})   Counter({'a': mystr('world'), 'c': mystr('C')})
Counter({'a': 'hello world', 'c': 'C', 'b': 'B'})
>>> Counter({'a': mylist([1, 2]), 'b': mylist([3, 4])})   Counter({'a': mylist([1, 1]), 'c': mylist([5, 6, 7])})
Counter({'c': [5, 6, 7], 'b': [3, 4], 'a': [1, 2, 1, 1]})
  

Другой — написанием пользовательского класса, подобного счетчику, подклассом defaultdict и, опять же, только реализацией __add__ метода.

 >>> from collections import defaultdict
>>> from functools import wraps
>>>
>>> class MapReducer(defaultdict):
...     @wraps(defaultdict.__init__)
...     def __init__(self, *args, **kwargs):
...         super().__init__(*args, **kwargs)
...         self._empty_val = self.default_factory()
...
...     def __add__(self, other):
...         if not isinstance(other, self.__class__):
...             return NotImplemented
...         result = self.__class__(self.default_factory)
...         for elem, val in self.items():
...             newval = val   other[elem]
...             if newval != self._empty_val:
...                 result[elem] = newval
...         for elem, val in other.items():
...             if elem not in self and val != self._empty_val:
...                 result[elem] = val
...         return result
...
...
>>>
>>> strmp = lambda x: MapReducer(str, x)
>>> strmp({'a': 'hello ', 'b': 'B'})   strmp({'a': 'world', 'c': 'C'})
MapReducer(<class 'str'>, {'a': 'hello world', 'b': 'B', 'c': 'C'})
>>> listmp = lambda x: MapReducer(list, x)
>>> listmp({'a': [1, 2], 'b': [3, 4]})   listmp({'a': [1, 1], 'c': [5, 6, 7]})
MapReducer(<class 'list'>, {'a': [1, 2, 1, 1], 'b': [3, 4], 'c': [5, 6, 7]})