Python内置的id()函数用于返回对象的唯一标识符,在CPython解释器中这个标识符就是对象在内存中的地址。理解它对于掌握变量引用、对象生命周期以及同一性判断非常关键。很多初学者容易把值相等和对象相同混为一谈,而id()恰恰能提供最底层的视角。

id()函数的基本用法与返回值含义
id()的语法极其简单,它接受一个对象作为参数,返回该对象当前的唯一整数标识。在CPython实现中,这个整数就是对象在堆内存中的地址,因此你经常会看到不同对象的id值相差很大,而同一个对象在存活期间的id始终不变。需要注意的是,对象销毁后其id可能被后续新对象复用,所以不能把id当作跨生命周期的永久编号。
下面这段代码展示了如何用id()观察普通对象:
a = [1, 2, 3] b = a c = [1, 2, 3] print(id(a)) # 输出a的标识 print(id(b)) # 与a相同,因为b引用同一对象 print(id(c)) # 与a不同,c是新列表对象 print(a is b) # True print(a is c) # False
从输出可以看出,b = a只是把引用赋给b,两者指向同一个列表,因此id(a)与id(b)完全一致;而c通过字面量新建列表,即便内容相同,也是另一个对象,id不同。这种机制解释了为什么修改a会影响b却不影响c。
除了复合对象,不可变对象同样适用。比如整数和字符串,不过它们存在解释器层面的优化,我们后面会专门说明。总之,id()让你跳过值的比较,直接看到对象身份这一层。
id()与is运算符及等号比较的区别
不少开发者在判断两个变量关系时,会在==和is之间犹豫。其实==比较的是值是否相等,由对象的__eq__方法决定;is比较的是对象同一性,本质上就是比较两者的id()是否相等;而id()本身只是把这种身份以整数形式暴露出来。因此a is b完全等价于id(a) == id(b)。
看一个容易踩坑的例子:
x = 256 y = 256 print(x == y) # True,值相等 print(x is y) # True,小整数池内同一对象 print(id(x) == id(y)) m = 257 n = 257 print(m is n) # 可能为False,超出小整数缓存 print(id(m) == id(n)) # 通常不同
这里涉及CPython的小整数对象池,区间[-5, 256]内的整数会被预先创建并复用,所以两个256字面量其实指向同一个对象,id相同;而257不在池内,每次出现都可能新建对象,导致id不同。用id()打印出来就能确认这一点,避免误以为is判断不可靠。
字符串也有类似的驻留机制,但并非所有字符串都驻留。短字符串或看起来像标识符的字符串往往共享,而动态拼接的长串通常独立。在需要确认两个字符串变量是否同一对象时,id()比猜测驻留规则更直观。不过生产代码中比较字符串内容仍应使用==,只有特定底层优化场景才用is配合id()验证。
利用id()排查引用问题与常见误区
在函数参数传递、容器嵌套等场景下,对象被多个变量引用是常态。当程序出现“改了A却影响了B”的诡异现象,用id()打印相关变量的标识,往往能快速定位是不是同一引用。例如在函数中修改传入的列表,外部也跟着变,这正是因为传参传的是引用,id一致。
一个典型误区的代码示例如下:
def append_val(lst, val):
lst.append(val)
base = []
group = [base, base, base]
print([id(x) for x in group]) # 三个元素id相同
append_val(group[0], 10)
print(group) # [[10], [10], [10]]
上面group列表里放了三个对同一base对象的引用,通过id()可以证明它们地址一样。修改其中一个,全部联动。如果不使用id()观察,很难意识到自己存的是引用而非拷贝。
另一个误区是认为id()返回的是对象内容的哈希。实际上默认hash()对可变对象不可用,而id()始终可用且不等于哈希值。还有人把id用于跨进程或持久化身份判断,这是错误的,因为不同进程内存布局独立,id只在单次运行内有意义。正确做法是业务层用唯一主键或uuid标识,而非依赖内存地址。
综上,id()是理解Python对象模型的显微镜。它不复杂,却能在引用纠缠、同一性判断、解释器优化验证等方面提供确凿证据。养成在可疑处打印id()的习惯,比单纯靠经验猜测要稳健得多。