在Python中检查可变性?

42

考虑以下这段代码:

a = {...} # a is an dict with arbitrary contents
b = a.copy()
  1. 在字典的键和值中,可变性扮演了什么角色?
  2. 如何确保一个字典的键或值的更改不会反映在另一个字典中?
  3. 这与字典键的可哈希约束有什么关系? (constraint)
  4. Python 2.x和Python 3.x之间有任何行为差异吗?

如何检查Python中的类型是否可变?

7个回答

26

1) 键不可变,除非您有一个用户定义的类,它既是可哈希的又是可变的。这就是强制执行的全部内容。然而,使用可哈希的可变对象作为字典键可能不是个好主意。

2) 通过不在两个字典之间共享值来实现。共享键是可以的,因为它们必须是不可变的。按照copy模块的方式复制字典绝对是安全的。在此处调用字典构造函数也可以: b = dict(a)。您也可以使用不可变值。

3) 所有内置的不可变类型都是可哈希的。所有内置的可变类型都不是可哈希的。为了使对象可哈希,它必须在其整个生命周期内具有相同的哈希值,即使它被改变了。

4) 我没有意识到过这一点;我正在描述2.x版本。

如果一个类型不是内置的不可变类型,则它是可变的。如果一个类型是内置的不可变类型,它就是不可变的: strintlongboolfloattuple,还有可能有其他几个我忘记了。用户定义的类型总是可变的。

如果一个对象不是不可变的,则它是可变的。如果一个对象由仅由不可变类型的子对象组成,就是不可变的。因此,一个列表的元组是可变的;您不能更改元组的元素,但是可以通过列表接口修改它们,从而改变整个数据。


4
关于不可变对象:"值可以改变的对象称为可变对象;一旦创建后其值无法更改的对象称为不可变对象。(一个包含对可变对象引用的不可变容器对象的值在后者的值改变时可能会发生变化;但是容器仍然被视为不可变,因为它所包含的对象集合不能被更改。因此,不可变性并不严格等同于具有不可更改的值,这更微妙一些。)" http://docs.python.org/py3k/reference/datamodel.html#objects-values-and-types - Ned Deily
1
@Karl Knechtel: 你可以为不可变性提供一个测试吗? - Matt Joiner
1
@Matt:我已经解释过了,这是不可能的——在Python中程序上不存在这个概念,也没有办法进行测试。 - Glenn Maynard
2
用户定义的类型并不总是可变的。例如,声明class mytuple(tuple): pass会创建一个不可变的用户定义类型。 - Pedro Gimeno
没有提到,但所有用户定义类型的实例默认都是可哈希的。https://docs.python.org/2/glossary.html - Alex-Bogdanov
显示剩余8条评论

15

实际上,在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),忽略任何其他属性。

最常见的用途是缓存和记忆化:是否缓存某个属性在逻辑上不会改变对象,并且通常不会影响相等性。

(我要在这里停下了——请不要一次问五个问题。)


前四个问题是我提出最后一个问题的背景。 - Matt Joiner

8

collections 模块中,有 MutableSequence、MutableSet 和 MutableMapping 类型。这些类型可以用来检查预定义类型是否可变。

issubclass(TYPE, (MutableSequence, MutableSet, MutableMapping))

如果您想在用户定义的类型上使用此功能,则该类型必须是从它们之一继承或注册为虚拟子类。

class x(MutableSequence):
    ...

或者

class x:
    ...

abc.ABCMeta.register(MutableSequence,x)

1
请注意,所有用户定义的类默认情况下都是可变的。 - Taha Jahangir

4

并不能保证可散列的类型也是不可变的,但至少要正确实现__hash__,这就要求该类型相对于它自己的哈希值和等式来说是不可变的。没有任何特别的强制规定。

无论如何,我们都是成年人了。如果不是真心想要实现__hash__,那么这样做是不明智的。粗略地说,这意味着如果一个类型实际上可以用作字典键,则它应该以这种方式使用。

如果你正在寻找类似于字典但又是不可变的东西,那么namedtuple可能是标准库中最好的选择。尽管它并不是非常好的近似,但这是一个开始。


1
cPython为了防止字典键不可变的情况下出现问题,采取了一些非常惊人的措施。最糟糕的情况是__getitem__等函数不能返回预期的键,因为它在错误的桶中。 - SingleNegationElimination
实现__hash__与其是否打算用作任何键没有关系,而且为可变类型实现它是完全正常的 - 实际上,每个用户定义的类型都会获得默认的__hash__ id(self)。将对象用作键添加了哈希值不变的要求,但这仅是字典的要求;它对__dict__没有影响。字典是__hash__用户;它们不定义它。 - Glenn Maynard
除了在哈希映射中使用它之外,__hash__ 的确切目的是什么? - SingleNegationElimination
1
哈希有许多用途。如果它不是用于一般用途,Python 就不会有一个名为 hash 的内置函数。 - Glenn Maynard
1
可以,好的,你有什么建议吗? - SingleNegationElimination
我和@TokenMacGuy一样。hash还在哪里被使用了? - Matt Joiner

2
  1. dict 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.

  2. "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
    
  3. I think this is explained by the first two answers.

  4. 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.

总之,以下是所有内容的主要要点:

  • 对于 keys,您关心的可能不是 可变性,而是 可哈希性和可比较性
  • 对于值,您关心可变性,因为根据定义,可变对象的值可以更改而不更改对它的引用。

我不认为有一种通用的方法来测试这两个要点中的任何一个。合适性测试取决于您的用例。例如,检查对象是否实现 __hash__ 和比较(__eq____cmp__)函数可能就足够了。同样地,您可以以某种方式“检查”对象的 __setattr__ 方法,以确定它是否可变。


你能解释一下为什么深拷贝成功了吗? - Matt Joiner
@Matt Joiner,这个问题是因为一个非常愚蠢的复制粘贴错误导致的。它并没有复制,而是像上面的例子一样重新分配了。我已经纠正了示例代码。 - Robert Kluin

1
你可以通过打印数据类型的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))

你可能已经注意到,内存地址在不同的计算机上是不同的,因此你不能检查不同计算机上相同数据类型并得到相同的结果。


-3

字典是无序的键值对集合。键必须是不可变的,因此可哈希。要确定对象是否可哈希,可以使用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()

4
这是错误的。可变对象可以,而且通常是可哈希的。可变性和可哈希性是两个独立的概念。 - Glenn Maynard
我的假设对于内置类型是否正确?还是仅仅巧合,我的代码测试支持我的答案的对象? - amillerrhodes
这对我来说看起来很好。@GlennMaynard,你能给出一些这不起作用的例子吗? - rnbguy
3
class Foo(object): pass x = Foo()以上代码中,x是可变的但可哈希(hashable)的。 - user2357112

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接