【问题标题】:In Python, when are two objects the same?在 Python 中,什么时候两个对象相同?
【发布时间】:2016-08-22 06:54:45
【问题描述】:

似乎2 is 23 is 3 在python 中总是正确的,一般来说,任何对整数的引用都与对同一整数的任何其他引用相同。 None(即None is None)也是如此。我知道这不会发生在用户定义的类型或可变类型上。但它有时也会在不可变类型上失败:

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

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

什么决定了一个对象是否会在内存中重复,或者是否会有一个包含大量引用的实例?它是否取决于对象在某种意义上是否是“原子的”?是否因实施而异?

【问题讨论】:

标签: python oop python-3.x object reference


【解决方案1】:

Python 有一些类型,它保证只有一个实例。这些实例的示例是NoneNotImplementedEllipsis。这些是(根据定义)单例,所以像None is None 这样的东西保证会返回True,因为没有办法创建NoneType 的新实例。

它还提供了一些 doubletons 1True, False 2 -- 所有对 True 的引用都指向同一个对象。同样,这是因为无法创建 bool 的新实例。

以上的事情都是python语言保证的。但是,正如您所注意到的,有些类型(都是不可变的)存储了一些实例以供重用。这是语言允许的,但不同的实现可能会选择使用或不使用这种允许——取决于它们的优化策略。属于此类的一些示例是小整数 (-5 -> 255)、空的 tuple 和空的 frozenset

最后,Cpython interns 解析过程中的某些不可变对象...

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

def foo():
    return (2,)

if __name__ == '__main__':
    print foo() is foo()

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


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


请注意,is 可用于比较非单例的事物。一种常见的用途是创建一个标记值:

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== 之间区别的更多详细信息(以及何时使用),请参阅以下帖子之一:


附录

我们已经讨论了这些 CPython 实现细节,并且我们声称它们是优化。尝试衡量我们从所有这些优化中得到的结果会很好(除了在使用 is 运算符时增加一点混乱)。

字符串“实习”和字典查找。

这是一个小脚本,如果您使用相同的字符串而不是不同的字符串来查找值,您可以运行该脚本来查看字典查找的速度有多快。请注意,我在变量名称中使用了术语“interned”——这些值不一定是 interned(尽管它们可能是)。我只是用它来表示“实习”字符串字典中的字符串。

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))

这里的确切值应该不太重要,但在我的计算机上,短字符串显示大约 7 分之一的速度更快。 long 字符串几乎快 2 倍(因为如果字符串有更多字符要比较,则字符串比较需要更长的时间)。这些差异在 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)

这个时间有点棘手(我很高兴有更好的想法如何在 cmets 中计时)。其要点是平均而言(在我的计算机上),创建一个元组所需的时间大约是创建列表的 60%。但是,foo_tuple() 平均花费的时间大约是 foo_list() 的 40%。这表明我们确实从这些实习生那里获得了一点加速。随着元组变大,节省的时间似乎会增加(创建更长的列表需要更长的时间——元组“创建”需要恒定的时间,因为它已经创建了)。

另请注意,我称其为“实习”。它实际上不是(至少在相同的意义上不是字符串被拘留)。我们可以在这个简单的脚本中看到不同之处:

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

我们看到字符串实际上是“内部”的——使用相同文字符号的不同调用返回相同的对象。元组“interning”似乎特定于一行。

【讨论】:

  • 很棒,包括哨兵,这是我在 Python 中看到的最常见(和实际)的非无身份使用。它有用的原因是在您可能将None 作为输入的情况下,因此虽然一个是不可变的而另一个不是,但它们以这种方式相关。
  • @JaredGoguen -- 是的,另一种选择是只做next(iterable) ...并抓住StopIteration。在某些情况下,我认为这更好,但是……众所周知,我会使用一两次哨兵对象。我想这取决于周围的其余代码。
  • 很好的答案。它不应该开始“Python 有一些 objects...”吗?你的例子都是对象——不是类型。
  • 另外,您可以将内部字符串添加到重用对象的类别中。
  • @Pharap -- 可能,但那部分以前是askedanswered (几次)......这个问题非常不同,我认为这不是一个骗局,但我不想专注于已经回答的内容......
【解决方案2】:

根据实现的不同而有所不同。

CPython 在内存中缓存一些不可变对象。对于像 1 和 2 这样的“小”整数(-5 到 255,如下面的 cmets 中所述)也是如此。 CPython 这样做是出于性能原因;大多数程序中通常使用小整数,因此只创建一个副本可以节省内存(并且因为整数是不可变的,所以很安全)。

对于像None 这样的“单例”对象也是如此;在任何给定时间都只有一个None

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

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

通常您想使用== 而不是is。 Python 的is 运算符不经常使用,除非检查变量是否为None

【讨论】:

  • 从 -5 到 255 的整数是单例并缓存在内存中。
  • 另外,比较身份并没有什么意义,例如id(a) == id(b),用于不可变类型(Noneobject() 除外)。相等比较会告诉您您需要知道的所有信息,并且由于对象是不可变的,因此不会影响另一个对象的更改。
  • 这是一个很好的答案,但是感觉就像您在同一平面上对待 None 的单例和 1 的单例一样。这些是完全不同的事情。 保证只有一个None,并且isNoneNone 的比较将始终为True。这里还有其他对象(NotImplementedEllipsis 立即来找我)。但是,正如您所说,1 是单例这一事实是一个实现细节。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-23
  • 2017-11-15
  • 1970-01-01
  • 2014-02-03
  • 1970-01-01
相关资源
最近更新 更多