什么对象保证有不同的身份?

时间:2012-04-17 10:06:00

标签: python object python-3.x object-identity

原始问题:

(我的问题适用于Python 3.2+,但我怀疑自Python 2.7以来这已经发生了变化。)

假设我使用了一个我们通常期望创建对象的表达式。示例:[1,2,3]; 42; 'abc'; range(10); True; open('readme.txt'); MyClass(); lambda x : 2 * x;等

假设在不同的时间执行两个这样的表达式并“评估为相同的值”(即,具有相同的类型,并且相等地进行比较)。在什么条件下Python提供我称之为不同对象保证的两个表达式实际上创建了两个不同的对象(即,x is y计算为False,假设两个对象是绑定到xy,两者都在同一时间范围内)?

我理解对于任何可变类型的对象,“不同对象保证”都包含:

x = [1,2]
y = [1,2]
assert x is not y # guaranteed to pass 

我也知道某些不可变类型(strint)保证不成立;对于某些其他不可变类型(boolNoneType),相反的保证成立:

x = True
y = not not x
assert x is not y # guaranteed to fail
x = 2
y = 3 - 1
assert x is not y # implementation-dependent; likely to fail in CPython
x = 1234567890
y = x + 1 - 1
assert x is not y # implementation-dependent; likely to pass in CPython

但是所有其他不可变类型呢?

特别是,在不同时间创建的两个元组是否具有相同的标识?

我对此感兴趣的原因是我将图表中的节点表示为int的元组,并且域模型使得任何两个节点都是不同的(即使它们由元组表示相同的价值观)。我需要创建一组节点。如果Python保证在不同时间创建的元组是不同的对象,我可以简单地将tuple子类化为重新定义相等以表示同一性:

class DistinctTuple(tuple):
  __hash__ = tuple.__hash__
  def __eq__(self, other):
    return self is other

x = (1,2)
y = (1,2)
s = set(x,y)
assert len(s) == 1 # pass; but not what I want
x = DistinctTuple(x)
y = DistinctTuple(y)
s = set(x,y)
assert len(s) == 2 # pass; as desired

但是如果不能保证在不同时间创建的元组是不同的,那么上面是一种可怕的技术,它隐藏了一个可能随机出现并可能很难复制和发现的休眠错误。在这种情况下,子类化将无济于事;我实际上需要添加到每个元组,作为一个额外的元素,一个唯一的ID。或者,我可以将我的元组转换为列表。无论哪种方式,我都会使用更多内存。显然,除非我原来的子类化解决方案不安全,否则我宁愿不使用这些替代方案。

我的猜测是Python不为内置或用户定义的不可变类型提供“独特的对象保证”。但是我没有在文档中找到关于它的明确声明。

更新1:

@LuperRouch @larsmans感谢您的讨论和答案到目前为止。这是我还不清楚的最后一个问题:

  

是否有可能创建用户定义的对象   类型导致重用现有对象?

如果可以的话,我想知道如何验证我使用的任何课程是否可能表现出这种行为。

这是我的理解。每次创建用户定义的类的对象时,首先调用类“__new__()方法”。如果重写此方法,则语言中的任何内容都不会阻止程序员返回对现有对象的引用,从而违反了我的“不同对象保证”。显然,我可以通过检查类定义来观察它。

如果用户定义的类没有覆盖__new__()(或明确地从基类中依赖__new__()),我不确定会发生什么。如果我写

class MyInt(int):
  pass

对象创建由int.__new__()处理。我希望这意味着我有时会看到以下断言失败:

x = MyInt(1)
y = MyInt(1)
assert x is not y # may fail, since int.__new__() might return the same object twice?

但在我对CPython的实验中,我无法实现这样的行为。这是否意味着该语言为不覆盖__new__的用户定义类提供了“独特的对象保证”,还是仅仅是一种任意的实现行为?

更新2:

虽然我的DistinctTuple被证明是一个非常安全的实现,但我现在明白,使用DistinctTuple来模拟节点的设计理念非常糟糕。

身份运营商已经可以使用该语言;使==的行为方式与is相同,在逻辑上是多余的。

