1. 为什么第三章值得单独拿出来复盘1.1 从“能写函数”到“能设计系统”的分水岭很多人学编程有个错觉前两章能写递归、能算斐波那契、能拿高阶函数拼来拼去就觉得自己已经摸到门槛了。但真正把第三章啃完的人会明白前两章顶多是在教你“怎么把一句话说清楚”第三章才开始教你“怎么把一件事组织明白”。第三章的核心命题只有一个复杂度怎么管。赋值、局部状态、对象、类、继承、消息传递这些东西堆在一起不是为了让你多背几个语法而是为了回答一个问题——当程序不再是几十行的小脚本而是一个需要持续演化、多人协作、状态不断变化的系统时你靠什么把它撑住。我见过太多人卡在这里。前两章刷得飞快到第三章突然发现“看不懂了”不是语法看不懂是不知道作者到底想让我学会什么。这一章不像第一章那样给你一个清晰的替换模型也不像第二章那样给你一套数据抽象的操作手册它更像是在你面前摆了一堆零件然后说来自己组装一台能跑的车。所以这篇复盘不打算按教材顺序逐节翻译而是按“一个从业者回头看哪些东西真正长在了我身上”这个角度来拆。适合两类人一类是正在啃第三章、卡在半路想找人聊聊的另一类是学完了但觉得脑子里一团浆糊、想重新梳理一遍的。1.2 第三章到底在解决什么问题先把话说透。第三章表面在讲“模块化”实际在讲三件事第一状态是有代价的。引入赋值之后同一个函数调两次可能返回不同结果时间顺序开始影响结果引用和相等不再是一回事。这些代价不是 bug是特性但你必须知道你在付什么账。第二对象是管理状态的一种组织方式。把状态和操作状态的方法捆在一起用消息传递来交互这是面向对象最朴素的内核。教材里用局部状态变量加消息分发函数来模拟对象这个视角比直接上 class 语法要本质得多。第三赋值让程序有了“历史”。没有赋值的时候一个表达式就是一个数学函数输入确定输出就确定。有了赋值程序开始有“当前状态”这个概念开始有“先做什么后做什么”的时序依赖开始需要并发控制、需要事务、需要各种工程手段来兜底。这三件事串起来就是第三章的骨架。你如果只记住了“用字典模拟对象”或者“类可以继承”那等于只捡了地上的螺丝没看见整台机器的图纸。2. 赋值、状态与同一性三个最容易翻车的概念2.1 赋值不是“把值放进盒子”初学者对赋值的直觉通常是“给变量一个值”这个直觉在简单场景下够用但一到复杂场景就崩。更准确的模型是环境是一张表赋值是修改这张表里某个名字对应的绑定。这个区别在什么时候要命在你把函数当参数传来传去的时候。比如下面这段def make_counter(): count 0 def inc(): nonlocal count count 1 return count return inc c1 make_counter() c2 make_counter() print(c1(), c1(), c2())输出是1 2 1。为什么 c2 不是从 3 开始因为每次调用make_counter()都会创建一个新的环境帧count是那个帧里的局部绑定c1 和 c2 各自持有不同的帧。这就是“环境模型”比“盒子模型”更准确的地方——盒子模型解释不了为什么两个看起来一样的变量互不干扰。注意Python 里nonlocal和global的区别经常被搞混。nonlocal找的是最近的外层函数作用域global找的是模块顶层。写闭包计数器的时候用nonlocal别用global否则多个计数器会共享同一个变量直接翻车。2.2 同一性和相等性is和不是一回事第三章会反复敲打一个点两个东西相等不代表它们是同一个东西。这在 Python 里对应和is。a [1, 2, 3] b [1, 2, 3] print(a b) # True print(a is b) # False c a print(a is c) # True比较的是值is比较的是身份内存地址。为什么这个区分重要因为一旦引入赋值和可变对象“改一个地方会不会影响另一个地方”就完全取决于它们是同一个对象还是两个碰巧相等的对象。我踩过的一个坑早期写缓存的时候用列表当 key 去字典里查结果发现同样的内容查不到。原因是列表不可哈希而且就算可哈希两个内容相同的列表也是不同对象。后来改成元组才解决。这个坑的本质就是没分清“相等”和“同一”。2.3 局部状态带来的时序陷阱没有赋值的时候求值顺序基本不影响结果纯函数嘛。有了赋值之后顺序就是一切。看这个经典例子def withdraw(account, amount): if account[balance] amount: account[balance] - amount return ok return insufficient单线程下没问题。但如果两个请求同时进来都先检查余额、都通过、都扣款余额就可能变成负数。这不是代码写错了是局部状态 并发 竞态条件这是第三章埋下的一个巨大伏笔后面讲并发控制的时候会回来收。实操心得只要你的函数里出现了“先读后写”的模式就要立刻警觉——这里有没有可能被并发打断单线程脚本无所谓一旦上了服务端或者多线程这就是事故高发区。3. 用消息传递模拟对象比 class 更本质的视角3.1 为什么教材不直接上 class很多人第一次看第三章会疑惑Python 明明有 class为什么教材要用闭包加字典来模拟对象这不是脱裤子放屁吗不是。教材这么做是为了让你看清对象到底是什么。class 语法是一层语法糖它把“状态 方法 消息分发”这套机制包装得很好看但包装太好看了以至于你只看见糖看不见里面的机制。用闭包模拟对象的时候状态是闭包捕获的局部变量方法是内部定义的函数消息传递是一个分发函数根据“消息名”去查表调用对应方法。这三样东西拆开看你才明白对象不是语言特性是一种组织状态和行为的模式。任何支持闭包和字典的语言都能实现对象class 只是让这件事写起来更顺手。3.2 一个可复现的对象模拟实现下面这段代码是我自己复盘时写的比教材版本多了一点错误处理方便直接拿去用def make_account(balance): def withdraw(amount): nonlocal balance if balance amount: balance - amount return balance raise ValueError(insufficient funds) def deposit(amount): nonlocal balance balance amount return balance def dispatch(message, amount0): if message withdraw: return withdraw(amount) elif message deposit: return deposit(amount) elif message balance: return balance else: raise ValueError(funknown message: {message}) return dispatch acc make_account(100) print(acc(withdraw, 30)) # 70 print(acc(deposit, 50)) # 120 print(acc(balance)) # 120关键点在于balance是make_account的局部变量外部完全访问不到只能通过dispatch发消息。这就是封装的本质——不是语法上的 private而是作用域上的不可达。3.3 消息传递与直接调用的取舍那实际项目里到底用哪种我的经验是分场景场景推荐方式理由业务代码、团队协作class可读性好工具链支持完善需要极致封装、状态极敏感闭包 消息外部无法绕过接口需要动态分发、插件式扩展字典分发运行时可注册新消息性能敏感的热路径直接函数调用消息分发有额外开销教材用消息传递是为了教学不是让你在生产代码里到处写dispatch。但它给你的那个视角——对象是状态的封装加消息的协议——会一直有用。你以后看任何 ORM、任何 RPC 框架、任何 actor 模型都能看到这个模式的影子。4. 继承、委托与“不要重复自己”的边界4.1 继承的本质是接口复用不是代码复用第三章讲继承的时候很多人第一反应是“子类可以复用父类代码”。这个理解不算错但太浅。继承真正值钱的地方是接口一致性所有子类都响应同一组消息调用方不需要知道具体是哪个子类。class Shape: def area(self): raise NotImplementedError class Circle(Shape): def __init__(self, r): self.r r def area(self): return 3.14159 * self.r ** 2 class Rect(Shape): def __init__(self, w, h): self.w w self.h h def area(self): return self.w * self.h def total_area(shapes): return sum(s.area() for s in shapes)total_area不关心你传的是圆还是矩形它只关心你有没有area方法。这就是接口复用的价值。如果你只把继承当代码复用工具很容易写出“为了复用一行代码而强行继承”的烂设计。4.2 委托比继承更灵活的地方继承是静态的编译期或者说类定义期就定死了。委托是动态的运行期可以换。看这个例子class Logger: def log(self, msg): print(f[LOG] {msg}) class Service: def __init__(self, logger): self.logger logger def do_work(self): self.logger.log(working) # ...Service不继承Logger而是持有一个 logger 对象把日志工作委托给它。好处是测试的时候可以传一个假的 logger生产环境可以传一个写文件的 loggerService代码一行不用改。实操心得新手容易滥用继承看到“is-a”关系就继承。但很多“is-a”其实是“has-a”或者“uses-a”。判断标准很简单——如果子类并不需要父类的全部接口或者父类改一个方法会波及一堆子类那就该考虑委托。4.3 什么时候该停下来重构第三章有个隐含主题抽象是有成本的。每加一层继承、每加一个间接层都增加了理解成本。什么时候该停我的经验是三条线同样的代码出现第三次才考虑抽象。出现两次可能是巧合三次才是模式。如果抽象之后读代码需要来回跳三个文件才能看懂一个逻辑那这个抽象大概率过度了。如果子类里有一堆方法只是pass或者raise NotImplementedError说明父类接口设计得太宽该拆。这些不是教材里写的是踩坑踩出来的。教材教你“可以这么做”工程经验教你“什么时候不该这么做”。5. 并发、时间与状态第三章埋下的深水炸弹5.1 为什么赋值让并发变难纯函数没有并发问题因为不共享状态。一旦有了赋值多个执行流可能同时读写同一块状态问题就来了。balance 100 def withdraw(amount): global balance if balance amount: balance - amount return True return False两个线程同时执行withdraw(80)可能都通过检查都扣款余额变成 -60。这就是检查后执行check-then-act的经典竞态。5.2 几种常见的应对思路教材在这一块讲得比较抽象我补一点工程视角的思路做法适用场景加锁用互斥锁包住临界区共享状态少、竞争不激烈原子操作用语言提供的原子类型简单计数、标志位不可变数据状态变更产生新对象函数式风格、读多写少消息队列状态只由一个执行流持有actor 模型、高并发Python 里最简单的是threading.Lockimport threading lock threading.Lock() balance 100 def withdraw(amount): global balance with lock: if balance amount: balance - amount return True return False但加锁不是银弹。锁的粒度、死锁、性能开销都是新问题。第三章的价值不在于教你具体怎么加锁而在于让你意识到状态和并发是一对孪生问题你不可能只要状态不要并发代价。5.3 从“流”的视角重新理解程序第三章还有一个很妙的视角转换把程序看成一条随时间演化的“流”每个时刻有一个状态快照。这个视角在响应式编程、状态管理库、数据库事务里都能看到。比如前端的状态管理本质就是维护一个全局状态流所有组件订阅这个流状态变更时通知订阅者。这和第三章讲的“局部状态 消息传递”是同一个思想的不同包装。注意不要被“流”这个词吓到。它不是什么高深概念就是“状态会变而且变化有顺序”这件事的另一种说法。理解了这一点你看很多框架的设计文档会顺畅很多。6. 常见卡点与排查清单6.1 学第三章时最容易卡住的五个地方卡点一闭包里的变量捕获。循环里创建闭包所有闭包捕获的是同一个变量最后都拿到循环结束时的值。解决办法是用默认参数或者工厂函数把当前值固定下来。# 错误示范 funcs [lambda: i for i in range(3)] print([f() for f in funcs]) # [2, 2, 2] # 正确做法 funcs [lambda ii: i for i in range(3)] print([f() for f in funcs]) # [0, 1, 2]卡点二可变默认参数。函数默认参数在定义时求值一次之后所有调用共享同一个对象。用列表当默认参数是经典坑。# 错误示范 def append_to(x, lst[]): lst.append(x) return lst # 正确做法 def append_to(x, lstNone): if lst is None: lst [] lst.append(x) return lst卡点三is和混用。小整数和短字符串在 Python 里有缓存is可能碰巧返回 True但这是实现细节不能依赖。判断相等永远用。卡点四继承链太深。三层以上的继承链基本就是维护噩梦。如果发现自己需要看爷爷类才能理解孙子类的行为该重构了。卡点五把消息传递当万能药。消息传递适合解耦但调试困难因为调用栈不直观。生产代码里除非有明确需求否则优先用直接调用。6.2 一份可以贴在显示器边上的速查表症状可能原因排查方向函数调两次结果不同依赖了外部可变状态检查闭包和全局变量改 A 影响了 BA 和 B 是同一个对象用id()确认身份并发下数据错乱检查后执行竞态加锁或改原子操作子类行为诡异继承链太深或父类接口太宽考虑改委托闭包拿到的值不对循环变量捕获问题用默认参数固定这张表是我自己整理出来贴工位上的比翻书快。7. 复盘之后我真正带走的东西第三章学完到现在语法细节忘得差不多了但有几个思维习惯留下来了。第一个习惯是看到状态就问生命周期。这个变量从哪来、活多久、谁能改、改了谁知道。这四个问题问下来大部分设计问题会自己浮出来。第二个习惯是区分“相等”和“同一”。写缓存、写去重、写比较逻辑的时候先想清楚要的是值相等还是对象同一能省掉很多诡异 bug。第三个习惯是对间接层保持警惕。每加一层抽象都要问自己这层抽象省下的重复值不值得增加的理解成本答案经常是不值得。第四个习惯是把并发当成默认前提。哪怕现在写的是单线程脚本也先想一下“如果这段代码被并发调用会怎样”。想清楚了再决定要不要加保护比事后补锅强。第三章不是那种学完就能立刻用上的章节它更像是一副眼镜。戴上之前你看代码就是代码戴上之后你能看见状态在流动、对象在交互、时间在起作用。这副眼镜一旦戴上摘不下来了。最后分享一个我自己的复习方法把第三章的每个例子用两种方式重写一遍一种用教材的闭包加消息传递一种用 Python 的 class。写完之后对比你会发现两种写法在表达力上其实等价差别只在可读性和工具支持。这个对比做上五六组第三章的核心思想基本就长在脑子里了。