原始问题:
(我的问题适用于 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
,假设两个对象绑定到x
and 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
对我的用例来说是一个糟糕的主意(我强烈怀疑它根本没有有效的用例)。