Python Counter作为袋子类型的比较
我需要在Python中使用一个像袋子或多重集合那样的数据类型。我知道collections.Counter通常是用来实现这个功能的。但是,比较运算符似乎不起作用:
In [1]: from collections import Counter
In [2]: bag1 = Counter(a=1, b=2, c=3)
In [3]: bag2 = Counter(a=2, b=2)
In [4]: bag1 > bag2
Out[4]: True
我觉得这像是个bug。我原本期待小于和大于运算符能像集合那样进行子集和超集的比较。但是如果真是这样的话,bag1 > bag2
就应该是假的,因为bag2
多了一个'a'
。而且Counter对象上似乎也没有子集和超集的方法。所以我有两个问题:
- Counter对象使用了什么样的比较逻辑?
- 我该如何比较Counter对象的子集、超集、真子集和真超集?
3 个回答
与其使用 all
来遍历 Counter
的值,我们可以这样实现“多重集合包含”检查:
from collections import Counter
from functools import total_ordering
class PartiallyOrderedCounter(Counter):
def __le__(self, other):
""" Multiset inclusion """
test = self.copy()
test.subtract(other)
return not -test
def __lt__(self, other):
""" Multiset strict inclusion """
return self <= other and self != other
这样做可能效率稍低,但足够有趣,值得一提。
它的工作原理是:Counter
的一元取反实现(还有二元 -
操作符)会把结果中的每个计数值都取反,然后去掉所有负数或零的值。而 .subtract
方法则是在原地操作(需要复制一份),但允许结果中有负数。因此,当我们对减法的结果取反时,就得到了一个只包含在 other
中“缺失”值的 Counter
(这些负数计数被取反成正数)。只有在没有这样的值时,计数器对象才会被视为假,这时我们通过使用 not
逻辑运算符返回 True
。
感谢 @PM2Ring 提供这个技巧。
这个没有答案的问题很有意思:
我该如何比较计数器对象,以判断它们是子集、超集、真子集还是真超集呢?
可以通过定义缺失的“丰富比较方法”来解决这个问题。你也可以使用一些自由函数,这样可以让使用这些函数的代码更加清晰。
from collections import Counter
class PartiallyOrderedCounter(Counter):
def __le__(self, other):
""" Multiset inclusion """
return all( v <= other[k] for k,v in self.items() )
def __lt__(self, other):
""" Multiset strict inclusion """
return self <= other and self != other
# TODO : __ge__ and __gt__
# Beware : they CANNOT be written in terms of __le__ or __lt__
a = PartiallyOrderedCounter('abc')
b = PartiallyOrderedCounter('ab')
c = PartiallyOrderedCounter('abe')
assert a <= a
assert not a < a
assert b <= a
assert b < a
assert not a < b
assert not c <= a
assert not a <= c
在Python 2中,比较会退回到字典的默认排序规则(Counter
是dict
的一个子类)。
字典(映射)只有在它们的排序后的(键,值)列表相等时,才会被认为是相等的。[5] 除了相等以外的结果会一致处理,但没有其他定义。[6]
在Python 3中,比较会引发一个TypeError
错误:
字典(映射)只有在它们拥有相同的(键,值)对时,才会被认为是相等的。顺序比较(比如'<','<=','>=','>')会引发
TypeError
错误。