3

原始问题:

(我的问题适用于 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,假设两个对象绑定到xand y,并且两者同时在范围内)?

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

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

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

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节点建模的设计理念非常糟糕。

身份运算符已经在该语言中可用;make 的==行为方式与is逻辑上是多余的相同。

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

更糟糕的是,大多数人实际上确实希望==比较一些“价值”而不是身份——即使对于用户定义的类也是如此。他们会因为我只看身份的覆盖而措手不及。

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

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

4

3 回答 3

4

Python 参考,第 3 节,数据模型

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

(强调补充。)

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

>>> 1 is 1
True
>>> (1,) is (1,)
False
>>> () is ()
True
于 2012-04-17T10:16:15.047 回答
3

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

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

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

使用来源,卢克。

说到int,小整数是预先分配的,并且这些预先分配的整数在您使用整数创建计算的任何地方都使用。当你这样做时,你不能让它工作MyInt(1) is MyInt(1),因为你所拥有的不是整数。然而:

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

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

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

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

于 2012-04-18T04:01:00.983 回答
1

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

您似乎对子类化的工作方式感到困惑:如果Bsubclasses 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 参考中指出的那样:

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

但是,请记住,这篇文章是在讨论 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__and__ge__方法。

[1] 我知道的唯一例外是__hash__——如果__eq__在子类上定义但子类想要父类__hash__,则必须明确说明(这是 Python 3 的更改)。

于 2012-04-18T14:15:20.107 回答