Skip to content

Commit b7d79b4

Browse files
authored
bpo-40755: Add rich comparisons to Counter (GH-20548)
1 parent 2b20136 commit b7d79b4

File tree

5 files changed

+76
-195
lines changed

5 files changed

+76
-195
lines changed

Doc/library/collections.rst

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -290,47 +290,6 @@ For example::
290290
>>> sorted(c.elements())
291291
['a', 'a', 'a', 'a', 'b', 'b']
292292

293-
.. method:: isdisjoint(other)
294-
295-
True if none of the elements in *self* overlap with those in *other*.
296-
Negative or missing counts are ignored.
297-
Logically equivalent to: ``not (+self) & (+other)``
298-
299-
.. versionadded:: 3.10
300-
301-
.. method:: isequal(other)
302-
303-
Test whether counts agree exactly.
304-
Negative or missing counts are treated as zero.
305-
306-
This method works differently than the inherited :meth:`__eq__` method
307-
which treats negative or missing counts as distinct from zero::
308-
309-
>>> Counter(a=1, b=0).isequal(Counter(a=1))
310-
True
311-
>>> Counter(a=1, b=0) == Counter(a=1)
312-
False
313-
314-
Logically equivalent to: ``+self == +other``
315-
316-
.. versionadded:: 3.10
317-
318-
.. method:: issubset(other)
319-
320-
True if the counts in *self* are less than or equal to those in *other*.
321-
Negative or missing counts are treated as zero.
322-
Logically equivalent to: ``not self - (+other)``
323-
324-
.. versionadded:: 3.10
325-
326-
.. method:: issuperset(other)
327-
328-
True if the counts in *self* are greater than or equal to those in *other*.
329-
Negative or missing counts are treated as zero.
330-
Logically equivalent to: ``not other - (+self)``
331-
332-
.. versionadded:: 3.10
333-
334293
.. method:: most_common([n])
335294

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

331+
Counters support rich comparison operators for equality, subset, and
332+
superset relationships: ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``.
333+
All of those tests treat missing elements as having zero counts so that
334+
``Counter(a=1) == Counter(a=1, b=0)`` returns true.
335+
336+
.. versionadded:: 3.10
337+
Rich comparison operations we were added
338+
339+
.. versionchanged:: 3.10
340+
In equality tests, missing elements are treated as having zero counts.
341+
Formerly, ``Counter(a=3)`` and ``Counter(a=3, b=0)`` were considered
342+
distinct.
343+
372344
Common patterns for working with :class:`Counter` objects::
373345

374346
sum(c.values()) # total of all counts

Lib/collections/__init__.py

Lines changed: 36 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,42 @@ def __delitem__(self, elem):
691691
if elem in self:
692692
super().__delitem__(elem)
693693

694+
def __eq__(self, other):
695+
'True if all counts agree. Missing counts are treated as zero.'
696+
if not isinstance(other, Counter):
697+
return NotImplemented
698+
return all(self[e] == other[e] for c in (self, other) for e in c)
699+
700+
def __ne__(self, other):
701+
'True if any counts disagree. Missing counts are treated as zero.'
702+
if not isinstance(other, Counter):
703+
return NotImplemented
704+
return not self == other
705+
706+
def __le__(self, other):
707+
'True if all counts in self are a subset of those in other.'
708+
if not isinstance(other, Counter):
709+
return NotImplemented
710+
return all(self[e] <= other[e] for c in (self, other) for e in c)
711+
712+
def __lt__(self, other):
713+
'True if all counts in self are a proper subset of those in other.'
714+
if not isinstance(other, Counter):
715+
return NotImplemented
716+
return self <= other and self != other
717+
718+
def __ge__(self, other):
719+
'True if all counts in self are a superset of those in other.'
720+
if not isinstance(other, Counter):
721+
return NotImplemented
722+
return all(self[e] >= other[e] for c in (self, other) for e in c)
723+
724+
def __gt__(self, other):
725+
'True if all counts in self are a proper superset of those in other.'
726+
if not isinstance(other, Counter):
727+
return NotImplemented
728+
return self >= other and self != other
729+
694730
def __repr__(self):
695731
if not self:
696732
return '%s()' % self.__class__.__name__
@@ -886,92 +922,6 @@ def __iand__(self, other):
886922
self[elem] = other_count
887923
return self._keep_positive()
888924

