Python 入门:对象模型与语法基础
更新时间:2026-08-30。本文回答:Python 的变量到底存了什么?为什么
a = []; b = a改b会动a?不可变对象到底"不可变"在哪? 这些是看懂后续生成器、GIL、性能优化的地基。
一、一切皆对象:变量是"名字",不是"盒子"
很多从 C/C++ 过来的工程师会本能地以为 x = 10 是把数字 10 塞进一个叫 x 的内存格子。在 Python 里这个直觉要改:变量只是贴在对象上的标签(引用),对象本身另有所在。
a = [1, 2, 3]
b = a # b 和 a 指向同一个列表对象
b.append(4)
print(a) # [1, 2, 3, 4] —— a 也变了理解这一点对性能很关键:Python 没有"在栈上放一个 int"这回事,所有数据都在堆上,变量只是引用。赋值、传参、返回值,操作的都是"引用的复制",不是对象本身的拷贝——这正是 Python 函数传参被叫做"传对象引用"(call by object reference)的原因。
| 操作 | C/C++ 直觉 | Python 实际 |
|---|---|---|
b = a(a 是列表) | 复制整个数组 | 复制引用,两个名字指向同一对象 |
| 函数传参 | 值拷贝 / 指针 | 引用拷贝,函数内可变对象会被改动 |
x = 10 | 栈上存 10 | 名字 x 绑定到整数对象 10 |
二、动态强类型:变量无类型,值有类型
- 动态:同一个名字可以先后绑定不同类型的值(
x = 10; x = "hi"合法)。 - 强类型:不会偷偷做
1 + "1"这种隐式转换,会直接抛TypeError。
x = 10
print(type(x)) # <class 'int'>
x = "hello"
print(type(x)) # <class 'str'>
print(1 + "1") # TypeError: unsupported operand types性能视角:动态类型意味着每次运算,解释器都要在运行时查对象的类型、查方法表,这是 Python 比 C 慢的核心来源之一。但这份灵活也让你能用生成器、迭代器与装饰器写出极省代码量的抽象。
三、可变 vs 不可变:理解"改不动"的对象
Python 对象分两类,这个区分贯穿整个语言设计:
- 不可变(immutable):
int、float、str、tuple、frozenset、bytes。创建后内容不能改,所谓"修改"是让名字指向新对象。 - 可变(mutable):
list、dict、set、bytearray、自定义类实例。原地可改。
s = "hello"
s += " world" # 不是改原字符串,而是新建一个字符串再绑定
print(id(s)) # 每次 += 后 id 都不同(新对象)
lst = [1, 2]
lst.append(3) # 原地修改,id 不变
print(id(lst)) # 和之前一样| 类型 | 可变? | 作为字典 key | 并发安全 |
|---|---|---|---|
str / tuple | 否 | 可以 | 天然安全 |
list / dict | 是 | 不可以 | 需加锁 |
为什么重要:不可变对象可以放心地当字典的 key、可以跨线程共享而不用担心被改;可变对象则要小心别名(aliasing)导致的意外修改。tuple 虽不可变,但若里面装了 list,那个 list 仍然可变——"浅不可变"的坑常在这里。
四、等号陷阱:+= 对不可变是重建,对可变是原改
初学者最容易踩的坑是下面这段:
def add_item(lst):
lst += [99] # lst 是可变参数,原地改了调用方的列表
def add_num(n):
n += 1 # n 是整数(不可变),只是局部名重绑,调用方无感
x = [1, 2]
add_item(x)
print(x) # [1, 2, 99] —— 调用方被改了!
y = 10
add_num(y)
print(y) # 10 —— 调用方没变这个差异完全由"可变/不可变"决定,不是 += 语法的问题。性能上,在循环里反复对字符串 += 会不断新建字符串、复制内容,复杂度退化为 O(n²);应该用 list 收集再 "".join()。
五、作用域与命名空间:LEGB 规则
Python 找名字按四层由内到外:
- Local:当前函数内
- Enclosing:外层嵌套函数
- Global:模块级
- Builtin:内置(如
len、print)
g = "模块级"
def outer():
e = "外层"
def inner():
print(e) # 找到 Enclosing 层的 e
print(g) # 找到 Global 层的 g
inner()性能视角:局部变量查找比全局变量快——因为全局/内置名字每次都要走字典查询。热点循环里把 len、math.sqrt 这类先绑到局部变量(_len = len),是老 Python 手常用的微优化(现代解释器已部分优化,但长循环仍有效)。
六、与本站性能主线的衔接
| Python 基础概念 | 本站对应 | 衔接文档 |
|---|---|---|
| 对象全在堆、引用语义 | L5 ELF/ABI 与内存布局 | 理解为什么 Python 内存开销大 |
| 不可变对象当 key | 哈希与缓存 | 缓存键的设计 |
| 动态查类型慢 | L4 CPU 微架构 | 解释执行 vs 编译执行开销 |
| 循环字符串拼接 O(n²) | GIL 与 asyncio | 把热点下沉到 C/numpy |
七、常见坑
- 默认参数别用可变对象:
def f(x=[])的[]在函数定义时创建一次,多次调用共享同一个列表,应该用x=None再判空新建。 is比较的是身份不是值:a is b看是否同一对象;比内容请用==。小整数有缓存(-5~256),别依赖这个偶然行为。- 浅拷贝陷阱:
copy.copy只复制外层,里面的可变元素仍共享;需要完全独立用copy.deepcopy。
一句话总结
Python 的变量是贴在堆对象上的名字(引用语义)、对象分可变/不可变、查找名字走 LEGB——搞清楚这三件事,后面生成器、GIL、性能优化里那些"为什么这样慢/为什么这样改"才说得通。