Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 13 additions & 41 deletions Doc/library/collections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -290,47 +290,6 @@ For example::
>>> sorted(c.elements())
['a', 'a', 'a', 'a', 'b', 'b']

.. method:: isdisjoint(other)

True if none of the elements in *self* overlap with those in *other*.
Negative or missing counts are ignored.
Logically equivalent to: ``not (+self) & (+other)``

.. versionadded:: 3.10

.. method:: isequal(other)

Test whether counts agree exactly.
Negative or missing counts are treated as zero.

This method works differently than the inherited :meth:`__eq__` method
which treats negative or missing counts as distinct from zero::

>>> Counter(a=1, b=0).isequal(Counter(a=1))
True
>>> Counter(a=1, b=0) == Counter(a=1)
False

Logically equivalent to: ``+self == +other``

.. versionadded:: 3.10

.. method:: issubset(other)

True if the counts in *self* are less than or equal to those in *other*.
Negative or missing counts are treated as zero.
Logically equivalent to: ``not self - (+other)``

.. versionadded:: 3.10

.. method:: issuperset(other)

True if the counts in *self* are greater than or equal to those in *other*.
Negative or missing counts are treated as zero.
Logically equivalent to: ``not other - (+self)``

.. versionadded:: 3.10

.. method:: most_common([n])

Return a list of the *n* most common elements and their counts from the
Expand Down Expand Up @@ -369,6 +328,19 @@ For example::
instead of replacing them. Also, the *iterable* is expected to be a
sequence of elements, not a sequence of ``(key, value)`` pairs.

Counters support rich comparison operators for equality, subset, and
superset relationships: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``.
All of those tests treat missing elements as having zero counts so that
``Counter(a=1) == Counter(a=1, b=0)`` returns true.

.. versionadded:: 3.10
Rich comparison operations we were added

.. versionchanged:: 3.10
In equality tests, missing elements are treated as having zero counts.
Formerly, ``Counter(a=3)`` and ``Counter(a=3, b=0)`` were considered
distinct.

Common patterns for working with :class:`Counter` objects::

sum(c.values()) # total of all counts
Expand Down
122 changes: 36 additions & 86 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,42 @@ def __delitem__(self, elem):
if elem in self:
super().__delitem__(elem)

def __eq__(self, other):
'True if all counts agree. Missing counts are treated as zero.'
if not isinstance(other, Counter):
return NotImplemented
return all(self[e] == other[e] for c in (self, other) for e in c)

def __ne__(self, other):
'True if any counts disagree. Missing counts are treated as zero.'
if not isinstance(other, Counter):
return NotImplemented
return not self == other

def __le__(self, other):
'True if all counts in self are a subset of those in other.'
if not isinstance(other, Counter):
return NotImplemented
return all(self[e] <= other[e] for c in (self, other) for e in c)

def __lt__(self, other):
'True if all counts in self are a proper subset of those in other.'
if not isinstance(other, Counter):
return NotImplemented
return self <= other and self != other

def __ge__(self, other):
'True if all counts in self are a superset of those in other.'
if not isinstance(other, Counter):
return NotImplemented
return all(self[e] >= other[e] for c in (self, other) for e in c)

def __gt__(self, other):
'True if all counts in self are a proper superset of those in other.'
if not isinstance(other, Counter):
return NotImplemented
return self >= other and self != other

def __repr__(self):
if not self:
return '%s()' % self.__class__.__name__
Expand Down Expand Up @@ -886,92 +922,6 @@ def __iand__(self, other):
self[elem] = other_count
return self._keep_positive()

def isequal(self, other):
''' Test whether counts agree exactly.

Negative or missing counts are treated as zero.

This is different than the inherited __eq__() method which
treats negative or missing counts as distinct from zero:

>>> Counter(a=1, b=0).isequal(Counter(a=1))
True
>>> Counter(a=1, b=0) == Counter(a=1)
False

Logically equivalent to: +self == +other
'''
if not isinstance(other, Counter):
other = Counter(other)
for elem in set(self) | set(other):
left = self[elem]
right = other[elem]
if left == right:
continue
if left < 0:
left = 0
if right < 0:
right = 0
if left != right:
return False
return True

def issubset(self, other):
'''True if the counts in self are less than or equal to those in other.

Negative or missing counts are treated as zero.

Logically equivalent to: not self - (+other)
'''
if not isinstance(other, Counter):
other = Counter(other)
for elem, count in self.items():
other_count = other[elem]
if other_count < 0:
other_count = 0
if count > other_count:
return False
return True

def issuperset(self, other):
'''True if the counts in self are greater than or equal to those in other.

Negative or missing counts are treated as zero.

Logically equivalent to: not other - (+self)
'''
if not isinstance(other, Counter):
other = Counter(other)
return other.issubset(self)

def isdisjoint(self, other):
'''True if none of the elements in self overlap with those in other.

Negative or missing counts are ignored.

Logically equivalent to: not (+self) & (+other)
'''
if not isinstance(other, Counter):
other = Counter(other)
for elem, count in self.items():
if count > 0 and other[elem] > 0:
return False
return True

# Rich comparison operators for multiset subset and superset tests
# have been deliberately omitted due to semantic conflicts with the
# existing inherited dict equality method. Subset and superset
# semantics ignore zero counts and require that p⊆q ∧ p⊇q ⇔ p=q;
# however, that would not be the case for p=Counter(a=1, b=0)
# and q=Counter(a=1) where the dictionaries are not equal.

