下面的脚本说明了我想要了解的set
和frozenset
的功能,如果可能的话,还会在collections.MutableSet的子类中进行复制。 (顺便说一句,这个功能不仅仅是set
和frozenset
的奇怪之处:它在Python的单元测试中对这些类型进行了主动验证。)
该脚本为类似于对象的几种类型/类中的每一种执行以下步骤:
d
,其n
个密钥是专门检测的整数,用于跟踪调用__hash__
方法的次数(d
的值均为{ {1}},但这无关紧要); None
密钥的__hash__
方法的累计次数(即在创建d
期间); < / LI>
d
作为构造函数的参数创建当前类集类型/类的对象s
(因此,d
的键将成为结果对象,而d
的值将被忽略); 以下是所有类型/类的d
设置为10的情况的输出(我在本文末尾给出完整代码):
n
结论很明确:从set: 10 10
frozenset: 10 10
Set: 10 20
myset: 10 20
构建set
或frozenset
不需要调用d
密钥的__hash__
方法,因此这些构造函数返回后,调用计数保持不变。但是,从d
创建Set
或myset
的实例时,情况并非如此。在每种情况下,似乎每次调用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
。
答案 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)
:您可以在这种情况下指定默认值,并且所有键都将采用该值,但由于您传递了映射,因此不会重新进行任何操作。但是,这仍然不够,我猜;您似乎想为每个密钥提供一个新的和唯一值。
因此,您的真正的问题非常简单,就是可以在不重新调整密钥的情况下为密钥分配新值。当用这种方式表达时,也许你可以看到它不可能以一种直截了当的方式。
通常,没有内置的方法来更改任意键的值:值对而不重新调整键。这有两个原因:
在为任意键分配值时,如果发生冲突,Python需要知道密钥和其哈希值。 Python 可以允许你传递一个键和一个预先计算好的哈希,但是你可以通过传递一个不一致的哈希来搞砸。所以我们所有人都可以让Python在那里做簿记。调用__hash__
的开销是值得的。 (请注意,至少在某些情况下,Python会缓存哈希 - 在这些情况下,这只会查找缓存的哈希值。)
更改值的另一种方法是更改存储在dict
指向的某个特定内存地址的指针值,该地址已保存并与密钥关联。这很简单,涉及暴露方式过多的Python内部结构。但是,这种方法是下面详述的一种黑客解决方案的基础。
现在,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
内部的混乱。我希望你同意这不是一个非常好的。