如何避免重复dict键?

时间:2012-02-19 01:56:42

标签: python set

下面的脚本说明了我想要了解的setfrozenset的功能,如果可能的话,还会在collections.MutableSet的子类中进行复制。 (顺便说一句,这个功能不仅仅是setfrozenset的奇怪之处:它在Python的单元测试中对这些类型进行了主动验证。)

该脚本为类似于对象的几种类型/类中的每一种执行以下步骤:

  1. 创建一个字典d,其n个密钥是专门检测的整数,用于跟踪调用__hash__方法的次数(d的值均为{ {1}},但这无关紧要);
  2. 计算(并保存以供日后使用)到目前为止已调用None密钥的__hash__方法的累计次数(即在创建d期间); < / LI>
  3. 使用d作为构造函数的参数创建当前类集类型/类的对象s(因此,d的键将成为结果对象,而d的值将被忽略);
  4. 重做(2)中描述的计算;
  5. 输出上述(2)和(4)的计算结果。
  6. 以下是所有类型/类的d设置为10的情况的输出(我在本文末尾给出完整代码):

    n

    结论很明确:从set: 10 10 frozenset: 10 10 Set: 10 20 myset: 10 20 构建setfrozenset不需要调用d密钥的__hash__方法,因此这些构造函数返回后,调用计数保持不变。但是,从d创建Setmyset的实例时,情况并非如此。在每种情况下,似乎每次调用d个键d

      

    如何修改__hash__(见下文)以便运行其myset作为参数的构造函数导致不调用d的密钥'哈希方法?

    谢谢!

    d

    请注意,from sets import Set from collections import MutableSet class hash_counting_int(int): def __init__(self, *args): self.count = 0 def __hash__(self): self.count += 1 return int.__hash__(self) class myset(MutableSet): def __init__(self, iterable=()): # The values of self.dictset matter! See further notes below. self.dictset = dict((item, i) for i, item in enumerate(iterable)) def __bomb(s, *a, **k): raise NotImplementedError add = discard = __contains__ = __iter__ = __len__ = __bomb def test_do_not_rehash_dict_keys(thetype, n=1): d = dict.fromkeys(hash_counting_int(k) for k in xrange(n)) before = sum(elem.count for elem in d) s = thetype(d) after = sum(elem.count for elem in d) return before, after for t in set, frozenset, Set, myset: before, after = test_do_not_rehash_dict_keys(t, 10) print '%s: %d %d' % (t.__name__, before, after) 的值是整数,并且明确与(忽略)self.dictset相同(在iterable.values()的情况下实际存在)!这是一种尝试(当然是微弱的),表明即使iterable.values是一个dict(不一定是这种情况)而且iterable被忽略,在这个例子所代表的真实代码中因为,values的{​​{1}} 总是重要。这意味着基于使用values的任何解决方案仍然必须解决为其键分配正确值的问题,并且再次面临在不调用其self.dictset方法的情况下迭代这些键的问题。 (此外,当self.dictset.update(iterable)不是__hash__的合适参数时,基于self.dictset.update(iterable)的解决方案也必须解决正确处理案例的问题,尽管这个问题并非不可克服。)< / p>

    编辑:1)阐明了myset.dictset值的重要性; 2)将iterable重命名为self.dictset.update

1 个答案:

答案 0 :(得分:2)

在最基本的层面上,它正在重复关键,因为你将一个genex传递给dict而不是映射。

你可以试试这个:

class myset(MutableSet):
    def __init__(self, iterable=()):
        self.dictset = {}
        self.dictset.update(iterable)

    def __bomb__(s, *a, **k): raise NotImplementedError
    add = discard = __contains__ = __iter__ = __len__ = __bomb__

输出:

set: 10 10
frozenset: 10 10
Set: 10 20
myset: 10 10

update也接受了一个genex,但如果iterable是一个映射,那么Python足够聪明,不会重新使用密钥。实际上,您甚至不必像上面那样单独创建字典。只要不将其封装在genex中,您就可以dict(mapping)。但是你已经表明你还想更改与密钥相关的值。从某种意义上说,这可能是dict.fromkeys(mapping, default_val):您可以在这种情况下指定默认值,并且所有键都将采用该值,但由于您传递了映射,因此不会重新进行任何操作。但是,这仍然不够,我猜;您似乎想为每个密钥提供一个新的和唯一值。

因此,您的真正的问题非常简单,就是可以在不重新调整密钥的情况下为密钥分配新值。当用这种方式表达时,也许你可以看到它不可能以一种直截了当的方式。

通常,没有内置的方法来更改任意键的值:值对而不重新调整键。这有两个原因:

  1. 在为任意键分配值时,如果发生冲突,Python需要知道密钥其哈希值。 Python 可以允许你传递一个键和一个预先计算好的哈希,但是你可以通过传递一个不一致的哈希来搞砸。所以我们所有人都可以让Python在那里做簿记。调用__hash__的开销是值得的。 (请注意,至少在某些情况下,Python会缓存哈希 - 在这些情况下,这只会查找缓存的哈希值。)

  2. 更改值的另一种方法是更改​​存储在dict指向的某个特定内存地址的指针值,该地址已保存并与密钥关联。这很简单,涉及暴露方式过多的Python内部结构。但是,这种方法是下面详述的一种黑客解决方案的基础。

  3. 现在,Python本身可以通过操作dict内部结构来有效地合并两个词典,因为1在这种情况下无效;保证所有碰撞已经处理完毕!但同样,这些内部结构不应暴露出来。对于fromkeys,Python可能在内部执行类似于2的操作,但默认值始终相同。我可以想象一种情况,Python会为fromkeys提供另一个关键字扩展,它可以接受函数而不是默认值;它将使用关联的键调用该函数并使用返回的值。那会很酷。但它不存在。

    所以我们的唯一希望是做一些hackish。由于我们非常简单地无法更改与dict键关联的值而不进行重复操作,因此我们只需将该键与可变值相关联即可。

    >>> a = dict((hash_counting_int(x), []) for x in range(10))
    >>> [x.count for x in a.keys()]
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    >>> b = dict(a)
    >>> [x.count for x in a.keys()]
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    >>> for n, v in enumerate(b.itervalues()):
    ...     v.append(n)
    ... 
    >>> [x.count for x in a.keys()]
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    >>> b
    {0: [0], 1: [1], 2: [2], 3: [3], 4: [4], 5: [5], 6: [6], 7: [7], 8: [8], 9: [9]}
    

    不幸的是,这是唯一可能的解决方案,不涉及在dict内部的混乱。我希望你同意这不是一个非常好的。