公司动态
面向对象与异常处理:从自动咖啡机看 Python 类设计
面向对象与异常处理从自动咖啡机看 Python 类设计一、背景引入学会了文件、函数和装饰器你已经能写出「可复用」的代码。但当程序需要长期维护一组相关联的状态——比如一台咖啡机的豆量、水量、奶量以及「待机 / 制作中 / 完成」的状态机——你会发现纯函数开始吃力参数越传越多状态散落各处改一处牵动全身。这就是面向对象OOP登场的时刻。很多教程一上来就讲「封装继承多态」的名词却不让读者看见「为什么需要它」。这一篇我换一个角度以一台自动咖啡机为线索在真实云服务器上把类、继承、装饰器方法、包以及异常处理全部跑一遍、抓真实回显。当你看到「穷鬼机豆不够时被业务异常拦下、还能恢复」的那一刻OOP 和异常就不再抽象了。所有代码在华为云 Flexus X 实例Ubuntu 24.04Python 3.12.3上真实执行环境回显与上一篇一致内核 6.8.0-106、Python 3.12.3、实验目录/root/lab-b。下面直接进入实操。二、分节实操2.1 类与实例、__init__、类变量 vs 实例变量类的根本作用是「把数据和行为绑在一起」。__init__在每次类名(...)时自动运行用来初始化实例自己的数据。# b4_class_basic.py节选classCounter:count0# 类变量属于类def__init__(self):self.value0# 实例变量c1Counter();c2Counter()c1.value10Counter.count5真实回显 1) 定义类与创建实例 旺财 汪汪叫 | 大黄 汪汪叫 d1 的 name: 旺财 id(d1): 131065760114464 d2 的 name: 大黄 id(d2): 131065760114512 2) 类变量 vs 实例变量 初始: Counter.count 0 c1.value 0 改后: Counter.count 5 c1.value 10 c2.value 0 c2 读取 count继承自已改的类变量: 5 3) 通过实例给类变量赋值会发生什么 c2.count 99 (实例变量, id 11758824 ) Counter.count 5 (仍是类变量, id 11755816 ) c1.count 5 (c1 没有实例变量读到类变量)讲解每个实例有独立的内存id不同self.value是各管各的。count是类变量挂在类上所有实例共享c2.count 99并不会改掉类变量而是在c2身上新建了一个同名的实例变量遮蔽了类变量id 不同可证。c1没这实例变量读到的仍是类上的5。经验法则实例级状态放self全局共享常量才放类变量。2.2 类变量的共享坑真实复现把「可变对象」当类变量是另一个高频 bug# b4_classvar_pitfall.pyclassStudent:scores[]# 危险所有实例共享同一个列表defadd(self,s):self.scores.append(s)aStudent();bStudent()a.add(90);b.add(80)真实回显 复现把可变对象当作类变量 a.scores: [90, 80] b.scores: [90, 80] - b 的一个操作影响了 a a.scores 与 b.scores 是同一对象: True 正确做法在 __init__ 里初始化实例变量 a.scores: [90] b.scores: [80] - 互不干扰讲解a.add(90)往Student.scores这个共享列表里塞了 90于是b.scores也立刻看到 90——同一对象is为True修正办法是如第 28 行把self.scores []写在__init__里让每个实例各持一份。不可变类变量如VERSION 1.0当作常量则安全问题只出在可变类变量上。2.3 继承与 super()、方法重写继承让我们「站在父类肩膀上」扩展而非复制代码。# b4_inherit.py节选classBird(Animal):def__init__(self,name,wingspan):super().__init__(name)# 调父类 __init__ 初始化 nameself.wingspanwingspanclassA:defact(self):print(A.act);super().act()classB:defact(self):print(B.act)classC(A,B):passC().act()真实回显 1) 基础继承与方法重写 咪咪 喵喵叫 旺财 汪汪叫 2) super() 调用父类方法扩展而非完全覆盖 小燕 25 小燕 啾啾叫 (父类默认: ...) 3) 多层继承中 super 的 MRO C 的 MRO: [C, A, B, object] A.act B.act 4) 重写内置行为 __str__ Point(3, 4)讲解Cat/Dog重写了speak各自发声。super().__init__(name)避免重复写初始化逻辑。super()不是「调父类」那么简单——在C(A, B)这种多继承里它按MRO方法解析顺序找下一个类。第 46 行 MRO 是[C, A, B, object]所以C().act()先执行A.act其中super().act()顺着 MRO 找到B.act于是先打印A.act再B.act。理解 MRO 是多继承不踩坑的关键。重写__str__则让print(Point(3,4))直接给人看的格式。2.4 类装饰器方法property / classmethod / staticmethod这三个「方法装饰器」解决了三种不同诉求。# b4_decorators.py节选classCircle:def__init__(self,r):self._rrpropertydefr(self):returnself._rr.setterdefr(self,value):ifvalue0:raiseValueError(半径不能为负)self._rvaluepropertydefarea(self):return3.14159*self._r**2真实回显 1) property把方法当属性访问并做校验 r 5 area 78.54 改半径后 area 314.16 设负值被拦下: 半径不能为负 2) classmethod操作类本身 当前共有 2 个用户 3) staticmethod与类/实例都无关的实用函数 MathUtil.add(2,3) 5 实例也能调用: 9讲解property让c.area像属性一样访问却能在内部做计算、在setter里做校验设负半径当场被拦。classmethod首参是类cls适合做「统计类级别信息」或工厂方法staticmethod既不绑实例也不绑类纯粹是个挂在命名空间下的工具函数类和实例都能调。三者把「属性 / 类方法 / 独立函数」三种语义清晰分开。2.5 实战自动咖啡机状态、库存、继承出不同机型现在把前面所有概念组装成一台咖啡机。基类CoffeeMachine管状态与库存子类EspressoMachine意式只用豆和水和CappuccinoMachine卡布加奶各自实现_brew。原料不足时抛业务异常。# b4_coffee.py节选classCoffeeMachine:def__init__(self,name,beans200,water500,milk200):self.namename self._beans,self._water,self._milkbeans,water,milk self._state待机def_consume(self,beans0,water0,milk0):ifself._beansbeans:raiseInsufficientBeansError(f咖啡豆不足需{beans}g剩{self._beans}g)...defmake(self,kind):self._state制作中self._brew(kind)self._state完成真实回显 创建两台不同机型 [意式一号] 状态待机 | 豆200g 水500ml 奶200ml [卡布二号] 状态待机 | 豆200g 水500ml 奶200ml 制作演示 意式一号 开始制作 Espresso ... 萃取 Espresso18g 豆 30ml 水 制作完成。 [意式一号] 状态完成 | 豆182g 水470ml 奶200ml 卡布二号 开始制作 Cappuccino ... 打奶泡并萃取 Cappuccino18g 豆 30ml 水 100ml 奶 制作完成。 [卡布二号] 状态完成 | 豆182g 水470ml 奶100ml 原料耗尽场景 [穷鬼机] 状态待机 | 豆10g 水30ml 奶100ml 穷鬼机 开始制作 Cappuccino ... 捕获业务异常: 咖啡豆不足需 18g剩 10g 恢复后: [穷鬼机] 状态待机 | 豆10g 水30ml 奶100ml 继承关系确认 EspressoMachine 是 CoffeeMachine 子类: True cap 实例类型: CappuccinoMachine讲解两台机器各自独立扣减库存意式扣豆和水、卡布再扣奶状态从「待机」流转到「制作中」再到「完成」这就是用类把状态机管理起来。_consume在扣减前检查库存不足时抛InsufficientBeansError——程序没崩而是被精确的业务异常拦下且机器状态机保持干净捕获后我手动把_state复位为「待机」机器可继续服务。issubclass/type()也印证了继承关系成立。2.6 模块与包__name__ __main__、python3 -m 运行包当代码变多要把它拆成模块/包。我建了一个mymath包含__init__.py、ops.py、__main__.py演示两种运行方式的差异。# b4_module.py节选print(本文件直接运行, __name__ ,__name__)importmymathprint(mymath.add(1,2) ,mymath.add(1,2))真实回显导入包 1) 主程序自身的 __name__ 本文件直接运行, __name__ __main__ 2) 导入自定义包 mymath触发 __init__ 加载 [mymath/ops.py] 被加载, __name__ mymath.ops [mymath/__init__.py] 被加载, __name__ mymath mymath.add(1,2) 3 mymath.mul(3,4) 12 4) 模拟一段只在直接运行时才执行的代码 这是 main()通常放在 if __name__ __main__ 下再单独用python3 -m mymath把整个包当程序跑########## EXTRA: python3 -m mymath ########## $ cd /root/lab-b python3 -m mymath [mymath/ops.py] 被加载, __name__ mymath.ops [mymath/__init__.py] 被加载, __name__ mymath 以包方式运行: __name__ __main__ add(2,3) 5 mul(4,5) 20讲解__name__是 Python 注入的模块名。文件被直接运行无论python3 b4_module.py还是python3 -m mymath时它等于__main__被import时则是自己的模块路径如mymath、mymath.ops。所以if __name__ __main__:是「既能被当作库导入、又能作为脚本运行」的标准写法。注意python3 -m mymath触发的是包内__main__.py且__name__同样是__main__——这正是把包当命令行工具发布的惯用法。2.7 异常层级、try/except/else/finally、自定义业务异常、三大坑异常处理是健壮系统的底线。先看异常本身也是一套类层级# b4_exceptions.py节选print(Exception 的父类:,Exception.__mro__[1].__name__)print(ValueError 是 Exception 子类:,issubclass(ValueError,Exception))真实回显 1) 异常也是类存在继承层级 Exception 的父类: BaseException ValueError 是 Exception 子类: True ZeroDivisionError 是 ArithmeticError 子类: True 2) try/except/else/finally 完整结构 try: 执行可能出错的代码 else: 没有异常时才执行 finally: 无论是否异常都执行常用于释放资源 divide(10, 2) 5.0 try: 执行可能出错的代码 except: 捕获除零错误 finally: 无论是否异常都执行常用于释放资源 divide(10, 0) None 3) 捕获多种异常 拿到异常对象 捕获到 TypeError - unsupported operand type(s) for : int and str 捕获到 ValueError - invalid literal for int() with base 10: abc 捕获到 IndexError - list index out of range 4) 自定义业务异常类 业务异常: 咖啡豆不足现有 10g需要 18g | have 10 need 18讲解try/except/else/finally四段分工明确——try放可能出错的代码except处理异常else在无异常时才跑避免在 else 里又抛出被误判finally永远跑关连接、释放锁的最佳位置。第 3 节把TypeError/ValueError/IndexError放进一个元组统一捕获并拿到异常对象拿到可读信息。第 4 节的InsufficientBeansError(Exception)是自定义业务异常携带have/need字段让调用方能精确区分「豆不够」和「水不够」而不是一律except Exception。接下来是三个真实踩坑全部抓真实 Traceback。先看第 7 坑「捕获后丢失原始 Traceback」对应的真实输出这是服务器返回的原始 traceback因 stderr 比 stdout 先刷新它会出现在日志顶部此处归位到对应小节Traceback (most recent call last): File /root/lab-b/b4_exceptions.py, line 71, in module 1 / 0 ~~^~~ ZeroDivisionError: division by zero During handling of the above exception, another exception occurred: Traceback (most recent call last): File /root/lab-b/b4_exceptions.py, line 73, in module raise ValueError(包装后抛出) ValueError: 包装后抛出真实回显其余两个坑 5) 坑1裸 except 会吞掉 KeyboardInterrupt/SystemExit 裸 except 把 KeyboardInterrupt 也吞了(CtrlC 将失效) 6) 坑2异常被静默吞噬问题被掩盖 load_config() 返回: None - 真正的错误被吃掉了 7) 坑3捕获后又丢失原始 Traceback 异常: 包装后抛出 原始 Traceback已用 raise ... from 保留更好: 见上方真实 Traceback最上层原因 1/0 已丢失只剩 ValueError讲解坑1 裸except:它等价于except BaseException:连KeyboardInterruptCtrlC和SystemExitsys.exit都接住导致你按 CtrlC 都杀不掉程序。务必写具体的except ValueError:之类或至少except Exception:。坑2 异常静默吞噬except Exception: pass把真正的错误吃掉调用方拿到None还以为成功。要么处理要么重新raise。坑3 丢失原始 Traceback上面那段真实 Traceback 清晰展示了——内层1/0抛ZeroDivisionError被except后直接raise ValueError(包装后抛出)于是最原始的1/0原因被覆盖只剩ValueError。正确做法是raise ValueError(...) from e或raise保留During handling of the above exception的因果链排障时能一路追到根因。2.8 标准库巡礼datetime / pathlib / collections最后快速认识三个日常高频标准库无需装第三方包。# b4_stdlib.py节选fromdatetimeimportdatetime,timedeltafrompathlibimportPathfromcollectionsimportCounter,defaultdict,namedtuple真实回显 1) datetime时间与运算 当前构造: 2026-07-24 10:00:00 加 3 天 2 小时: 2026-07-27 12:00:00 格式化: 2026-07-24 10:00:00 解析: 2026-07-24 00:00:00 2) pathlib面向对象的路径操作 路径拼接: stdlib_demo/note.txt 是否存在: True | 文件名: note.txt | 后缀: .txt 读取内容: hello pathlib 父目录: stdlib_demo 3) collections.Counter计数 词频: Counter({apple: 3, banana: 2, cherry: 1}) 最常见2个: [(apple, 3), (banana, 2)] 4) collections.defaultdict缺省值 defaultdict: {a: [1, 2], b: [3]} 5) collections.namedtuple轻量结构化 Point: Point(x3, y4) | x 3 | 可被索引: 4讲解datetime支持时间加减timedelta与格式化/解析pathlib.Path用/拼路径、.exists()/.name/.suffix/.read_text()一行搞定比os.path字符串拼接优雅collections.Counter做词频统计、defaultdict免判空、namedtuple用字段名访问元组——这些都是「写更少、读更清楚」的官方利器。三、踩坑清单序号坑现象 / 真实报错正确做法1可变对象作类变量一个实例改了所有实例都变is为 True可变状态放self写在__init__里2误把实例赋值当改类变量c2.count99只是新建实例变量遮蔽类变量需要改类级共享状态用类名.count...3多继承不看 MRO调super()跳到意料之外的类用类名.__mro__确认调用顺序4property不设 setter 就赋值AttributeError: cant set attribute需要可写就补x.setter并校验5裸except:吞掉 CtrlC程序杀不掉捕获具体异常或至少except Exception6except: pass静默吞错调用方拿到None误以为成功处理或重新raise别静默7捕获后raise NewErr丢原 Traceback只剩外层错误根因如1/0消失用raise NewErr(...) from e保留因果链8模块if __name____main__缺失被 import 时自动跑一堆副作用代码入口逻辑都收进该判断内四、总结这篇我们从一台自动咖啡机出发把面向对象与异常处理讲透类与实例__init__初始化实例状态类变量与实例变量要分清尤其警惕可变类变量被所有实例共享的坑继承与 super用super()扩展而非覆盖父类MRO决定多继承下super()的走向类装饰器方法property把方法当属性并做校验classmethod/staticmethod分清「类级」与「独立工具」实战咖啡机用类管理状态机与库存原料不足时由自定义业务异常精确拦截并安全恢复模块与包__name__ __main__是「库/脚本两用」的标准写法python3 -m让包也能当程序跑异常try/except/else/finally各有分工自定义异常携带业务字段并务必避开裸except、静默吞噬、丢失原始 Traceback 三大坑标准库datetime/pathlib/collections是日常最高频的「官方外挂」。走到这里《Python 实战》进阶篇的函数式上篇与面向对象本篇两条主线就齐了。下阶段可以把两者结合用类组织状态、用装饰器横切日志与权限、用异常隔离故障边界写出既清晰又可长期维护的工程代码。本文实验均在华为云 Flexus X 实例Ubuntu 24.04, Python 3.12.3上真实执行。