变量与对象
在 Python 中,变量不是存储数据的盒子,而是绑定到对象的标签。理解这一点,是掌握 Python 动态类型系统的关键。
变量是名字,不是盒子
许多语言(如 C)把变量看作内存中的盒子:声明 int x = 10 后,盒子里就装着整数 10。Python 完全不同:
a = 1
这行代码做了两件事:创建一个整数对象 1,然后把名字 a 绑定到这个对象上。名字和对象之间是"贴标签"的关系,而非"装东西"的关系。
a = 1
b = a # b 也绑定到同一个对象 1
a = 2 # a 改绑定到对象 2,b 仍然绑定到 1
print a # 2
print b # 1
b = a 不是"把 a 的值复制给 b",而是"让 b 也指向 a 所指的对象"。当 a = 2 时,a 撕下旧标签贴到新对象上,b 不受影响。
对象的三要素
每个 Python 对象都有三个固有属性:
身份(Identity):对象创建后永不改变,可以理解为内存地址。用 id() 查看:
a = [1, 2, 3]
print id(a) # 如 140234567890
类型(Type):决定对象能做什么操作。用 type() 查看:
print type(42) # <type 'int'>
print type("hello") # <type 'str'>
print type([1, 2]) # <type 'list'>
类型在对象创建时确定,之后不可变。你不能把一个整数对象"变成"字符串对象,只能让变量重新绑定到另一个对象。
值(Value):可变对象的值可以改变,不可变对象的值永远固定。
# 可变对象:列表
a = [1, 2, 3]
print id(a) # 140234567890
a.append(4) # 修改值,但身份不变
print id(a) # 仍然是 140234567890
# 不可变对象:整数
b = 10
print id(b) # 如 140230000000
c = b + 1 # 不是修改 10,而是创建新对象 11
print id(c) # 不同的地址
可变与不可变对象
| 类型 | 可变? | 示例 |
|---|---|---|
| int, long, float, complex | 否 | 42, 3.14 |
| bool | 否 | True, False |
| str, unicode | 否 | "hello", u"你好" |
| tuple | 否 | (1, 2, 3) |
| list | 是 | [1, 2, 3] |
| dict | 是 | {"a": 1} |
| set | 是 | {1, 2, 3} |
可变对象的"别名陷阱"是 Python 初学者最常踩的坑:
a = [1, 2, 3]
b = a # b 和 a 指向同一个列表对象
b.append(4)
print a # [1, 2, 3, 4] —— a 也被改了!
因为 a 和 b 绑定到同一个列表对象,通过 b 修改列表,通过 a 也能看到变化。如果需要独立的副本,必须显式复制:
a = [1, 2, 3]
b = a[:] # 切片复制,创建新列表
b.append(4)
print a # [1, 2, 3] —— a 不受影响
动态类型的本质
Python 是动态类型语言:变量没有类型,对象才有类型。同一个变量可以在不同时刻绑定到不同类型的对象:
x = 10
print type(x) # <type 'int'>
x = "hello"
print type(x) # <type 'str'>
x = [1, 2]
print type(x) # <type 'list'>
这与静态类型语言(如 C、Java)形成鲜明对比。在 C 中,int x 声明后,x 永远只能存储整数。在 Python 中,x 只是一个名字,它可以指向任何对象。
动态类型带来了灵活性,但也增加了运行时出错的风险。Python 2 不会在编译时检查类型,以下代码完全合法,直到运行时才报错:
def add(a, b):
return a + b
print add(1, 2) # 3
print add("1", "2") # "12"
print add(1, "2") # TypeError: unsupported operand type(s)
垃圾回收
当一个对象没有任何名字绑定时,它就变成了"垃圾"。Python 主要使用引用计数来回收垃圾:每个对象内部维护一个计数器,记录有多少个名字指向它。计数器归零时,对象立即被销毁,内存被释放。
a = [1, 2, 3] # 列表对象的引用计数 = 1
b = a # 引用计数 = 2
del a # 引用计数 = 1
del b # 引用计数 = 0,对象被销毁
引用计数简单高效,但无法处理循环引用:
a = []
b = []
a.append(b) # a 引用 b
b.append(a) # b 引用 a
del a
del b # 两个对象的引用计数都是 1(互相引用),无法释放
Python 2 的 gc 模块会定期检测并打破循环引用,但引用计数仍是主要的垃圾回收机制。这意味着大多数情况下,对象会在失去引用的瞬间被清理,不需要等待垃圾回收器运行。