id()函数有什么作用?

118

我阅读了 Python 2 文档,并注意到 id() 函数:

返回对象的“身份”。该身份是一个整数(或长整数),在对象的生命周期内保证是唯一且恒定的。在生命周期没有重叠的两个对象可能具有相同的 id() 值。

CPython 实现细节:这是对象在内存中的地址。

因此,我使用 id() 函数和列表进行实验:

>>> list = [1,2,3]
>>> id(list[0])
31186196
>>> id(list[1])
31907092 // increased by 896
>>> id(list[2])
31907080 // decreased by 12

这个函数返回的整数是什么?它是否与C语言中的内存地址同义?如果是,为什么这个整数不对应于数据类型的大小?

id()在实践中何时使用?


4
在脚本语言中,如果你将一个32位整数存储在数据结构中,并不意味着你将使用多出32位的内存。任何你存储的数据都会附带元数据,比如类型、大小、长度等等。 - Marc B
cpython 从堆中分配内存,随着对象的 malloc 和 free 操作,堆会变得混乱。 - tdelaney
Python的数字不仅仅是简单的数据。它们是对象,最初使用longs内部表示,如果值变得太大,则自动升级为BigNumber-style表示。 - Gareth Latty
13个回答

170

您的帖子提出了几个问题:

函数返回的数字是什么?

它是 "整数(或长整数),在该对象的生命周期内保证是唯一且恒定的。" (Python标准库 - 内置函数)一个独一无二的数字。没有更多和更少。将其视为 Python 对象的社会安全号码或员工 ID 号码。

它与 C 中的内存地址相同吗?

从概念上讲,是的,因为它们都保证在其生命周期内在其宇宙中是唯一的。在 Python 的一个特定实现中,它实际上是对应 C 对象的内存地址。

如果是这样,为什么数字不会立即按数据类型的大小增加(我假设它将是 int)?

因为列表不是数组,并且列表元素是引用,而不是对象。

我们什么时候真正使用 id( ) 函数?

很少。您可以通过比较它们的 ids 来测试两个引用是否相同,但始终建议使用 is运算符 进行比较。 id( ) 只在调试情况下真正有用。


2
我们通常在演示目的中使用id(),例如在这里https://dev59.com/OWQm5IYBdhLWcg3w6Car。 - Nabin
把它看作是社会安全号码,除了当人死亡后可以重新使用。 - Alex W

56

那就是对象在内存中的位置的身份标识...

这个例子可能会帮助你更好地理解这个概念。

foo = 1
bar = foo
baz = bar
fii = 1

print id(foo)
print id(bar)
print id(baz)
print id(fii)

> 1532352
> 1532352
> 1532352
> 1532352

它们都指向内存中的同一个位置,这就是它们值相同的原因。在本例中,1 只被存储了一次,任何其他指向 1 的东西都将引用该内存位置。


12
如果你使用超出-5到256范围之外的数字,那么fii变量的id将会不同。 - saurav
非常有趣。你能分享更多吗? - jouell
4
我认为这个答案具有误导性,因为大多数数字并不适用;请参阅[“is” operator behaves unexpectedly with integers](https://dev59.com/DHVC5IYBdhLWcg3wZwTj)。 - Kevin Ji

12

Rob的答案(上面得票最高)是正确的。我想补充一点,在某些情况下使用ID是有用的,因为它允许比较对象并找到哪些对象引用了您的对象。

后者通常有助于您调试奇怪的错误,其中可变对象作为参数传递给类,并被分配给类中的局部变量。如果改变这些对象,将会改变类中的变量。这表现为多个事物同时发生变化的奇怪行为。

最近我在一个Python/Tkinter应用程序中遇到了这个问题,在一个文本输入字段中编辑文本会随着我的输入而更改另一个文本输入字段中的文本 :)

以下是一个示例,说明如何使用函数id()来跟踪引用的位置。毫无疑问,这不是覆盖所有可能情况的解决方案,但您可以了解到大致思路。再次强调,ID在后台使用,用户看不到它们:

class democlass:
    classvar = 24

    def __init__(self, var):
        self.instancevar1 = var
        self.instancevar2 = 42

    def whoreferencesmylocalvars(self, fromwhere):
        return {__l__: {__g__
                    for __g__ in fromwhere
                        if not callable(__g__) and id(eval(__g__)) == id(getattr(self,__l__))
                    }
                for __l__ in dir(self)
                    if not callable(getattr(self, __l__)) and __l__[-1] != '_'
                }

    def whoreferencesthisclassinstance(self, fromwhere):
        return {__g__
                    for __g__ in fromwhere
                        if not callable(__g__) and id(eval(__g__)) == id(self)
                }

