在Python中,两个对象何时相同?

看来, 2 is 23 is 3将永远在Python中是真实的,一般来说,任何引用一个整数是相同的任何其他引用相同的整数。 None发生了同样的情况(即None is None )。 我知道这不会发生在用户定义的types或可变types上。 但它有时也会在不可变types上失败:

 >>> () is () True >>> (2,) is (2,) False 

也就是说:空的元组的两个独立的构造对内存中的同一个对象产生引用,但是相同的一个(不可变的)元素元组的两个独立结构最终创build了两个相同的对象。 我testing了frozenset ,并以类似于元组的方式frozenset的工作。

什么决定了一个对象是否会被复制到内存中,或者只有一个具有大量引用的实例? 这是否取决于对象在某种意义上是否是“primefaces”呢? 它根据实施情况而有所不同吗?

Python有一些types,它保证只有一个实例。 这些实例的例子是NoneNotImplementedEllipsis 。 这些(根据定义)是单身人士,所以像None is None一样None is None保证返回True因为没有办法创build一个新的NoneType实例。

它也提供了几个doubletons 1 TrueFalse 2 – 所有引用True指向同一个对象。 再次,这是因为没有办法创build一个新的bool实例。

以上所有内容都由python语言保证。 但是,正如你已经注意到的,有一些types(都是不可变的)存储一些实例以供重用。 这是由语言所允许的,但是不同的实现可以select使用这个限制,这取决于他们的优化策略。 属于这个类别的一些例子是小整数(-5 – > 255),空tuple和空frozenset

最后,Cpython在分析过程中intern一些不可变的对象。

例如,如果您使用Cpython运行以下脚本,则会看到它返回True

 def foo(): return (2,) if __name__ == '__main__': print foo() is foo() 

这似乎奇怪。 Cpython正在玩的技巧是每当它构造函数foo ,就会看到一个包含其他简单(不可变)文字的元组文字。 python只是创build一次而不是创build这个元组(或它的等价物)。 由于整个交易是不可变的,所以没有改变这个对象的危险。 对于一次又一次地调用相同的紧密循环而言,这对于性能来说可能是一个巨大的胜利。 小string也是被实施的。 真正的胜利在于字典查找。 Python可以做一个(非常快的)指针比较,然后在检查散列冲突时回退到比较慢的string比较。 由于python的大部分都是基于字典查找的,所以这对于整个语言来说可能是一个很大的优化。


1我可能只是编造了这个词…但希望你能明白…
2在正常情况下,你不需要检查对象是否是对True的引用 – 通常你只关心对象是否是“truthy” – 例如, if some_instance: ...将执行该分支。 但是,我只是为了完整而把它放在这里。


请注意,可以用来比较不是单身人士的东西。 一个常见的用法是创build一个标记值:

 sentinel = object() item = next(iterable, sentinel) if items is sentinel: # iterable exhausted. 

要么:

 _sentinel = object() def function(a, b, none_is_ok_value_here=_sentinel): if none_is_ok_value_here is sentinel: # Treat the function as if `none_is_ok_value_here` was not provided. 

这个故事的寓意是总是说出你的意思。 如果你想检查一个值是否是另一个值,那么使用is运算符。 如果你想检查一个值是否等于另一个值(但可能不同),那么使用== 。 有关is== (以及何时使用哪个)之间区别的更多详细信息,请参阅以下任一post:

  • Python中的==和`is`是否有区别?
  • Python无比较:我应该使用“is”还是==?

附录

我们已经讨论了这些CPython的实现细节,我们声称它们是优化的。 我们很高兴能够衡量我们从所有这些优化中获得的东西(除了与is运算符一起工作时,还有一点点混乱)。

string“interning”和字典查找。

