有时候,一个好的例子,胜过百篇解释。本文分享 10 个我用了好几年才真正掌握的 Python 概念 —— 希望你读完之后,会想说一句:“哇,这些我早该搞明白了。”
为什么这些“基础”反而容易被忽略
当你刚学 Python 的时候,大多教程都会教语法:变量、循环、函数、基本数据结构……看似“入门简单”。作者也曾以为学了一年就“基本会了”。
真正问题在于,这些容易被误解 / 忽视的细节 —— mutable vs immutable、参数传递方式 (pass-by-object-reference)、生成器 vs 迭代器、is vs == 等 —— 它们不是“高级炫技”,却是决定代码行为是否正确的关键基础。
每当我真正弄懂它们的时候,潜藏在代码里的各种 bug /怪异行为 /性能坑,才突然“豁然开朗”。
因此,对于中级 Python 开发者 —— 把这些基础概念真正搞清楚,比盲目追新特性更重要。
目录
- 1.可变 (Mutable) vs 不可变 (Immutable) 对象
- 2.参数传递机制 — “Pass-by-Object-Reference”
- 3.is vs == 的区别
- 4.迭代器 (Iterator) 和 生成器 (Generator)
- 5.列表 / 生成式 (List / Comprehension) vs 传统循环
- 6.默认参数 (Default Argument) 的陷阱
- 7.可变默认参数 + 深浅拷贝问题
- 8.闭包 (Closure) + 延迟绑定 (Late Binding) 陷阱
- 9.多重继承 & MRO(方法解析顺序)陷阱
- 10.duck typing / 接口 / “Pythonic” 编程风格
1.可变 (Mutable) vs 不可变 (Immutable) 对象
很多初学者默认 Python 中 “变量 = 值” 的关系就像数学中的等号。但事实上,不同类型 (可变 / 不可变) 的对象,其行为差异极大 —— 理解不当就容易引入难以察觉的 bug。
不可变对象 (immutable):一旦创建,内容不能改变 (例如:字符串 str、整数 int、元组 tuple 等)
可变对象 (mutable):内容可以改变 (例如:列表 list、字典 dict、多数自定义对象、集合 set 等)
当你把一个可变对象赋值给多个变量 / 传入函数 /作为默认参数 /放在数据结构里 —— 如果你修改了它,就可能“无声地”影响到其他地方。
✔ 如何正确对待 / 避免误用
- 对于需要“独立副本 /不会被意外修改”的场景,用 immutable 对象,或在使用 mutable 对象前 copy/深拷贝 (deep copy);
- 对于函数参数 /默认参数 /共享数据结构,尤其小心不要滥用 mutable 对象;
- 理解 “赋值只是共享引用 (reference)”——并不代表复制 (copy)。
2.参数传递机制 — “Pass-by-Object-Reference”
❓ 很多人理解成 “值传递 / 引用传递”
在很多语言 (如 C/C++、Java) 中,“传值 vs 传引用”是经典概念。Python 没有 “传引用给变量 / 传值给变量”的二分 —— 它采用的是 “对象引用传递 (pass-by-object-reference)” 或称 “共享引用 (shared reference)” 模型。
这意味着:当你把一个对象 (mutable 或 immutable) 作为参数传入函数时,函数接收到的是对象的引用 (pointer/reference),而不是它的“副本”。
-
如果是 immutable 对象 (例如整数、字符串、tuple),“修改”操作实际上是创建新对象 → 原对象不变。
-
如果是 mutable 对象 (list、dict …),函数里如果改变内容 (比如 list.append / dict 修改 …),就会同时影响外部传入的对象。
✔ 常见避免错误的做法
-
如果函数内部不应该修改传入数据 — 手动在函数内做 shallow copy / deep copy;
-
明确在文档 / 注释 /函数名中注明 “函数是否有副作用 (mutate input)”;
-
尽量使用 immutable 数据结构 (tuple, frozenset, namedtuple / dataclass with frozen=True…) + 纯函数 (pure function) 编程风格。
3.is vs == 的区别
❓ 为什么很多人搞混
==:用于判断 值 (value /内容) 是否相等 — 对象的“等价性 (equality)”。
is:用于判断 引用 (identity) 是否相同 — 即两个变量是否引用同一个对象。
在 immutable 对象 (尤其较小整数, short string) 上,Python 可能做缓存 / 重用 (interning),这会让 a is b 和 a == b 同时为 True —— 导致误以为 “is 与 == 区别不大 / 可以混用”。
但对于一般 mutable 对象 (list / dict /自定义对象 …) 或更大 / 特殊 immutable 对象,就绝不能混用。
✔ 推荐做法
-
一般判断“内容是否一致 /相等” → 用 ==。
-
仅在你真的想知道“两个引用是否指向同一个对象 (同一块内存 /同一个实例)”时用 is。
-
对于 None 判断 /类型判断常用 is None / is instance(obj, MyClass) (但注意 isinstance 本身不是 is)。
4.迭代器 (Iterator) 和 生成器 (Generator)
❓ 为什么初学者容易混淆
Python 提供了非常灵活、语法简洁的迭代机制 (for-loop / list-comprehension / generator / yield …),但背后的机制并不总是显而易见。
-
Iterator:实现了 iter() 与 next() 的对象,可以逐个产出元素,直到耗尽 (StopIteration)
-
Generator:是一种特殊的 iterator,通过 def … yield … 定义 — 每次 yield 产出一个值,状态 (局部变量、中间状态) 保持。
很多人习惯用 list comprehension /普通 list → list,而忽视 generator 的惰性 (lazy) 特性。
✔ 为什么要理解 + 推荐做法
-
对于大数据 /流式数据 /不需要一次性读入所有数据的场景,用 generator 更节省内存、性能更好;
-
避免同时将 iterator / generator “复用” (因为一旦迭代完毕,就不能再次从头开始,除非重新生成);
-
清楚理解 “迭代耗尽 (exhausted)” 的概念,避免莫名其妙空循环 /数据不见的问题。
5.列表 / 生成式 (List / Comprehension) vs 传统循环
❓ 传统循环 vs 一行表达的“好看”可能隐藏陷阱
Python 的 list comprehension / dict / set comprehension 提供了一种非常简洁、 Pythonic 的方式来构造数据结构。许多 Python 开发者鼓励使用它,因为它读起来短、写起来快。
但如果写得不当 /过度嵌套,就可能牺牲可读性 /引入错误 /降低调试效率。
✔ 推荐做法
-
简单映射 /过滤 /变换 → 用 comprehension;
-
逻辑复杂、条件多、流程复杂 → 用传统循环 + 明确注释;
-
遵循 “清晰优于晦涩 (readability over cleverness)” 的原则 (也对齐 Zen of Python) 。
6.默认参数 (Default Argument) 的陷阱
❓ 默认参数貌似方便,但可能带 bug
在 Python 中,函数定义时可以给参数设默认值,例如:
def foo(x, lst = []):
lst.append(x)
return lst
你或许期望,foo(1) → [1],foo(2) → [2],结果却是 [1,2] —— 因为默认参数 lst=[] 只被创建一次,每次调用复用同一个列表。
✔ 推荐做法
不要用 mutable 对象 (list / dict / set) 作为默认参数;
如果你确实需要 “可选 / 默认空容器 / 默认空对象”,用 None,然后在函数内部初始化:
def foo(x, lst=None):
if lst is None:
lst = []
lst.append(x)
return lst
7.可变默认参数 + 深浅拷贝问题
这个与第 6 项是延伸。
❓浅拷贝 vs 深拷贝、引用 vs 内容
浅拷贝 (shallow copy) 只复制对象的顶层 (新对象,但内部子对象引用不变);
深拷贝 (deep copy) 递归复制所有子对象 → 完全独立拷贝。
当你的默认参数 /共享数据结构里有复杂嵌套 (list of dict, dict of list…),浅拷贝 /浅初始化往往不能避免引用共享,从而引起混乱。
✔ 推荐做法
对于复杂数据结构 (嵌套 list/dict) /需要完全隔离副本 → 使用 copy.deepcopy();
对函数参数 /配置 /状态管理谨慎使用 mutable 结构。
8.闭包 (Closure) + 延迟绑定 (Late Binding) 陷阱
❓什么是延迟绑定
当你在循环里创建多个 λ /函数 / closure 时,如果不注意,所有 closure 可能会共享同一个变量引用 (而这个变量最终为循环结束时的值) —— 导致行为异常。
例如 (伪代码):
funcs = []
for i in range(3):
funcs.append(lambda: print(i))
for f in funcs:
f() # 都打印 3,而不是 0,1,2
很多初学者/中级开发者因为不理解 closure + late-binding,而深受其坑。
✔ 推荐做法
如果你需要在循环中生成多个函数 / closure,且希望它们捕获“当时 i 的值” → 使用 default-arg trick / lambda arg binding:
funcs = []
for i in range(3):
funcs.append(lambda i=i: print(i))
对 closure 的行为 (作用域 /变量捕获 /生命周期) 有清晰理解;
避免过度依赖 closure 写复杂逻辑,必要时用类 /对象 /显式函数代替。
9.多重继承 & MRO(方法解析顺序)陷阱
❓为什么多重继承容易出错
Python 支持多重继承 (multiple inheritance),但多个父类之间的方法 /属性冲突、钻石继承 (diamond inheritance) 等问题极容易导致令人迷惑的行为。
尤其对于 MRO (Method Resolution Order,方法解析顺序) 理解不清,就更容易造成 bug。
✔ 推荐做法
-
尽量避免 /慎用多重继承;
-
使用组合 (composition) + 接口 (protocols / duck typing) 或 mixin (但也要谨慎);
-
如果一定要用多重继承,应明确了解 MRO 顺序 (ClassName.mro());
-
注释清晰,避免父类方法重写 /冲突。
10.duck typing / 接口 / “Pythonic” 编程风格
🎯 什么是 “Pythonic” + duck typing
在 Python 中,强调 “行为 (behavior) 优于类型 (type)” —— 如果一个对象 “像鸭子 (duck) 那样走、像鸭子那样叫”,那它就是鸭子。
因为有一句经典的谚语:
“If it walks like a duck and quacks like a duck, it must be a duck.”
“如果它走路像鸭子,叫声像鸭子,那它就是鸭子。”
翻译成程序员语言:
Python 不关心你是不是 Duck 类
它只关心你有没有 walk() 和 quack() 方法
如果你都实现了,那你就是“鸭子类型”
所以 对象“能做什么”比它“是什么类型”更重要。
这种风格强调灵活性、多态,以及“接口优于继承 / 类型判断”。
但这也带来了容易混乱的地方 —— 如果不小心,一个对象可能因为缺少某些方法 /属性,在运行时出错,而且错误显式性差 /难以追踪。
✔ 推荐做法
编写 “鸭子” 类 /接口 时,明确文档 /类型注释 (type hint / Protocols / ABC);
对关键接口 /协议 (methods / 属性) 做单元测试 (unit test) /断言 (assert);
对于关键数据结构 /关键路径,尽量用静态类型检查 (mypy / Pyright 等) + 运行时检查 (assert / validation);
在多人协作 /大项目中慎用过度 “动态 /魔法式” 编程,保持可读 / 可维护。
总结:
“基础不牢,地动山摇” — 对于 Python 而言,这句话比任何彩蛋语法都更真切。

被折叠的 条评论
为什么被折叠?



