为啥 id() == id() 和 id([]) == id([]) 在 Python 中?

Posted

技术标签:

【中文标题】为啥 id() == id() 和 id([]) == id([]) 在 Python 中?【英文标题】:Why does id() == id() and id([]) == id([]) in CPython?为什么 id() == id() 和 id([]) == id([]) 在 Python 中? 【发布时间】:2011-04-22 02:11:21 【问题描述】:

为什么 CPython(不知道其他 Python 实现)有以下行为?

tuple1 = ()
tuple2 = ()                                                                                                   
dict1 = 
dict2 = 
list1 = []
list2 = []
# makes sense, tuples are immutable
assert(id(tuple1) == id(tuple2))
# also makes sense dicts are mutable
assert(id(dict1) != id(dict2))
# lists are mutable too
assert(id(list1) != id(list2))
assert(id(()) == id(()))
# why no assertion error on this?
assert(id() == id())
# or this?
assert(id([]) == id([]))

我有一些想法可能,但找不到具体的原因。

编辑

进一步证明格伦和托马斯的观点:

[1] id([])
4330909912
[2] x = []
[3] id(x)
4330909912
[4] id([])
4334243440

【问题讨论】:

哇,这很奇怪。看起来如果你得到一个新的 dict/list 的 id,然后让它的引用计数下降到零,然后得到另一个新的 dict/list,它将具有相同的 id。当其引用计数降至零时,它看起来像一个未修改的字典/列表被保存以备后用。我的猜测是,这是针对创建并立即丢弃字典/列表的代码的情况的优化。这很常见,例如:使用setdefault 的代码经常这样做。 @Potatoswatter:绝对不是。对象的ID在创建后永远不会改变,而list和dicts是可变对象,所以空lists和dicts不能像字符串和小数字那样被记忆。 @Glenn Maynard:从技术上讲,您可以通过在丢弃之前先清空它们来记忆空列表和字典;但是,与创建新处理器相比,这可能只是浪费处理器周期。 @Lie Ryan:我真的不知道你在说什么,但你不能记住空列表,因为对象的 id 必须在其生命周期内保持不变。 @Glenn Maynard:是的,你可以。从技术上讲,您可以拥有一个空列表和空字典池;每次你需要一个新的字典时,都会检查这个池,每次你处理一个列表时(即当引用计数为零时)你把列表/字典放回池中。不要求两个不同时间的两个对象不具有相同的 id()。但是,这里没有太多的节省。 我刚刚意识到,这不是记忆,而是缓存 【参考方案1】:

一旦对象超出范围,CPython 就会对对象进行垃圾收集,因此第二个[] 是在收集第一个[] 之后创建的。因此,大多数情况下它最终都位于相同的内存位置。

这非常清楚地显示了正在发生的事情(在 Python 的其他实现中输出可能会有所不同):

class A:
    def __init__(self): print("a")
    def __del__(self): print("b")

# a a b b False
print(A() is A())
# a b a b True
print(id(A()) == id(A()))

【讨论】:

那么为什么print id(); a = []; print id() 在cpython 中会打印两次相同的值呢?存储在a 中的列表不应该占用第一个字典释放的位置吗? @Laurence:不,不一定。分配器很复杂并且经过高度优化;他们不会简单地获取可用的第一个地址。在这种情况下,dict 对象和 list 对象具有非常不同的大小,这可能会将它们放入不同的分配桶中。 在 Python 3.x 中,分配器的工作方式(在高级别)是a documented part of the the C API,而不是埋在源代码中的 cmets 中,尽管我认为特定的自定义列表和 dict/set 分配器并且 freelists 仅记录在源代码中(Objects/listobject.cdictobject.c)。 另一种查看方式是这个 sn-p:x = []; i = id(x); del x; gc.collect(); i == id([])。如果您放弃gc.collect() 调用,这将(可能)返回False,否则(可能)返回True @filmor 这不是 CPython 垃圾收集的工作原理【参考方案2】:

当您调用id() 时,Python 会创建一个字典并将其传递给id 函数。 id 函数获取它的 id(它的内存位置),并丢弃 dict。字典被破坏了。当您快速连续执行两次(同时没有创建任何其他字典)时,Python 第二次创建的字典恰好使用与第一次相同的内存块。 (CPython 的内存分配器比听起来更有可能。)因为(在 CPython 中)id 使用内存位置作为对象 id,所以两个对象的 id 是相同的。如果您将 dict 分配给一个变量然后获取它的 id(),这显然不会发生,因为 dicts同时是活动的,所以它们的 id 必须不同。

可变性不会直接发挥作用,但缓存元组和字符串的代码对象会发挥作用。在相同的代码对象(函数或类主体或模块主体)中,相同的文字(整数、字符串和某些元组)将被重用。可变对象永远不能重复使用,它们总是在运行时创建。

简而言之,对象的 id 仅在对象的生命周期内是唯一的。在对象被销毁之后,或者在它被创建之前,其他东西可以具有相同的 id。

【讨论】:

【参考方案3】:

列表和字典上的 == 运算符不会比较对象 ID 以查看它们是否是同一个对象 - 为此使用 obj1 is obj2

取而代之的是 == 操作符比较 dict 列表的成员以查看它们是否相同。

【讨论】:

他不是在比较[] == [],而是在比较id([]) == id([]) 注意他不是在比较列表和字典,而是他们的id() OP 并没有尝试这样做。 实际上,大多数 == 的实现都会先使用 is 运算符进行检查,然后再进行成员检查。原因是具有相同 id 的两个对象必须具有相同的内容。但是你是对的,id() 比较应该使用is 运算符而不是id(a) == id(b) 来完成;并且获取对象的 id() 通常是没有意义的。 @Lie Ryan 有趣的是,这在这种情况下不起作用:[] is []False。我想将第一个 [] 传递给 id 会为其创建一个退出范围。

以上是关于为啥 id() == id() 和 id([]) == id([]) 在 Python 中?的主要内容,如果未能解决你的问题,请参考以下文章

配置文件和应用程序 ID 不匹配,为啥?

为啥 id_rsa 和 id_rsa.pub 文件的内容是字母?

为啥employee_id和department_id不会在数据库Hibernate jpa中自动更新

虽然在 mobilefirst 控制台中为设备列出了设备 ID 和设备型号,但没有为同一设备生成用户 ID。为啥?

为啥以下查询不会生成最受欢迎公寓的 ID 和地址?

为啥我无法在 PHP 和 MySQL 中获取最后一个插入 ID?