Python 有一些类型,它保证只有一个实例。这些实例的示例是None、NotImplemented 和Ellipsis。这些是(根据定义)单例,所以像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”似乎特定于一行。