考虑以下这段代码:
a = {...} # a is an dict with arbitrary contents
b = a.copy()
- 在字典的键和值中,可变性扮演了什么角色?
- 如何确保一个字典的键或值的更改不会反映在另一个字典中?
- 这与字典键的可哈希约束有什么关系? (constraint)
- Python 2.x和Python 3.x之间有任何行为差异吗?
如何检查Python中的类型是否可变?
考虑以下这段代码:
a = {...} # a is an dict with arbitrary contents
b = a.copy()
如何检查Python中的类型是否可变?
1) 键不可变,除非您有一个用户定义的类,它既是可哈希的又是可变的。这就是强制执行的全部内容。然而,使用可哈希的可变对象作为字典键可能不是个好主意。
2) 通过不在两个字典之间共享值来实现。共享键是可以的,因为它们必须是不可变的。按照copy
模块的方式复制字典绝对是安全的。在此处调用字典构造函数也可以: b = dict(a)
。您也可以使用不可变值。
3) 所有内置的不可变类型都是可哈希的。所有内置的可变类型都不是可哈希的。为了使对象可哈希,它必须在其整个生命周期内具有相同的哈希值,即使它被改变了。
4) 我没有意识到过这一点;我正在描述2.x版本。
如果一个类型不是内置的不可变类型,则它是可变的。如果一个类型是内置的不可变类型,它就是不可变的: str
、int
、long
、bool
、float
、tuple
,还有可能有其他几个我忘记了。用户定义的类型总是可变的。
如果一个对象不是不可变的,则它是可变的。如果一个对象由仅由不可变类型的子对象组成,就是不可变的。因此,一个列表的元组是可变的;您不能更改元组的元素,但是可以通过列表接口修改它们,从而改变整个数据。
实际上,在Python语言级别上,不存在可变或不可变的概念。一些对象不提供任何更改它们的方式(例如字符串和元组),因此是“有效不可变的”,但这只是概念性的;在语言级别上,没有任何属性表明这一点,无论是对您的代码还是对Python本身。
不可变性实际上与字典无关;使用可变值作为键完全没有问题。重要的是比较和哈希:对象必须始终等于自身。例如:
class example(object):
def __init__(self, a):
self.value = a
def __eq__(self, rhs):
return self.value == rhs.value
def __hash__(self):
return hash(self.value)
a = example(1)
d = {a: "first"}
a.data = 2
print d[example(1)]
在这里,example
并不是不可变的;我们使用a.data = 2
来修改它。然而,我们无需担心就将其用作散列的键。为什么?我们所改变的属性对相等性没有影响:散列没有改变,而且example(1)
始终等于example(1)
,忽略任何其他属性。
最常见的用途是缓存和记忆化:是否缓存某个属性在逻辑上不会改变对象,并且通常不会影响相等性。
(我要在这里停下了——请不要一次问五个问题。)
在 collections 模块中,有 MutableSequence、MutableSet 和 MutableMapping 类型。这些类型可以用来检查预定义类型是否可变。
issubclass(TYPE, (MutableSequence, MutableSet, MutableMapping))
如果您想在用户定义的类型上使用此功能,则该类型必须是从它们之一继承或注册为虚拟子类。
class x(MutableSequence):
...
或者
class x:
...
abc.ABCMeta.register(MutableSequence,x)
并不能保证可散列的类型也是不可变的,但至少要正确实现__hash__
,这就要求该类型相对于它自己的哈希值和等式来说是不可变的。没有任何特别的强制规定。
无论如何,我们都是成年人了。如果不是真心想要实现__hash__
,那么这样做是不明智的。粗略地说,这意味着如果一个类型实际上可以用作字典键,则它应该以这种方式使用。
如果你正在寻找类似于字典但又是不可变的东西,那么namedtuple
可能是标准库中最好的选择。尽管它并不是非常好的近似,但这是一个开始。
__getitem__
等函数不能返回预期的键,因为它在错误的桶中。 - SingleNegationElimination__hash__
与其是否打算用作任何键没有关系,而且为可变类型实现它是完全正常的 - 实际上,每个用户定义的类型都会获得默认的__hash__
id(self)
。将对象用作键添加了哈希值不变的要求,但这仅是字典的要求;它对__dict__
没有影响。字典是__hash__
的用户;它们不定义它。 - Glenn Maynard__hash__
的确切目的是什么? - SingleNegationEliminationhash
的内置函数。 - Glenn Maynardhash
还在哪里被使用了? - Matt Joinerdict keys must be hashable, which implies they have an immutable hash value. dict values may or may not be mutable; however, if they are mutable this impacts your second question.
"Changes to the keys" will not be reflected between the two dicts. Changes to immutable values, such as strings will also not be reflected. Changes to mutable objects, such as user defined classes will be reflected because the object is stored by id (i.e. reference).
class T(object):
def __init__(self, v):
self.v = v
t1 = T(5)
d1 = {'a': t1}
d2 = d1.copy()
d2['a'].v = 7
d1['a'].v # = 7
d2['a'] = T(2)
d2['a'].v # = 2
d1['a'].v # = 7
import copy
d3 = copy.deepcopy(d2) # perform a "deep copy"
d3['a'].v = 12
d3['a'].v # = 12
d2['a'].v # = 2
I think this is explained by the first two answers.
Not that I know of in this respect.
一些额外的想法:
要理解 键(keys) 的行为,需要知道两件事情:键必须是可哈希的(这意味着它们实现了object.__hash__(self)
),并且它们还必须是“可比较”的(这意味着它们实现了类似于object.__cmp__(self)
的东西)。文档中一个重要的收获是:默认情况下,用户定义的对象的哈希函数返回id()
。
考虑以下示例:
class K(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __hash__(self):
return self.x + self.y
k1 = K(1, 2)
d1 = {k1: 3}
d1[k1] # outputs 3
k1.x = 5
d1[k1] # KeyError! The key's hash has changed!
k2 = K(2, 1)
d1[k2] # KeyError! The key's hash is right, but the keys aren't equal.
k1.x = 1
d1[k1] # outputs 3
class NewK(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __hash__(self):
return self.x + self.y
def __cmp__(self, other):
return self.x - other.x
nk1 = NewK(3, 4)
nd1 = {nk1: 5}
nd1[nk1] # outputs 5
nk2 = NewK(3, 7)
nk1 == nk2 # True!
nd1[nk2] # KeyError! The keys' hashes differ.
hash(nk1) == hash(nk2) # False
nk2.y = 4
nd1[nk2] # outputs 5
# Where this can cause issues:
nd1.keys()[0].x = 5
nd1[nk1] # KeyError! nk1 is no longer in the dict!
id(nd1.keys()[0]) == id(nk1) # Yikes. True?!
nd1.keys()[0].x = 3
nd1[nk1] # outputs 5
id(nd1.keys()[0]) == id(nk1) # True!
值更容易理解,字典存储对象的引用。 请阅读可哈希部分。 字符串之类的东西是不可变的,如果您“更改”它们,则更改后的字典现在引用一个新对象。 可变对象可以“原地更改”,因此两个字典的值都将更改。
d1 = {1: 'a'}
d2 = d1.copy()
id(d1[1]) == id(d2[1]) # True
d2[1] = 'z'
id(d1[1]) == id(d2[1]) # False
# the examples in section 2 above have more examples of this.
总之,以下是所有内容的主要要点:
我不认为有一种通用的方法来测试这两个要点中的任何一个。合适性测试取决于您的用例。例如,检查对象是否实现 __hash__
和比较(__eq__
或 __cmp__
)函数可能就足够了。同样地,您可以以某种方式“检查”对象的 __setattr__
方法,以确定它是否可变。
id
或内存位置来轻松检查数据类型是可变还是不可变的。如果数据类型是不可变的,那么随着您更新变量,内存位置的地址将发生更改,例如:stn = 'Hello'
print(id(stn))
stn
的内存地址,但是当你将该变量与某个值连接起来并继续打印内存地址时,你将得到与第一个输出不同的输出。stn += ' world'
print(id(stn))
lists = [1, 2, 3]
print(id(lists))
列表
添加一些数字,内存位置的地址将继续保持不变。lists.append(4)
print(id(lists))
你可能已经注意到,内存地址在不同的计算机上是不同的,因此你不能检查不同计算机上相同数据类型并得到相同的结果。
字典是无序的键值对集合。键必须是不可变的,因此可哈希。要确定对象是否可哈希,可以使用hash()
函数:
>>> hash(1)
1
>>> hash('a')
12416037344
>>> hash([1,2,3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> hash((1,2,3))
2528502973977326415
>>> hash({1: 1})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
hash()
。class Foo(object): pass
x = Foo()
以上代码中,x
是可变的但可哈希(hashable)的。 - user2357112
class mytuple(tuple): pass
会创建一个不可变的用户定义类型。 - Pedro Gimeno