下面是一个小脚本,你可以运行这个脚本来看看如果使用相同的string来查找值而不是不同的string,字典查找速度会更快。 请注意,我在variables名称中使用了“interned”一词 – 这些值不一定是实际的(尽pipe它们可能是)。 我只是用它来表示“interned”string字典中的string。

 import timeit interned = 'foo' not_interned = (interned + ' ').strip() assert interned is not not_interned d = {interned: 'bar'} print('Timings for short strings') number = 100000000 print(timeit.timeit( 'd[interned]', setup='from __main__ import interned, d', number=number)) print(timeit.timeit( 'd[not_interned]', setup='from __main__ import not_interned, d', number=number)) #################################################### interned_long = interned * 100 not_interned_long = (interned_long + ' ').strip() d[interned_long] = 'baz' assert interned_long is not not_interned_long print('Timings for long strings') print(timeit.timeit( 'd[interned_long]', setup='from __main__ import interned_long, d', number=number)) print(timeit.timeit( 'd[not_interned_long]', setup='from __main__ import not_interned_long, d', number=number)) 

这里的确切值不应该太重要,但是在我的电脑上,短弦显示速度大约是1分7。 string几乎快了两倍(因为string比较需要更长的时间,如果string有更多字符比较)。 python3.x的差异并不明显,但它们仍然在那里。

元组“实习”

这里有一个你可以玩的小脚本:

 import timeit def foo_tuple(): return (2, 3, 4) def foo_list(): return [2, 3, 4] assert foo_tuple() is foo_tuple() number = 10000000 t_interned_tuple = timeit.timeit('foo_tuple()', setup='from __main__ import foo_tuple', number=number) t_list = (timeit.timeit('foo_list()', setup='from __main__ import foo_list', number=number)) print(t_interned_tuple) print(t_list) print(t_interned_tuple / t_list) print('*' * 80) def tuple_creation(x): return (x,) def list_creation(x): return [x] t_create_tuple = timeit.timeit('tuple_creation(2)', setup='from __main__ import tuple_creation', number=number) t_create_list = timeit.timeit('list_creation(2)', setup='from __main__ import list_creation', number=number) print(t_create_tuple) print(t_create_list) print(t_create_tuple / t_create_list) 

这一个是有点棘手的时间(我很乐意采取任何更好的想法如何在评论时间)。 其中的要点是平均(在我的电脑上),一个元组需要大约60%的时间来创build列表。 但是, foo_tuple()平均占foo_list()花费时间的40%。 这表明我们确实从这些实习生中获得了一点加速。 节省的时间似乎随着元组变大而增加(创build一个较长的列表需要更长的时间 – 元组“创build”自从创build之后需要一段时间)。

另外请注意,我称之为“实习”。 它实际上不是(至less不是在同一个意义上,这些string是被禁止的)。 我们可以在这个简单的脚本中看到不同之处:

 def foo_tuple(): return (2,) def bar_tuple(): return (2,) def foo_string(): return 'foo' def bar_string(): return 'foo' print(foo_tuple() is foo_tuple()) # True print(foo_tuple() is bar_tuple()) # False print(foo_string() is bar_string()) # True 

我们看到这些string确实是“interned” – 使用相同的文字符号的不同的调用返回相同的对象。 元组“实习”似乎是特定于一行的。

根据实施情况而有所不同。

CPython在内存中caching一些不可变的对象。 对于像1和2这样的“小”整数(-5到255,正如下面的评论中所指出的那样)是这样的。 CPython出于性能的原因来做这件事。 大多数程序中通常使用小整数,所以它节省了内存,只创build了一个副本(因为整数是不可变的,因此是安全的)。

像“ None ”的“单身”对象也是如此; 任何时候都只有一个None存在。

其他对象(如空元组, () )可能被实现为单例,或者它们可能不是。

一般来说,你不应该假定不可变对象将以这种方式实现。 CPython是出于性能原因这么做的,但是其他的实现可能不会,而且CPython甚至可能在未来的某个时候停止。 (唯一的例外可能是None ,因为x is None是一个常见的Python习惯用法,很可能在不同的解释器和版本中实现。

通常你想用==代替is 。 Python的运算符是不经常使用的,除非检查variables是否为None