889-
def isequal(self, other):
890-
''' Test whether counts agree exactly.
891-
892-
Negative or missing counts are treated as zero.
893-
894-
This is different than the inherited __eq__() method which
895-
treats negative or missing counts as distinct from zero:
896-
897-
>>> Counter(a=1, b=0).isequal(Counter(a=1))
898-
True
899-
>>> Counter(a=1, b=0) == Counter(a=1)
900-
False
901-
902-
Logically equivalent to: +self == +other
903-
'''
904-
if not isinstance(other, Counter):
905-
other = Counter(other)
906-
for elem in set(self) | set(other):
907-
left = self[elem]
908-
right = other[elem]
909-
if left == right:
910-
continue
911-
if left < 0:
912-
left = 0
913-
if right < 0:
914-
right = 0
915-
if left != right:
916-
return False
917-
return True
918-
919-
def issubset(self, other):
920-
'''True if the counts in self are less than or equal to those in other.
921-
922-
Negative or missing counts are treated as zero.
923-
924-
Logically equivalent to: not self - (+other)
925-
'''
926-
if not isinstance(other, Counter):
927-
other = Counter(other)
928-
for elem, count in self.items():
929-
other_count = other[elem]
930-
if other_count < 0:
931-
other_count = 0
932-
if count > other_count:
933-
return False
934-
return True
935-
936-
def issuperset(self, other):
937-
'''True if the counts in self are greater than or equal to those in other.
938-
939-
Negative or missing counts are treated as zero.
940-
941-
Logically equivalent to: not other - (+self)
942-
'''
943-
if not isinstance(other, Counter):
944-
other = Counter(other)
945-
return other.issubset(self)
946-
947-
def isdisjoint(self, other):
948-
'''True if none of the elements in self overlap with those in other.
949-
950-
Negative or missing counts are ignored.
951-
952-
Logically equivalent to: not (+self) & (+other)
953-
'''
954-
if not isinstance(other, Counter):
955-
other = Counter(other)
956-
for elem, count in self.items():
957-
if count > 0 and other[elem] > 0:
958-
return False
959-
return True
960-
961-
# Rich comparison operators for multiset subset and superset tests
962-
# have been deliberately omitted due to semantic conflicts with the
963-
# existing inherited dict equality method. Subset and superset
964-
# semantics ignore zero counts and require that p⊆q ∧ p⊇q ⇔ p=q;
965-
# however, that would not be the case for p=Counter(a=1, b=0)
966-
# and q=Counter(a=1) where the dictionaries are not equal.
967-
968-
def _omitted(self, other):
969-
raise TypeError(
970-
'Rich comparison operators have been deliberately omitted. '
971-
'Use the isequal(), issubset(), and issuperset() methods instead.')
972-
973-
__lt__ = __le__ = __gt__ = __ge__ = __lt__ = _omitted
974-
975925

976926
########################################################################
977927
### ChainMap

Lib/test/test_collections.py

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2123,29 +2123,6 @@ def test_multiset_operations(self):
21232123
set_result = setop(set(p.elements()), set(q.elements()))
21242124
self.assertEqual(counter_result, dict.fromkeys(set_result, 1))
21252125