更糟糕的是,如果==本可以做一些有用的事情,我就把它变得不可用了。例如,很可能在我的程序中的某个地方我想看看两个节点是否由同一对整数表示; ==本来是完美的 - 事实上,这就是它默认做的事情......

更糟糕的是,大多数人确实希望==比较一些“价值”而不是身份 - 即使是用户定义的类。他们不会意识到我的覆盖只会看到身份。

最后......我必须重新定义==的唯一原因是允许具有相同元组表示的多个节点成为集合的一部分。这是错误的方法!这不是需要改变的==行为,而是容器类型!我只需要使用多重集而不是集合。

简而言之,虽然我的问题可能对其他情况有一些价值,但我绝对相信创建class DistinctTuple对我的用例来说是一个糟糕的想法(我强烈怀疑它根本没有有效的用例)

3 个答案:

答案 0 :(得分:4)

Python参考,section 3, Data model

  

对于不可变类型,计算新值的操作可能实际上返回对具有相同类型和值的任何现有对象的引用,而对于可变对象,不允许

(强调补充。)

在实践中,似乎CPython只缓存空元组:

>>> 1 is 1
True
>>> (1,) is (1,)
False
>>> () is ()
True

答案 1 :(得分:3)

  

是否有可能创建用户定义类型的对象导致重用现有对象?

如果且仅当用户定义的类型明确设计为执行此操作时,才会发生这种情况。使用__new__()或某些元类。

  

我想知道如何验证我使用过的任何课程是否会出现这样的行为。

使用来源,Luke。

当涉及int时,预先分配小整数,并且在使用整数计算的任何地方使用这些预分配的整数。当你执行MyInt(1) is MyInt(1)时,你无法使这个工作,因为你所拥有的不是整数。但是:

>>> MyInt(1) + MyInt(1) is 2
True

这是因为MyInt(1)+ MyInt(1)当然没有返回MyInt。它返回一个int,因为这是整数的__add__返回的内容(也就是检查预分配整数的位置)。如果有的话只是表明通常对int进行子类化并不是特别有用。 : - )

  

这是否意味着该语言为不覆盖 new 的用户定义类提供了“独特的对象保证”,还是仅仅是一种任意的实现行为? < / p>

它不能保证,因为没有必要这样做。默认行为是创建一个新对象。如果您不希望这种情况发生,则必须覆盖它。保证毫无意义。

答案 2 :(得分:1)

  

如果Python保证在不同时间创建的元组是不同的对象,我可以简单地将tuple子类化为重新定义相等以表示身份。

您似乎对子类化如何工作感到困惑:如果B子类A,则B可以使用A的所有方法[1] - 但A方法将用于B的实例,而不是A的实例。这适用于__new__

--> class Node(tuple):
...   def __new__(cls):
...     obj = tuple.__new__(cls)
...     print(type(obj))
...     return obj
...
--> n = Node()
<class '__main__.Node'>

正如@larsman在Python reference中指出的那样:

  

对于不可变类型,计算新值的操作实际上可以返回对具有相同类型和值的任何现有对象的引用,而对于可变对象,则不允许这样做

但是,请记住这篇文章是关于Python的内置类型,而不是用户定义的类型(它们可以像他们喜欢的那样疯狂)。


我理解上面的摘录,以保证Python不会返回与现有对象相同的新可变对象,并且用户定义并在Python代码中创建的类本质上是可变的(再次参见上面的注释)疯狂的用户定义类。)

更完整的Node类(请注意,您不需要明确引用tuple.__hash__):

class Node(tuple):
    __slots__ = tuple()
    __hash__ = tuple.__hash__
    def __eq__(self, other):
        return self is other
    def __ne__(self, other):
        return self is not other

--> n1 = Node()
--> n2 = Node()
--> n1 is n2
False
--> n1 == n2
False
--> n1 != n2
True

--> n1 <= n2
True
--> n1 < n2
False

正如您在上两次比较中所看到的,您可能还希望覆盖__le____ge__方法。

[1]我所知道的唯一例外是__hash__ - 如果在子类上定义__eq__但子类需要父类“__hash__,则必须明确说出所以(这是Python 3的变化)。