【问题标题】:Unordered list as dict key [closed]无序列表作为dict键[关闭]
【发布时间】:2020-10-30 22:30:56
【问题描述】:

我希望能够做类似的事情:

foo = Counter(['bar', 'shoo', 'bar'])
tmp = {}
tmp[foo] = 5

换句话说,Counter 是否有可散列的等价物? 请注意,我不能使用 frozenset,因为我有重复的元素,我想保留在键中。

编辑: 在我的实际应用中, foo 中的对象可能无法相互比较,因此无法对列表进行排序。

【问题讨论】:

  • 摆脱Counter,对列表进行排序,转换为元组并将其用作键? (魔鬼的倡导者找到你的实施的限制)
  • 列表可以有多长?这是合理的还是我们在谈论数千?对列表元素有任何限制吗?一种快速的解决方案(如果列表很短并且其中不能包含某个特殊字符)是对列表进行字符串化。例如key = '|'.join(['bar', 'shoo', 'bar'])。您可以稍后通过拆分特殊字符来重新创建列表
  • 不幸的是,Python 对此没有很好的解决方案。我会选择上面@roganjosh 的解决方案。如果会有很多重复,也许将所有内容都变成(项目计数)的元组。然后,这些元组可以放入frozenset。
  • @sedavidw 不要忘记对列表进行排序 :)
  • frozenset(foo.items()) 有什么问题?

标签: python python-3.x dictionary


【解决方案1】:

您似乎需要一种使用无序键值对作为键的方法。 frozenset 可能是要走的路,尽管您必须使用Counteritems 而不是keys 创建它。

foo = Counter(['bar', 'shoo', 'bar'])
tmp = {}
tmp[frozenset(foo.items())] = 5

# tmp: {frozenset({('bar', 2), ('shoo', 1)}): 5}

如果这令人满意,您可以通过定义自己的映射类型来实现此转换,如下所示:

from collections import Counter

class CounterDict:
    def __init__(self):
        self.data = {}

    def __setitem__(self, key, value):
        if isinstance(key, Counter):
            self.data[frozenset(key.items())] = value
        else:
            raise TypeError

    def __getitem__(self, key):
        if isinstance(key, Counter):
            return self.data[frozenset(key.items())]
        else:
            raise TypeError

foo = Counter(['bar', 'shoo', 'bar'])
tmp = CounterDict()
tmp[foo] = 42
tmp[foo] # 42

您可以通过使CounterDict 成为collections.UserDict 的子类来使此实现更丰富。

【讨论】:

    【解决方案2】:

    您可以做几件事。一种是对列表进行排序并将结果转换为tuple。这适用于小列表。

    如果您有大型列表(有重复项),您可以将其转换为 frozenset,其元素是 (word, count) 对的元组。例如,如果您的列表是['bar', 'shoo', 'bar'],那么您将拥有frozenset({('bar', 2), ('shoo', 1)})

    您可以使用Counter 构造它,或者只构造一个word: count 条目的dict,然后将这些条目转换为元组并从中构造一个frozenset

    这两者都是可散列的,并且支持原始列表中的重复元素。

    【讨论】:

      【解决方案3】:

      使用 Counter 的字符串表示?

      foo = Counter(['bar', 'shoo', 'bar'])
      tmp = {}
      
      tmp[str(foo)] = 5
      

      【讨论】:

      • 这也是我的第一个想法!它的一个问题是键没有排序,因此您可以拥有具有不同字符串表示的逻辑等效的 Counter 对象,但我认为您可以通过做更多的按摩来解决这个问题(例如,str(sorted(foo.items())) 使它“稳定”)。
      • @Samwise 我认为如果列表中的对象不可比较,这将不起作用。
      【解决方案4】:

      问题在于Counter 是一个可变对象,dict 也是。如果您可以将其转换为不可变的,则将其转换为元组。例如,

      dict_as_tuple = tuple(sorted(foo.items()))
      foo[dict_as_tuple] = 5
      

      您可以通过

      进行向后转换到字典
      d = {k: v for k, v in dict_as_tuple}
      

      我同意 @roganjosh 的观点,即这是魔鬼的拥护者建议,但这是我可能想到的最接近解决方案的事情。

      【讨论】:

      • 比较有信心来自Counterfoo.items() 现在将保持秩序,因此items() 将根据它遇到的字符串。因此,['bar', 'shoo', 'bar']['shoo', 'bar', 'bar'] 将不相等。您需要删除 Counter 并对列表进行排序以使其可重复
      • 实际上,我现在完全不确定自己的观点。最后一个让我思考的问题:)
      • 我认为如果我将两种不同方式生成的同一个Counter对象转换为元组,元组对象可能会不同。感谢指正,我已经解决了!
      【解决方案5】:

      映射键需要是可散列的。我们可以创建 Counter 的 HashableCounter 子类,该子类提供 __hash__() 方法,因此实例可以用作键。

      如果对象相等,哈希值必须相等。哈希值必须独立于订单项被添加到 HashableCounter。

      添加到 HashableCounter 的对象是可散列的。通过按哈希值对键进行排序来定义键的规范顺序。然后将有序 (key, value) 对的元组的哈希值作为 HashableCounter 的哈希值。

      注意事项:

      • 如果 HashableCounter 更新或更改,哈希值将 即使它是同一个对象(相同的 HashableCounter 实例)。
      • 这可能被视为对哈希概念的滥用。
      • 不同运行程序的哈希值可能不同, 不同的 Python 版本或实现、不同的操作系统等。
      • 这不是预期的用法 - 记录它。

      代码如下:

      from collections import Counter
      
      class HashableCounter(Counter):
          def __hash__(self):
              items = sorted(self.items(), key=lambda t:hash(t[0]))
              return hash(tuple(items))
      
      foo1 = HashableCounter(['bar', 'shoo', 'bar'])
      print(f"foo1 = {foo1}, hash = {hash(foo1)}")
      
      # different order, same hash
      foo2 = HashableCounter(['shoo', 'bar', 'bar'])  
      print(f"foo2 = {foo2}, hash = {hash(foo2)}")
      
      #use foo1 as a key
      tmp = {}
      tmp[foo1] = 5
      print(f"tmp = {tmp}")
      
      # update tmp using foo2 as the key
      tmp[foo2] *= 3
      print(f"tmp = {tmp}")
      
      print(foo1 == foo2)
      

      输出:

      foo1 = HashableCounter({'bar': 2, 'shoo': 1}), hash = 8916343845449291478
      foo2 = HashableCounter({'bar': 2, 'shoo': 1}), hash = 8916343845449291478
      tmp = {HashableCounter({'bar': 2, 'shoo': 1}): 5}
      tmp = {HashableCounter({'bar': 2, 'shoo': 1}): 15}
      True
      

      【讨论】:

        猜你喜欢
        • 2018-11-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-16
        • 1970-01-01
        • 2019-04-06
        • 1970-01-01
        • 2022-06-17
        相关资源
        最近更新 更多