summaryrefslogtreecommitdiffstats
path: root/Lib/collections
diff options
context:
space:
mode:
authorRaymond Hettinger <rhettinger@users.noreply.github.com>2020-05-28 15:35:46 (GMT)
committerGitHub <noreply@github.com>2020-05-28 15:35:46 (GMT)
commit60398512c86c5535edd817c99ccb50453b3b0471 (patch)
treee2ce7e054b2cc0a81ddd5bb94a33f3b3369d9937 /Lib/collections
parent0de437de6210c2b32b09d6c47a805b23d023bd59 (diff)
downloadcpython-60398512c86c5535edd817c99ccb50453b3b0471.zip
cpython-60398512c86c5535edd817c99ccb50453b3b0471.tar.gz
cpython-60398512c86c5535edd817c99ccb50453b3b0471.tar.bz2
bpo-40755: Add missing multiset operations to Counter() (GH-20339)
Diffstat (limited to 'Lib/collections')
-rw-r--r--Lib/collections/__init__.py110
1 files changed, 104 insertions, 6 deletions
diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py
index 011a0c1..55fb46c 100644
--- a/Lib/collections/__init__.py
+++ b/Lib/collections/__init__.py
@@ -710,12 +710,24 @@ class Counter(dict):
# To strip negative and zero counts, add-in an empty counter:
# c += Counter()
#
- # Rich comparison operators for multiset subset and superset tests
- # are 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.
+ # When the multiplicities are all zero or one, multiset operations
+ # are guaranteed to be equivalent to the corresponding operations
+ # for regular sets.
+ # Given counter multisets such as:
+ # cp = Counter(a=1, b=0, c=1)
+ # cq = Counter(c=1, d=0, e=1)
+ # The corresponding regular sets would be:
+ # sp = {'a', 'c'}
+ # sq = {'c', 'e'}
+ # All of the following relations would hold:
+ # set(cp + cq) == sp | sq
+ # set(cp - cq) == sp - sq
+ # set(cp | cq) == sp | sq
+ # set(cp & cq) == sp & sq
+ # cp.isequal(cq) == (sp == sq)
+ # cp.issubset(cq) == sp.issubset(sq)
+ # cp.issuperset(cq) == sp.issuperset(sq)
+ # cp.isdisjoint(cq) == sp.isdisjoint(sq)
def __add__(self, other):
'''Add counts from two counters.
@@ -874,6 +886,92 @@ class Counter(dict):
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