关于oop:在Python中,两个对象何时相同?

In Python, when are two objects the same?

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

1
2
3
4
>>> () is ()
True
>>> (2,) is (2,)
False

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

如何确定一个对象是在内存中被复制,还是将有一个具有大量引用的实例?它是否取决于物体在某种意义上是否是"原子的"?它是否因实施而有所不同?


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

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

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

最后,cpython intern在解析过程中的某些不变对象…好的。

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

1
2
3
4
5
def foo():
    return (2,)

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

这似乎很奇怪。cpython的诀窍是,每当构造函数foo时,它都会看到一个包含其他简单(不变)文本的元组文本。与一次又一次地创建这个元组(或者它的等价物)不同,Python只创建了一次。因为整个交易是不可变的,所以不存在对象被更改的危险。这对于性能来说是一个巨大的胜利,在这种情况下,相同的紧环会被反复调用。小弦也被收起来了。真正的胜利在于查字典。当检查哈希冲突时,python可以进行(极快的)指针比较,然后返回较慢的字符串比较。由于python的很多内容都是建立在字典查找上的,所以这对于整个语言来说是一个很大的优化。好的。

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

注意,is可用于比较非单件物品。一个常见的用途是创建一个sentinel值:好的。

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

或:好的。

1
2
3
4
_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==之间的区别(以及何时使用该区别)的更多详细信息,请参阅以下文章之一:好的。

  • python中的`=`和'is'有区别吗?
  • python none比较:我应该使用"is"还是==?

补遗

我们已经讨论了这些cpython实现细节,并且声称它们是优化的。最好是尝试测量我们从所有这些优化中得到什么(除了在使用is操作符时增加一些混乱)。好的。字符串"interning"和字典查找。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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分之一。长字符串几乎快了2倍(因为如果字符串有更多的字符要比较,则字符串比较需要更长的时间)。在python3.x上的差异并不是很明显,但它们仍然存在。好的。tuple"实习"

下面是一个小脚本,您可以在其中玩:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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%。这表明我们确实从这些实习生身上得到了一点提速。随着元组变大,节省的时间似乎会增加(创建一个更长的列表需要更长的时间——元组"创建"自创建以来需要恒定的时间)。好的。

还要注意,我把这个叫做"实习"。它实际上不是(至少不是在同样的意义上,弦被囚禁)。我们可以在这个简单的脚本中看到不同之处:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

我们看到字符串实际上是"内部的"——使用相同的文字符号的不同调用返回相同的对象。tuple"interning"似乎只针对一行。好的。好啊。


根据实施情况不同。

cpython在内存中缓存一些不可变的对象。这对于1和2这样的"小"整数是正确的(-5到255,如下面的注释所述)。cpython这样做是出于性能方面的考虑;小整数在大多数程序中都是常用的,因此它将内存保存为只创建一个副本(并且是安全的,因为整数是不可变的)。

这也适用于"单体"对象,如None;在任何给定时间,只有一个None存在。

其他对象(如空元组、())可以作为单例实现,也可以不实现。

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

通常你想用==而不是is。python的is操作符并不常用,除非检查变量是否为None时使用。