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

fon*_*ini 36 python oop reference object python-3.x

似乎2 is 2并且3 is 3在python中总是如此,并且通常,对整数的任何引用都与对同一整数的任何其他引用相同.同样的情况,以None(即None is None).我知道,这并没有发生在用户定义的类型或可变类型.但它有时也会在不可变类型上失败:

>>> () is ()
True
>>> (2,) is (2,)
False
Run Code Online (Sandbox Code Playgroud)

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

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

mgi*_*son 38

Python有一些类型,它保证只有一个实例.这些实例的例子是None,NotImplementedEllipsis.这些(根据定义)单例,因此None is None保证返回,True因为无法创建新的实例NoneType.

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

以上的东西都是由python语言保证的.但是,正如您所注意到的,有一些类型(所有不可变的)存储一些实例以供重用.这是语言所允许的,但不同的实现可能会选择使用此容差 - 取决于其优化策略.属于此类别的一些示例是小整数(-5 - > 255),空tuple和空frozenset.

最后,Cpython intern在解析期间的某些不可变对象......

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

def foo():
    return (2,)

if __name__ == '__main__':
    print foo() is foo()
Run Code Online (Sandbox Code Playgroud)

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


1我可能刚刚编写了这个词......但希望你能得到这个想法...
2在正常情况下,你不需要检查对象是否是一个引用True- 通常你只关心对象是否是"truthy" - 例如,如果if some_instance: ...将执行分支.但是,为了完整起见,我把它放在这里.


请注意,is可用于比较非单身人士的事物.一个常见的用途是创建一个标记值:

sentinel = object()
item = next(iterable, sentinel)
if items is sentinel:
   # iterable exhausted.
Run Code Online (Sandbox Code Playgroud)

要么:

_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.
Run Code Online (Sandbox Code Playgroud)

这个故事的寓意是永远说出你的意思. 如果要检查值是否为其他值,请使用is运算符.如果要检查值是否等于另一个值(但可能不同),请使用==.有关is和之间==(以及何时使用)之间差异的更多详细信息,请参阅以下帖子之一:


附录

我们已经讨论了这些CPython实现细节,我们声称它们是优化的.尝试衡量我们从所有这些优化中得到的结果(除了在与is运营商合作时稍微增加一些混淆),这将是很好的.

字符串"interning"和字典查找.

这是一个小脚本,如果您使用相同的字符串来查找值而不是其他字符串,则可以运行该脚本以查看字典查找的速度.注意,我在变量名中使用术语"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))
Run Code Online (Sandbox Code Playgroud)

这里的确切值不应该太大,但在我的计算机上,短字符串显示7个部分中的1个部分更快.在串几乎快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)
Run Code Online (Sandbox Code Playgroud)

这个时间有点棘手(我很乐意采取任何更好的想法如何在评论中计时).这样做的要点是,平均而言(在我的计算机上),一个元组创建列表的时间大约为60%.但是,foo_tuple()平均花费大约40%的时间foo_list().这表明我们确实从这些实习生那里获得了一点加速.随着元组变大,节省的时间似乎会增加(创建更长的列表需要更长的时间 - 元组"创建"需要花费不变的时间,因为它已经创建了).

还要注意我称之为"实习".实际上并非如此(至少在相同的意义上,字符串是固定的).我们可以看到这个简单脚本的不同之处:

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
Run Code Online (Sandbox Code Playgroud)

我们看到字符串实际上是"interned" - 使用相同文字表示法的不同调用返回相同的对象.元组"实习"似乎特定于单行.

  • 伟大的包括哨兵,这是我在Python中看到的最常见(和实际)非 - 使用身份.它有用的原因是你可能有'None`作为输入,所以虽然一个是不可变的而另一个不是,但它们是以这种方式相关的. (3认同)
  • @Pharap - 可能,但那部分已被[问](http://stackoverflow.com/q/132988/748858)和[已回答](http://stackoverflow.com/questions/14247373/python-none-比较 - 应该使用 - 是 - 或/ 14247383#14247383)之前(几次)...这个问题是不同的,我认为它不是一个骗局,但我不想专注于已经回答了...... (2认同)

mip*_*adi 21

它根据实施而有所不同.

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

"单身"对象也是如此None; None在任何时候都只有一个存在.

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

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

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

  • 这是一个很好的答案,但感觉就像你在同一个平面上处理"无"的单身 - 单身"单身".那些是完全不同的东西._guaranteed_只有一个`None`和'is`比较`None`和`None`将永远是'True`.这里还有其他对象(`NotImplemented`和`Ellipsis`立即开始使用).但是,正如您所说,"1"是单例的事实是一个实现细节. (6认同)
  • 从-5到255的整数是单例并缓存在内存中. (2认同)
  • 另外,对于不可变类型(除了`None`和`object()`之外),比较标识(例如`id(a)== id(b)`)并没有多大意义.相等比较告诉你需要知道的一切,因为对象是不可变的,所以不会改变一个会影响另一个. (2认同)