a = [1,2,3,4]
b = a
c = b
democlassinstance = democlass(a)
d = democlassinstance
e = d
f = democlassinstance.classvar
g = democlassinstance.instancevar2

print( 'My class instance is of', type(democlassinstance), 'type.')
print( 'My instance vars are referenced by:', democlassinstance.whoreferencesmylocalvars(globals()) )
print( 'My class instance is referenced by:', democlassinstance.whoreferencesthisclassinstance(globals()) )

输出:

My class instance is of <class '__main__.democlass'> type.
My instance vars are referenced by: {'instancevar2': {'g'}, 'classvar': {'f'}, 'instancevar1': {'a', 'c', 'b'}}
My class instance is referenced by: {'e', 'd', 'democlassinstance'}

在变量名中使用下划线是为了防止名称冲突。函数使用 "fromwhere" 参数,这样您就可以让它们知道从哪里开始搜索引用。该参数由列出给定命名空间中所有名称的函数填充。Globals() 是其中之一。


10

id() 函数(在 CPython 中)确实返回所引用对象的地址,但你的困惑来自于 Python 列表与 C 数组非常不同。在 Python 列表中,每个元素都是 引用。因此,你所做的更类似于以下 C 代码:

int *arr[3];
arr[0] = malloc(sizeof(int));
*arr[0] = 1;
arr[1] = malloc(sizeof(int));
*arr[1] = 2;
arr[2] = malloc(sizeof(int));
*arr[2] = 3;
printf("%p %p %p", arr[0], arr[1], arr[2]);
换句话说,您正在从引用中打印地址,而不是相对于存储列表的位置的地址。
在我的情况下,我发现id()函数非常方便,可以在从C调用python时创建不透明句柄返回给C代码。这样做,您可以轻松使用字典从其句柄查找对象,并确保其唯一性。

5
如果您使用的是Python 3.4.1,那么您会得到与您提出的问题不同的答案。
list = [1,2,3]
id(list[0])
id(list[1])
id(list[2])

返回值:

1705950792   
1705950808  # increased by 16   
1705950824  # increased by 16

整数从-5到256拥有一个固定的ID,多次查找时其ID不会改变,而其他数字在每次查找时都具有不同的ID。
-5到256的数字具有递增的ID,并且相差16。
id()函数返回的数字是存储在内存中的每个项的唯一标识符,类比于C语言中的内存位置。

5

我刚开始学习Python,当我使用交互式shell查看变量是否被分配到同一个值时,我使用id。

每个值都有一个id,它是与计算机内存中存储位置相关的唯一编号。


4
is 操作符用它来检查两个对象是否相同(而不是相等)。从 id() 返回的实际值几乎从未被用于任何事情,因为它实际上没有意义,并且它还依赖于平台。

3
答案几乎是从不。ID主要在Python内部使用。
普通的Python程序员可能永远不需要在他们的代码中使用`id()`。

3
也许我不是一般人,但我经常使用id()函数。我能想到的两种用法是:一是手写的身份标识字典,二是针对某个对象自定义的repr()函数,因为身份标识很重要,但默认的repr函数并不适用。 - user395760
5
我不会争辩那些是常见情况。 - Gareth Latty

2

这是内存中对象的地址,就像文档所说的一样。但是,它附加了元数据,需要存储对象的属性和位置信息。因此,当您创建名为“list”的变量时,还会为列表及其元素创建元数据。

因此,除非您是该语言的绝对大师,否则无法根据先前元素确定列表下一个元素的ID,因为您不知道语言在元素之间分配的内容。


1
实际上,成为Python大师并不能很好地预测任何对象的id()。你需要对相关的内存管理器非常熟悉,知道它们在某个时间点的确切状态,并且知道对象分配的确切顺序。换句话说:这是不可能发生的。 - user395760
如果你绝对了解Python的所有细节,包括可能的内存管理器,那么你就是一个绝对的Python大师。 - Lajos Arpad
1
了解Python和了解特定的实现(例如CPython)是两码事。即使深入了解CPython也没有用,因为CPython调用的有几个内存管理器不是CPython的一部分,而是各自操作系统的一部分。正如我之前所说,即使了解所有这些非Python的东西也没有用,因为实际地址取决于内存管理器的运行时状态,有时甚至是随机的。 - user395760
1
更不用说,知道这个是没有用的,因为依赖这样的东西就是依赖于实现细节,因此会变得脆弱和缺乏灵活性。 - Gareth Latty

2

我有一个想法,可以在日志中使用id()的值。
这是一种便宜且相当简短的方法。
在我的情况下,我使用tornado,id()会像一个锚点一样将通过web socket散布和混合的消息分组。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接