首页 文章

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

提问于
浏览
35

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

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

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

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

2 回答

  • 37

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

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

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

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

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

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

    这看起来很奇怪 . Cpython正在玩的技巧是,每当它构造函数 foo 时,它就会看到包含其他简单(不可变)文字的元组文字 . 而不是创建这个元组(或者它不会因为整个交易是不可变的而改变该对象的危险 . 这对于性能来说是一个很大的胜利,其中相同的紧密循环被一遍又一遍地调用 . 小字符串也被实例化 . 这里真正的胜利是在字典查找中.Python可以做一个(超快速)指针比较,然后在检查哈希冲突时回退到较慢的字符串比较 . 由于python的大部分是 Build 在字典查找上的,所以这可能是一个很大的优化语言整体 .


    1我可能刚刚写了这个词......但希望你能得到这个想法......
    2在正常情况下,你不需要检查对象是否是对True的引用 - 通常你只关心对象是否“真实” - 例如如果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.
    

    The moral of this story is to always say what you mean. 如果要检查值是否为另一个值,请使用 is 运算符 . 如果要检查值是否等于另一个值(但可能不同),请使用 == . 有关 is== (以及何时使用)之间差异的更多详细信息,请参阅以下帖子之一:


    附录

    我们声称,他们很乐意尝试衡量我们从所有这些优化中得到的结果(除了在使用 is 运算符时稍微增加一些混乱) .

    字符串“interning”和字典查找 .

    这里's a small script that you can run to see how much faster dictionary lookups are if you use the same string to look up the value instead of a different string. Note, I use the term 1112861 in the variable names -- These values aren' t必然实习(虽然它们可能是) . 我只是用它来表明"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个部分中的1个部分更快 . 长字符串几乎快2倍(因为如果字符串有更多字符要比较,字符串比较需要更长的时间) . 这些差异仍然存在 .

    元组“实习”

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

    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% . 但是, foo_tuple() 平均占 foo_list() 所需时间的40%左右 . 这表明我们确实从这些实习生那里获得了一点加速 . 随着元组变大,节省时间似乎会增加(创建更长的列表需要更长的时间 - 元组"creation"自已创建以来需要不变的时间) .

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

    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
    

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

  • 21

    它根据实施而有所不同 .

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

    对于像 None 这样的"singleton"对象也是如此;在任何特定时间都只存在一个 None .

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

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

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

相关问题