2126-
def test_subset_superset_not_implemented(self):
2127-
# Verify that multiset comparison operations are not implemented.
2128-
2129-
# These operations were intentionally omitted because multiset
2130-
# comparison semantics conflict with existing dict equality semantics.
2131-
2132-
# For multisets, we would expect that if p<=q and p>=q are both true,
2133-
# then p==q. However, dict equality semantics require that p!=q when
2134-
# one of sets contains an element with a zero count and the other
2135-
# doesn't.
2136-
2137-
p = Counter(a=1, b=0)
2138-
q = Counter(a=1, c=0)
2139-
self.assertNotEqual(p, q)
2140-
with self.assertRaises(TypeError):
2141-
p < q
2142-
with self.assertRaises(TypeError):
2143-
p <= q
2144-
with self.assertRaises(TypeError):
2145-
p > q
2146-
with self.assertRaises(TypeError):
2147-
p >= q
2148-
21492126
def test_inplace_operations(self):
21502127
elements = 'abcd'
21512128
for i in range(1000):
@@ -2234,49 +2211,32 @@ def test_multiset_operations_equivalent_to_set_operations(self):
22342211
self.assertEqual(set(cp - cq), sp - sq)
22352212
self.assertEqual(set(cp | cq), sp | sq)
22362213
self.assertEqual(set(cp & cq), sp & sq)
2237-
self.assertEqual(cp.isequal(cq), sp == sq)
2238-
self.assertEqual(cp.issubset(cq), sp.issubset(sq))
2239-
self.assertEqual(cp.issuperset(cq), sp.issuperset(sq))
2240-
self.assertEqual(cp.isdisjoint(cq), sp.isdisjoint(sq))
2241-
2242-
def test_multiset_equal(self):
2243-
self.assertTrue(Counter(a=3, b=2, c=0).isequal('ababa'))
2244-
self.assertFalse(Counter(a=3, b=2).isequal('babab'))
2245-
2246-
def test_multiset_subset(self):
2247-
self.assertTrue(Counter(a=3, b=2, c=0).issubset('ababa'))
2248-
self.assertFalse(Counter(a=3, b=2).issubset('babab'))
2249-
2250-
def test_multiset_superset(self):
2251-
self.assertTrue(Counter(a=3, b=2, c=0).issuperset('aab'))
2252-
self.assertFalse(Counter(a=3, b=2, c=0).issuperset('aabd'))
2253-
2254-
def test_multiset_disjoint(self):
2255-
self.assertTrue(Counter(a=3, b=2, c=0).isdisjoint('cde'))
2256-
self.assertFalse(Counter(a=3, b=2, c=0).isdisjoint('bcd'))
2257-
2258-
def test_multiset_predicates_with_negative_counts(self):
2259-
# Multiset predicates run on the output of the elements() method,
2260-
# meaning that zero counts and negative counts are ignored.
2261-
# The tests below confirm that we get that same results as the
2262-
# tests above, even after a negative count has been included
2263-
# in either *self* or *other*.
2264-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isequal('ababa'))
2265-
self.assertFalse(Counter(a=3, b=2, d=-1).isequal('babab'))
2266-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issubset('ababa'))
2267-
self.assertFalse(Counter(a=3, b=2, d=-1).issubset('babab'))
2268-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issuperset('aab'))
2269-
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).issuperset('aabd'))
2270-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isdisjoint('cde'))
2271-
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).isdisjoint('bcd'))
2272-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isequal(Counter(a=3, b=2, c=-1)))
2273-
self.assertFalse(Counter(a=3, b=2, d=-1).isequal(Counter(a=2, b=3, c=-1)))
2274-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issubset(Counter(a=3, b=2, c=-1)))
2275-
self.assertFalse(Counter(a=3, b=2, d=-1).issubset(Counter(a=2, b=3, c=-1)))
2276-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).issuperset(Counter(a=2, b=1, c=-1)))
2277-
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).issuperset(Counter(a=2, b=1, c=-1, d=1)))
2278-
self.assertTrue(Counter(a=3, b=2, c=0, d=-1).isdisjoint(Counter(c=1, d=2, e=3, f=-1)))
2279-
self.assertFalse(Counter(a=3, b=2, c=0, d=-1).isdisjoint(Counter(b=1, c=1, d=1, e=-1)))
2214+
self.assertEqual(cp == cq, sp == sq)
2215+
self.assertEqual(cp != cq, sp != sq)
2216+
self.assertEqual(cp <= cq, sp <= sq)
2217+
self.assertEqual(cp >= cq, sp >= sq)
2218+
self.assertEqual(cp < cq, sp < sq)
2219+
self.assertEqual(cp > cq, sp > sq)
2220+
2221+
def test_eq(self):
2222+
self.assertEqual(Counter(a=3, b=2, c=0), Counter('ababa'))
2223+
self.assertNotEqual(Counter(a=3, b=2), Counter('babab'))
2224+
2225+
def test_le(self):
2226+
self.assertTrue(Counter(a=3, b=2, c=0) <= Counter('ababa'))
2227+
self.assertFalse(Counter(a=3, b=2) <= Counter('babab'))
2228+
2229+
def test_lt(self):
2230+
self.assertTrue(Counter(a=3, b=1, c=0) < Counter('ababa'))
2231+
self.assertFalse(Counter(a=3, b=2, c=0) < Counter('ababa'))
2232+
2233+
def test_ge(self):
2234+
self.assertTrue(Counter(a=2, b=1, c=0) >= Counter('aab'))
2235+
self.assertFalse(Counter(a=3, b=2, c=0) >= Counter('aabd'))
2236+
2237+
def test_gt(self):
2238+
self.assertTrue(Counter(a=3, b=2, c=0) > Counter('aab'))
2239+
self.assertFalse(Counter(a=2, b=1, c=0) > Counter('aab'))
22802240

22812241

22822242
################################################################################

Misc/NEWS.d/next/Library/2020-05-23-18-24-13.bpo-22533.k64XGo.rst

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add rich comparisons to collections.Counter().

0 commit comments

Comments
 (0)