#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]})