Why does id({}) == id({}) and id([]) == id([]) in CPython?
为什么CPython(不了解其他Python实现)具有以下行为?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 2 3 4 5 6 7
| [1] id([])
4330909912
[2] x = []
[3] id(x)
4330909912
[4] id([])
4334243440 |
-
哇真奇怪看起来如果您获得新的dict /列表的ID,然后让其引用计数降至零,然后获得另一个具有相同ID的新的dict /列表,则好像。看起来当未修改的字典/列表的引用计数降至零时,它会保存下来供以后使用。我的猜测是,这是针对代码创建并立即丢弃字典/列表的情况的优化。这很常见,例如:使用setdefault的代码经常执行此操作。
-
@Potatoswatter:绝对不是。对象的ID在创建后就永远不会更改,并且列表和字典是可变对象,因此无法以字符串和小数字的方式来记住空的列表和字典。
-
@Glenn Maynard:从技术上讲,您可以通过在清空之前先清空它们来记住空列表和命令。但是,与创建新处理器相比,这可能只是浪费处理器周期。
-
@Lie Ryan:我真的不知道您在说什么,但是您无法记住空白??列表,因为对象的ID在其生命周期内必须保持不变。
-
@格伦·梅纳德:是的,可以。从技术上讲,您可以有一个空列表和空字典的池。每次您需要新的字典时,都会检查该池,并且每次处置列表时(即refcount为零时),都会将列表/字典放回池中。不需要两个不同时间的两个对象不具有相同的id()。但是,这里没有太多的节省。我只是意识到,这不是记忆,而是缓存
调用id({})时,Python创建一个字典并将其传递给id函数。 id函数获取其ID(其内存位置),并丢弃dict。该词典被销毁了。当您快速连续执行两次(在此期间未创建任何其他字典)时,字典Python将第二次创建使用与第一次相同的内存块。 (CPython的内存分配器使听起来的可能性更大。)由于(在CPython中)id使用内存位置作为对象ID,所以两个对象的ID相同。如果将字典分配给变量然后获得其id(),显然不会发生这种情况,因为这些字典同时处于活动状态,因此它们的id必须不同。
可变性并不直接起作用,但是缓存元组和字符串的代码对象却可以。在同一代码对象(函数或类主体或模块主体)中,将重复使用相同的文字(整数,字符串和某些元组)。可变对象永远不能重复使用,它们总是在运行时创建的。
简而言之,对象的ID仅在对象的生存期内是唯一的。在销毁对象之后或在创建对象之前,其他对象可以具有相同的ID。
-
有没有一种简短的说法"不管优胜劣汰,投票给最上层的装饰者而忽略其余的人"?
-
有点格伦,你把它标记为答案:)
-
@spenthil:标记顶部答案正确而忽略了下面的答案吗?的确确实如此。 :P
-
抱歉,在标记之前正在进行实验(请参阅OP)。
-
尽管您是正确的,但我一直在寻找可验证的推理方法(请参阅@Glenn Maynard)
-
在不对答案的相对优点发表任何评论的情况下,我认为应该扩大投票范围,例如值some_constant * log(upvoter_rep) log(answerer_rep)
-
@Glenn:我不确定为什么我的答案没有你的优点。当然,它的时间更长,并且不包含您在您的实验中所做的实验,但这是因为我实际上是从CPython来源了解这些东西的。无需进行实验。
-
我的答案不包含任何实验,它包含一个特定示例,旨在精确演示正在发生的事情。 我认为我的答案比较清楚,因为它用两个简洁的句子而不是几个段落来解释整个问题。 但是,要明确一点:我认为这根本不是一个错误的答案。 我觉得有趣的是,答案是+12是愚蠢的,而我的是+1。
CPython会在对象超出范围时立即对其进行垃圾回收,因此第二个[]是在收集第一个[]之后创建的。因此,大多数情况下,它最终都位于同一存储位置。
这可以非常清楚地显示正在发生的事情(在其他Python实现中,输出可能会有所不同):
1 2 3 4 5 6 7 8
| class A(object):
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()) |
-
尽管Thomas的回答同样正确,但您仍提供了我一直在寻找的具体推理。
-
那么,为什么print id({}); a = []; print id({})在cpython中打印两次相同的值?存储在a中的列表不应该占用第一个字典释放的位置吗?
-
@劳伦斯:不,不一定。分配器很复杂且经过了优化。他们不会简单地选择可用的第一个地址。在这种情况下,dict对象和list对象的大小非常不同,这可能会将它们放入不同的分配存储桶中。
-
在Python 3.x中,分配器的工作方式(高级)是C API的文档化部分,而不是源代码中的注释,尽管我认为特定的自定义列表以及dict / set分配器和自由列表是仅记录在源文件中(Objectslistobject.c和dictobject.c)。
-
另一种查看此内容的方法是以下代码段:x = []; i = id(x); del x; gc.collect(); i == id([])。如果删除gc.collect()调用,则将(可能)返回False,否则(可能)返回True。
-
@filmor那不是CPython垃圾回收的工作方式
它在Jython中的工作方式不同...
1 2 3 4
| >>> id({})
1
>>> id([])
2 |
是否可以对"内装"常用(即空)容器进行优化以节省分配成本?
(在CPython中)这建议不:
1 2 3 4 5 6 7 8 9 10 11
| >>> def mutateid(obj):
... obj.append('x')
... print obj
... print id(obj)
...
>>> mutateid([])
['x']
4299590472
>>> id([])
4299590472
>>> |
-
那实际上有两个原因:首先,Jython使用Javas GC,这意味着在最后一个引用消失后(如在CPython中)就收集了对象。其次,由于Java对象不在固定的内存位置(例如CPython中),因此Jython不能将对象的内存地址用作其ID。它必须使用其他内容,同时保留id()的语义。我记得,Jython使用的计数器仅在您对对象调用id()时才开始计数。
-
该代码并没有真正测试OP正在询问的相同现象。 Jython中id({}) == id({})返回什么?
-
>>> id({})== id({})错误>>>
列表和字典上的==运算符不比较对象ID以查看它们是否是同一对象-为此使用obj1 is obj2。
相反,==运算符将比较字典列表中的成员,以查看它们是否相同。
-
他不比较[] == [],他比较id([]) == id([])。
-
请注意,他不比较列表和字典,而是比较它们的id()。
-
OP不会尝试这样做。
-
实际上,==的大多数实现在执行逐成员检查之前先使用is运算符进行检查。原因是两个具有相同ID的对象必须具有相同的内容。但是您认为应该使用is运算符而不是id(a) == id(b)进行id()比较;并且获取对象的id()通常是毫无意义的。
-
@Lie Ryan有趣的是,在这种情况下不起作用:[] is []是False。我猜想将第一个[]传递给id会为其创建一个范围。
-
@AaronMcSmooth:[] is []返回False表示两个列表确实是两个不同的对象,这是预期的行为。发明is运算符的目的是要抓住比较对象的引用,因此它们在比较之前不会超出范围(用==运算符和id()的语义无法保证)。