def _omitted(self, other):
raise TypeError(
'Rich comparison operators have been deliberately omitted. '
'Use the isequal(), issubset(), and issuperset() methods instead.')

__lt__ = __le__ = __gt__ = __ge__ = __lt__ = _omitted


########################################################################
### ChainMap
Expand Down
92 changes: 26 additions & 66 deletions Lib/test/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -2123,29 +2123,6 @@ def test_multiset_operations(self):
set_result = setop(set(p.elements()), set(q.elements()))
self.assertEqual(counter_result, dict.fromkeys(set_result, 1))

def test_subset_superset_not_implemented(self):
# Verify that multiset comparison operations are not implemented.

# These operations were intentionally omitted because multiset
# comparison semantics conflict with existing dict equality semantics.

# For multisets, we would expect that if p<=q and p>=q are both true,
# then p==q. However, dict equality semantics require that p!=q when
# one of sets contains an element with a zero count and the other
# doesn't.

p = Counter(a=1, b=0)
q = Counter(a=1, c=0)
self.assertNotEqual(p, q)
with self.assertRaises(TypeError):
p < q
with self.assertRaises(TypeError):
p <= q
with self.assertRaises(TypeError):
p > q
with self.assertRaises(TypeError):
p >= q

def test_inplace_operations(self):
elements = 'abcd'
for i in range(1000):
Expand Down Expand Up @@ -2234,49 +2211,32 @@ def test_multiset_operations_equivalent_to_set_operations(self):
self.assertEqual(set(cp - cq), sp - sq)
self.assertEqual(set(cp | cq), sp | sq)
self.assertEqual(set(cp & cq), sp & sq)
self.assertEqual(cp.isequal(cq), sp == sq)
self.assertEqual(cp.issubset(cq), sp.issubset(sq))
self.assertEqual(cp.issuperset(cq), sp.issuperset(sq))
self.assertEqual(cp.isdisjoint(cq), sp.isdisjoint(sq))

def test_multiset_equal(self):
self.assertTrue(Counter(a=3, b=2, c=0).isequal('ababa'))
self.assertFalse(Counter(a=3, b=2).isequal('babab'))

def test_multiset_subset(self):
self.assertTrue(Counter(a=3, b=2, c=0).issubset('ababa'))
self.assertFalse(Counter(a=3, b=2).issubset('babab'))

def test_multiset_superset(self):
self.assertTrue(Counter(a=3, b=2, c=0).issuperset('aab'))
self.assertFalse(Counter(a=3, b=2, c=0).issuperset('aabd'))

def test_multiset_disjoint(self):
self.assertTrue(Counter(a=3, b=2, c=0).isdisjoint('cde'))
self.assertFalse(Counter(a=3, b=2, c=0).isdisjoint('bcd'))

def test_multiset_predicates_with_negative_counts(self):
# Multiset predicates run on the output of the elements() method,
# meaning that zero counts and negative counts are ignored.
# The tests below confirm that we get that same results as the
# tests above, even after a negative count has been included
# in either *self* or *other*.
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isequal('ababa'))
self.assertFalse(Counter(a=3, b=2, d=-1).isequal('babab'))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issubset('ababa'))
self.assertFalse(Counter(a=3, b=2, d=-1).issubset('babab'))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issuperset('aab'))
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).issuperset('aabd'))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isdisjoint('cde'))
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).isdisjoint('bcd'))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isequal(Counter(a=3, b=2, c=-1)))
self.assertFalse(Counter(a=3, b=2, d=-1).isequal(Counter(a=2, b=3, c=-1)))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issubset(Counter(a=3, b=2, c=-1)))
self.assertFalse(Counter(a=3, b=2, d=-1).issubset(Counter(a=2, b=3, c=-1)))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issuperset(Counter(a=2, b=1, c=-1)))
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).issuperset(Counter(a=2, b=1, c=-1, d=1)))
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isdisjoint(Counter(c=1, d=2, e=3, f=-1)))
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).isdisjoint(Counter(b=1, c=1, d=1, e=-1)))
self.assertEqual(cp == cq, sp == sq)
self.assertEqual(cp != cq, sp != sq)
self.assertEqual(cp <= cq, sp <= sq)
self.assertEqual(cp >= cq, sp >= sq)
self.assertEqual(cp < cq, sp < sq)
self.assertEqual(cp > cq, sp > sq)

def test_eq(self):
self.assertEqual(Counter(a=3, b=2, c=0), Counter('ababa'))
self.assertNotEqual(Counter(a=3, b=2), Counter('babab'))

def test_le(self):
self.assertTrue(Counter(a=3, b=2, c=0) <= Counter('ababa'))
self.assertFalse(Counter(a=3, b=2) <= Counter('babab'))

def test_lt(self):
self.assertTrue(Counter(a=3, b=1, c=0) < Counter('ababa'))
self.assertFalse(Counter(a=3, b=2, c=0) < Counter('ababa'))

def test_ge(self):
self.assertTrue(Counter(a=2, b=1, c=0) >= Counter('aab'))
self.assertFalse(Counter(a=3, b=2, c=0) >= Counter('aabd'))

def test_gt(self):
self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab'))
self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab'))


################################################################################
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add rich comparisons to collections.Counter().