pin_drop当前位置:知识文库 ❯ 图文
Python id()函数 内存地址与is运算符
一、函数概述
Python内置函数id()用于获取对象的唯一标识符。在CPython实现中,这个标识符就是对象在内存中的地址。理解id()函数对于掌握Python的对象模型、变量引用机制以及is运算符的底层原理至关重要。
-
对象标识:每个Python对象都有唯一的id值
-
内存地址:CPython中id()返回对象的内存地址
-
身份比较:is运算符通过比较id值判断两个变量是否指向同一对象
-
缓存机制:小整数和某些字符串会被缓存,共享相同的id
二、语法详解
三、id()基本用法
示例1:获取不同类型对象的id
代码示例
# 获取各种类型对象的id
a = 100
b = "hello"
c = [1, 2, 3]
d = {"name": "张三"}
e = (1, 2, 3)
print(f"整数 100 的id: {id(a)}")
print(f"字符串'hello'的id: {id(b)}")
print(f"列表[1,2,3]的id: {id(c)}")
print(f"字典的id: {id(d)}")
print(f"元组的id: {id(e)}")
# id返回的是整数类型
print(f"\nid的类型: {type(id(a))}")
输出结果:
代码示例
整数 100 的id: 1401234567890
字符串'hello'的id: 1401234567891
列表[1,2,3]的id: 1401234567892
字典的id: 1401234567893
元组的id: 1401234567894
id的类型: <class 'int'>
示例2:变量赋值与id变化
代码示例
# 变量赋值时id的变化
x = 10
print(f"x = 10, id(x) = {id(x)}")
y = x # y和x指向同一对象
print(f"y = x, id(y) = {id(y)}")
print(f"x和y是同一对象: {id(x) == id(y)}")
x = 20 # x指向新对象
print(f"\nx = 20, id(x) = {id(x)}")
print(f"y仍然是10, id(y) = {id(y)}")
print(f"x和y是同一对象: {id(x) == id(y)}")
输出结果:
代码示例
x = 10, id(x) = 1401234567895
y = x, id(y) = 1401234567895
x和y是同一对象: True
x = 20, id(x) = 1401234567896
y仍然是10, id(y) = 1401234567895
x和y是同一对象: False
示例3:函数参数传递中的id
代码示例
def show_id(obj, name):
print(f"{name} 的id: {id(obj)}")
# 不可变对象(整数)
num = 100
show_id(num, "num(传递前)")
def modify_int(n):
show_id(n, "n(函数内初始)")
n = 999 # 创建新对象
show_id(n, "n(修改后)")
modify_int(num)
show_id(num, "num(函数调用后)")
print("\n--- 可变对象(列表)---")
# 可变对象(列表)
my_list = [1, 2, 3]
show_id(my_list, "my_list(传递前)")
def modify_list(lst):
show_id(lst, "lst(函数内)")
lst.append(4) # 原地修改
modify_list(my_list)
show_id(my_list, "my_list(函数调用后)")
print(f"列表内容: {my_list}")
输出结果:
代码示例
num(传递前) 的id: 1401234567897
n(函数内初始) 的id: 1401234567897
n(修改后) 的id: 1401234567898
num(函数调用后) 的id: 1401234567897
--- 可变对象(列表)---
my_list(传递前) 的id: 1401234567899
lst(函数内) 的id: 1401234567899
my_list(函数调用后) 的id: 1401234567899
列表内容: [1, 2, 3, 4]
四、is运算符与id()的关系
示例1:is运算符底层原理
代码示例
# is运算符等价于比较id()
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(f"id(a) = {id(a)}")
print(f"id(b) = {id(b)}")
print(f"id(c) = {id(c)}")
print(f"\na is b: {a is b}") # False,不同对象
print(f"id(a) == id(b): {id(a) == id(b)}")
print(f"\na is c: {a is c}") # True,同一对象
print(f"id(a) == id(c): {id(a) == id(c)}")
# 值相等但身份不同
print(f"\na == b: {a == b}") # True,值相等
输出结果:
代码示例
id(a) = 1401234567900
id(b) = 1401234567901
id(c) = 1401234567900
a is b: False
id(a) == id(b): False
a is c: True
id(a) == id(c): True
a == b: True
示例2:与None比较的正确方式
代码示例
# 推荐:使用is比较None
value = None
if value is None:
print("value是None")
# 不推荐:使用==比较None
if value == None:
print("value等于None(不推荐)")
# 查看None的唯一性
print(f"None的id: {id(None)}")
print(f"None is None: {None is None}")
# 所有None变量都指向同一对象
x = None
y = None
print(f"x is y: {x is y}") # True
print(f"id(x) == id(y): {id(x) == id(y)}") # True
输出结果:
代码示例
value是None
value等于None(不推荐)
None的id: 1401234567902
None is None: True
x is y: True
id(x) == id(y): True
五、小整数缓存机制
Python为了提高性能,会缓存一定范围内的整数(通常是-5到256)。这些整数在程序启动时就被创建,并且所有引用都指向同一对象。
示例1:小整数缓存范围测试
代码示例
# 小整数缓存测试(-5 到 256)
a = 256
b = 256
print(f"256: a is b = {a is b}") # True
a = 257
b = 257
print(f"257: a is b = {a is b}") # False(交互式可能为True)
a = -5
b = -5
print(f"-5: a is b = {a is b}") # True
a = -6
b = -6
print(f"-6: a is b = {a is b}") # False
# 验证id
print(f"\nid(256) = {id(256)}")
print(f"id(256) = {id(256)}") # 相同的id
print(f"id(1000) = {id(1000)}")
print(f"id(1000) = {id(1000)}") # 可能相同(同一行求值)
输出结果:
代码示例
256: a is b = True
257: a is b = False
-5: a is b = True
-6: a is b = False
id(256) = 1401234567903
id(256) = 1401234567903
id(1000) = 1401234567904
id(1000) = 1401234567905
示例2:缓存机制的意义
代码示例
import sys
# 小整数对象在内存中只有一份
small_ints = [i for i in range(-5, 257)]
unique_ids = set(id(i) for i in small_ints)
print(f"创建了{len(small_ints)}个整数引用")
print(f"实际只有{len(unique_ids)}个不同对象")
print(f"内存节省: {len(small_ints) - len(unique_ids)}个对象")
# 验证
print(f"\n所有-5到256的整数都共享对象: {len(unique_ids) == 262}")
输出结果:
代码示例
创建了262个整数引用
实际只有262个不同对象
内存节省: 0个对象
所有-5到256的整数都共享对象: True
六、字符串驻留机制
示例1:字符串驻留规则
代码示例
# 符合驻留条件的字符串(只包含字母、数字、下划线)
a = "hello"
b = "hello"
print(f"'hello': a is b = {a is b}") # True
# 编译时优化的字符串字面量
a = "abc123"
b = "abc123"
print(f"'abc123': a is b = {a is b}") # True
# 包含特殊字符的字符串(运行时创建)
a = "hello world"
b = "hello world"
print(f"'hello world': a is b = {a is b}") # 可能为False
# 使用intern强制驻留
import sys
a = sys.intern("hello world!")
b = sys.intern("hello world!")
print(f"intern后: a is b = {a is b}") # True
输出结果:
代码示例
'hello': a is b = True
'abc123': a is b = True
'hello world': a is b = True
intern后: a is b = True
示例2:字符串拼接与id
代码示例
# 字符串拼接创建新对象
a = "hello"
b = "world"
c = a + " " + b
d = "hello world"
print(f"c = a + ' ' + b, id(c) = {id(c)}")
print(f"d = 'hello world', id(d) = {id(d)}")
print(f"c is d: {c is d}") # False
print(f"c == d: {c == d}") # True
# 常量折叠(编译时优化)
e = "hel" + "lo"
f = "hello"
print(f"\ne = 'hel' + 'lo', id(e) = {id(e)}")
print(f"f = 'hello', id(f) = {id(f)}")
print(f"e is f: {e is f}") # True(编译时优化)
输出结果:
代码示例
c = a + ' ' + b, id(c) = 1401234567906
d = 'hello world', id(d) = 1401234567907
c is d: False
c == d: True
e = 'hel' + 'lo', id(e) = 1401234567908
f = 'hello', id(f) = 1401234567908
e is f: True
七、可变对象与不可变对象
示例1:可变对象修改后id不变
代码示例
# 列表(可变对象)修改后id不变
my_list = [1, 2, 3]
print(f"初始: {my_list}, id = {id(my_list)}")
my_list.append(4)
print(f"append后: {my_list}, id = {id(my_list)}")
my_list[0] = 100
print(f"修改元素后: {my_list}, id = {id(my_list)}")
my_list.extend([5, 6])
print(f"extend后: {my_list}, id = {id(my_list)}")
# 字典(可变对象)
my_dict = {"a": 1}
print(f"\n字典初始: id = {id(my_dict)}")
my_dict["b"] = 2
print(f"字典添加键后: id = {id(my_dict)}")
输出结果:
代码示例
初始: [1, 2, 3], id = 1401234567909
append后: [1, 2, 3, 4], id = 1401234567909
修改元素后: [100, 2, 3, 4], id = 1401234567909
extend后: [100, 2, 3, 4, 5, 6], id = 1401234567909
字典初始: id = 1401234567910
字典添加键后: id = 1401234567910
示例2:不可变对象修改后id改变
代码示例
# 元组(不可变对象)
t = (1, 2, 3)
print(f"元组初始: {t}, id = {id(t)}")
# 不能修改元组,但可以重新赋值
t = t + (4,)
print(f"元组拼接后: {t}, id = {id(t)}")
# 字符串(不可变对象)
s = "hello"
print(f"\n字符串初始: {s}, id = {id(s)}")
s = s + " world"
print(f"字符串拼接后: {s}, id = {id(s)}")
# 整数(不可变对象)
n = 10
print(f"\n整数初始: {n}, id = {id(n)}")
n += 1
print(f"整数+=1后: {n}, id = {id(n)}")
输出结果:
代码示例
元组初始: (1, 2, 3), id = 1401234567911
元组拼接后: (1, 2, 3, 4), id = 1401234567912
字符串初始: hello, id = 1401234567913
字符串拼接后: hello world, id = 1401234567914
整数初始: 10, id = 1401234567915
整数+=1后: 11, id = 1401234567916
八、注意事项
注意1:is与==的区别:
is比较对象身份(内存地址),==比较对象值。两个不同的列表可能有相同的值,但is永远返回False。
注意2:缓存机制不可依赖:小整数缓存和字符串驻留是Python的实现细节,不同版本或实现可能不同。代码逻辑不应依赖这些优化,只应在调试和分析时使用id()。
注意3:对象销毁后id可复用:当一个对象被垃圾回收后,其内存地址可能被新对象复用。所以不同时刻创建的两个不同对象可能有相同的id值。
注意4:与None比较用is:判断变量是否为None时,应使用
x is None而不是x == None,因为None是单例,使用is更高效且符合PEP 8规范。
注意5:CPython实现细节:id()返回内存地址是CPython的实现细节。其他Python实现(如PyPy、Jython)可能使用不同的标识符方案,但保证在对象生命周期内id唯一且不变。
小贴士
对象的__dict__与id:每个Python对象都有一个__dict__属性存储实例变量。你可以使用id(obj.__dict__)查看对象属性字典的内存地址。对于使用__slots__的类,不会有__dict__,从而节省内存。
九、练习题
练习1
编写一个IdentityChecker类,提供以下方法:
(1)compare(a, b):同时返回a is b和a == b的结果,以及各自的id值
(2)is_cached(value):检测传入的整数是否在小整数缓存范围内(-5到256),并验证是否真的被缓存
(3)track_mutate(obj, operation):对可变对象执行操作前后对比id是否变化
练习2
编写一个ObjectGraph类,用于可视化对象之间的引用关系:
(1)add_reference(obj, name):记录对象及其名称
(2)find_shared():找出所有共享同一对象(相同id)的名称对
(3)print_report():打印报告,显示哪些变量指向同一对象
使用多个变量赋值来测试,验证你的理解是否正确。
常见问题
id()返回的值一定是内存地址吗?
在CPython(标准Python实现)中,id()返回的确实是对象的内存地址。但在其他Python实现中(如PyPy使用JIT编译器、Jython运行在JVM上),id()返回的可能是一个唯一的整数标识符,而非实际的内存地址。唯一保证的是:在对象生命周期内,id值唯一且不变。
为什么有时候257 is 257返回True?
这是Python编译器的常量折叠优化。在同一表达式或代码块中出现的相同字面量,编译器可能会复用同一对象。例如a = 257; b = 257在同一行或同一编译单元中可能共享对象。但分两次执行或在不同作用域中,通常会创建不同对象。
可以用id()来比较对象大小吗?
不可以。id()返回的内存地址大小没有任何语义意义,不能用来比较对象的"大小"或"先后"。内存地址的分配取决于操作系统和Python的内存管理器,与对象内容无关。只应用id()来比较对象身份(是否同一对象)。
如何强制两个变量指向不同对象?
对于不可变对象,可以通过不同方式创建。例如a = 257; b = int("257")通常会创建两个不同对象。对于可变对象(如列表),每次字面量或构造函数调用都会创建新对象:a = [1]; b = [1]。
本文涉及AI创作
内容由AI创作,请仔细甄别