35

考虑这段代码

a = {...} # a is an dict with arbitrary contents
b = a.copy()
  1. 可变性在字典的键和值中起什么作用?
  2. 如何确保对一个字典的键或值的更改不会反映在另一个字典中?
  3. 这与dict 键的可散列约束有何关系?
  4. Python 2.x 和 Python 3.x 之间的行为有什么不同吗?

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

4

7 回答 7

21

1) 键不能是可变的,除非您有一个用户定义的可散列但也是可变的类。这就是强加给你的一切。但是,使用可散列的可变对象作为 dict 键可能不是一个好主意。

2)通过不在两个字典之间共享值。可以共享密钥,因为它们必须是不可变的。在copy模块的意义上,复制字典绝对是安全的。b = dict(a)在这里调用 dict构造函数也可以:你也可以使用不可变的值。

3) 所有内置的不可变类型都是可散列的。所有内置的可变类型都不是可散列的。对于一个可散列的对象,它必须在其整个生命周期内具有相同的散列,即使它是变异的。

4)我不知道;我正在描述 2.x。

如果一个类型不是不可变的,那么它就是可变的。如果一个类型是内置的不可变类型,那么它就是不可变的:str, int, long, bool, float, tuple, 可能还有几个我忘记了。用户定义的类型总是可变的。

如果一个对象不是不可变的,那么它就是可变的。如果一个对象递归地仅由不可变类型的子对象组成,则它是不可变的。因此,列表元组是可变的;元组的元素不能替换,但是可以通过列表界面修改,改变整体数据。

于 2010-12-07T06:51:13.297 回答
14

在 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 = 2example(1)example(1)

最常见的用途是缓存和记忆:缓存或不缓存属性不会在逻辑上更改对象,并且通常对相等性没有影响。

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

于 2010-12-07T09:11:34.440 回答
6

模块集合中有 MutableSequence、MutableSet、MutableMapping 。可用于检查预制类型的可变性。

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

如果要在用户定义的类型上使用它,则该类型必须继承自其中之一或注册为虚拟子类。

class x(MutableSequence):
    ...

或者

class x:
    ...

abc.ABCMeta.register(MutableSequence,x)
于 2010-12-07T07:45:52.407 回答
3

确实不能保证可散列的类型也是不可变的,但至少,正确实现__hash__要求该类型是不可变的,就其自身的散列和相等而言。这不是以任何特定方式强制执行的。

然而,我们都是成年人。__hash__除非你真的是认真的,否则实施是不明智的。粗略地说,这只是说如果一个类型实际上可以用作字典键,那么它打算以这种方式使用。

如果您正在寻找类似于dict 的东西,但也是不可变的,那么namedtuple可能是您从标准库中的最佳选择。诚然,这不是一个很好的近似值,但它是一个开始。

于 2010-12-07T06:56:14.397 回答
2
  1. dict必须是可散列的,这意味着它们具有不可变的散列值。dict可能是可变的,也可能不是可变的;但是,如果它们是可变的,这会影响您的第二个问题。

  2. “对键的更改”不会反映在两个字典之间。对不可变值(例如字符串)的更改也不会反映。对可变对象(例如用户定义的类)的更改将被反映,因为该对象是通过 id(即引用)存储的。

    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. 我认为前两个答案可以解释这一点。

  4. 在这方面我不知道。

一些额外的想法

要了解键的行为,需要了解两件主要的事情键必须是可散列的(这意味着它们实现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!

更容易理解,dict 存储对对象的引用。阅读关于 hashable 的部分。像字符串这样的东西是不可变的,如果你“改变”它们,你改变它的字典现在引用一个新对象。可变的对象可以“就地更改”,因此两个字典的值都会改变。

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__某种方式“检查”对象的方法以确定它是否是可变的。

于 2010-12-07T06:58:18.823 回答
0

您可以通过打印该数据类型的内存位置或地址来轻松检查数据类型是可变还是不可变,id如果数据类型是不可变的,则内存位置的地址将随着您更新变量而改变,例如:

stn = 'Hello'
print(id(stn)) 

您将获得该变量的内存位置地址,stn但是当您将该变量与某个值连接然后继续print内存位置地址时,您将同样获得与第一个不同的输出

stn += ' world'
print(id(stn))

当然,您将从第一个获得另一个内存位置地址,但是当您对可变数据类型执行此操作时,内存位置的地址将保持不变,例如

lists = [1, 2, 3]
print(id(lists))

在这里,您将获得内存位置的地址,并且如果您继续并附加一些数字,lists则内存位置的地址将继续相同

lists.append(4)
print(id(lists))

并且您注意到内存位置的地址对于所有计算机都不相同,因此您无法将相同的数据类型检查到不同的计算机并获得相同的结果

于 2020-07-06T16:26:31.920 回答
-2

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

于 2010-12-07T06:57:04.427 回答