【问题标题】:What objects are guaranteed to have different identity?保证哪些对象具有不同的身份?
【发布时间】:2012-04-28 16:05:24
【问题描述】:

原始问题:

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

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

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

我了解,对于任何可变类型的对象,“不同对象保证”都成立:

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 感谢您到目前为止的讨论和回答。这是我仍然不清楚的最后一个问题:

有没有机会创建一个用户定义的对象 type 会导致重用现有对象?

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

这是我的理解。每当创建用户定义类的对象时,首先调用该类的__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 对我的用例来说是一个糟糕的主意(我强烈怀疑它根本没有有效的用例)。

【问题讨论】:

  • 您不能创建用户定义的不可变类型 AFAIK。在上面的示例中,两个 DistinctTuple 对象将始终具有不同的 ID。
  • @LuperRouch:你可以,甚至可以通过重载__new__ 获得Nonebool 提供的那种保证,但是对象的不变性不一定由实施。
  • @larsmans:确定你可以在你的__new__ 中返回一个元组,但是你没有创建用户定义的对象。
  • @LuperRouch:您可以创建一个MyTuple 对象池并在MyTuple__new__ 中从中返回对象。
  • @larsmans:我不认为这会让 Python 将这些对象视为不可变的,例如 str 和 tuple (不能保证对象池是不可修改的)。

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


【解决方案1】:

Python 参考,section 3, Data model

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

(已添加重点。)

实际上,CPython 似乎只缓存空元组:

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

【讨论】:

  • tuple 派生的用户定义类是否被认为是不可变的?我可以做一个便宜的修改来使它可变,即获得“不同的对象保证”吗?
  • @max:我认为可变的tuple 子类型会违反LSP。你真的应该继承list
  • 好吧,我不打算修改它。我只想保证每次创建它时,它都会是一个不同的对象。实际上,我不确定 class MyTuple(tuple): pass 是否出于此保证的目的是可变的。
  • @max:您可以在对象上设置属性(a = MyTuple(); a.foo = 1),因此 MyTuple 对象是可变的,并且每个实例都有不同的 ID。
  • @max:您可以重载__new__ 以获得您所追求的保证,但老实说,我会寻找一种不同的方法来解决您要解决的问题。跨度>
【解决方案2】:

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

当且仅当用户定义的类型被明确设计为这样做时,才会发生这种情况。使用__new__() 或一些元类。

我想知道如何验证我使用的任何类是否可能表现出这种行为。

使用来源,卢克。

对于int,小整数是预先分配的,这些预先分配的整数在您创建整数计算的任何地方都使用。当您执行MyInt(1) is MyInt(1) 时,您无法使其正常工作,因为您所拥有的不是整数。然而:

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

这当然是因为 MyInt(1) + MyInt(1) 不会返回 MyInt。它返回一个 int,因为这是整数的 __add__ 返回的内容(这也是检查预分配整数的地方)。如果有什么只是表明子类化 int 通常并不是特别有用。 :-)

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

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

【讨论】:

  • 谢谢。你说不需要保证;但是MyInt(1) is not MyInt(1)默认真的那么明显吗?毕竟MyInt依赖int.__new__()来分配内存。据我所知,int.__new__() 可能总是返回对包含1 的内存的引用——无论我是创建int 还是MyInt
  • 不,这并不明显。你只是不需要关心它。如前所述,您没有理由对 int 进行子类化,这没有任何意义。它不会返回对包含 1 的内存的引用,而是返回对 objects 的引用。如果它的行为像你暗示的那样,那么 MyInt(1) 不会创建 MyInt,它会返回一个 int。它显然没有。
  • @max 用户定义的类型是可变的(自己尝试一下:a = MyInt(); a.foo = "bar"; 你不能在普通的 int 上这样做)。这本身(可变性)证明每次调用 MyInt() 时都会得到一个新对象,否则语言将一团糟。
  • @LennartRegebro:谢谢。因此,MyInt(1) is MyInt(1) 的唯一方法是如果int.__new__ 发疯并不仅缓存int 对象,而且在某些情况下还缓存其子类的对象。这显然没有发生,但严格来说,该语言并不能保证它不会发生,对吧?
  • 是的,如果它缓存了它的子类的对象,或者在上述情况下它根本没有返回 MyInt() 对象(它当然会这样做)。
【解决方案3】:

如果 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 改变)。

猜你喜欢
  • 1970-01-01
  • 2022-10-05
  • 1970-01-01
  • 2015-05-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多