前言本文为“python面向对象编程”系列教程上篇拓展篇请读者在学习本篇之前确保已掌握上篇中涉及的内容。在上篇中我们初步接触了类和对象学会了如何定义类的属性和方法并讨论了数据存储与职责划分的原则。在本篇中笔者将结合项目 BingGo 的开发经验通过一个象棋游戏引擎设计的案例进一步介绍 Python 中常用的工具类如Enum、dataclass、pydantic.BaseModel等。这些工具类均由标准库或第三方库提供它们借助元编程、装饰器等高阶手段帮助我们以更少的代码、更清晰的语义构建健壮的面向对象系统。一、搭建 OOP 框架象棋游戏需要维护大量状态数据包括棋盘布局、当前回合、走子历史等。我们自然选用 OOP 范式定义一个Game类作为总管。from__future__importannotations# 延迟注解求值fromtypingimportLiteral,OptionalclassGame:def__init__(self):self.phase:Literal[playing,end]playing# 棋局状态self.board:list[list[Optional[Qizi]]][[Nonefor_inrange(9)]for_inrange(9)]# 棋盘状态self.turn:Literal[red,black]red# 当前回合self.history:list[Move][]# 走子历史def__str__(self):col_header .join(str(i)foriinrange(9))lines[col_header]fori,rowinenumerate(self.board):row_strstr(i) .join(q.typifqisnotNoneelse·forqinrow)lines.append(row_str)return\n.join(lines)def__getitem__(self,key:tuple[int,int]|int)-Optional[Qizi]|list[Optional[Qizi]]:ifisinstance(key,int):returnself.board[key]ifisinstance(key,tuple)andlen(key)2:returnself.board[key[0]][key[1]]raiseTypeError(fargument key of Game.__getitem__ should be int or (int, int), got{key}:{type(key)}instead)def__setitem__(self,key:tuple[int,int],value:Optional[Qizi])-None:ifisinstance(key,tuple)andlen(key)2:self.board[key[0]][key[1]]valuereturnraiseTypeError(fargument key of Game.__setitem__ should be (int, int), got{key}:{type(key)}instead)defapply_move(self,move:Move)-None:# 走子...在架构设计阶段最重要的是明确各数据的存储方式和通信方式。Python 对象有固定的类型而变量名只是标签可以随时指向不同类型的对象因此是一门动态/强类型语言强要求每个数据有固定的类型动态又允许我们不必在每个地方都显式注明类型。此时使用类型注解在关键位置注明类型不仅可以帮助我们梳理编码思路还能大幅提高代码的可读性并让 IDE 提供更好的自动补全和静态检查支持。类型注解是什么类型注解Type Annotation是 Python 3.5 引入的语法用变量: 类型的形式注明变量期望的数据类型。它不会强制检查但能帮助 IDE 提供补全和静态检查如mypy。Literal[playing, end]表示该变量只能取playing或end这两个字符串值之一超出则类型检查报错。Optional[Qizi]等价于Qizi | None表示该位置既可以是Qizi对象也可以是None。二、使用 Enum 替代字面量Game.phase和Game.turn的值都被限定在有限个定值选择之内。这种场景下使用 enum 枚举类型优于普通字符串或整数常量。fromenumimportEnumclassPhase(Enum):playing0end1classColor(Enum):redrblackb什么是EnumEnum是 Python 标准库提供的枚举类它的每个成员都是唯一的、有名字的常量。通过class Phase(Enum)的方式可以实现类的继承Phase就成为了Enum的子类由此继承了Enum类的特性。使用Enum有何优势消除“魔数”代码中不再出现playing或0这类难以解释的字面量。精确的类型检查类型注解可以声明Phase任何试图传入其他值的操作都会在静态检查或运行时被捕获。IDE 代码补全输入Phase.即可获得所有可选成员避免拼写错误。改造后的GameclassGame:def__init__(self):self.phase:PhasePhase.playing self.turn:ColorColor.red...业内约定常量一般使用全大写格式命名比如PLAYING 0但笔者觉得这实在是太丑陋了于是自作主张改为小写读者可自行选择。三、使用 dataclass 固定化棋子对于游戏中的棋子我们建立一个类Qizi用于保存其基本属性。读者不妨尝试自行设计和尝试书写class Qizi的实现注意慎重考虑其应当具有的属性可以根据DRY、KISS、YAGNI三大原则自检。以下是笔者的思考classQizi:def__init__(self,typ:str,color:Color):self.typtyp self.colorcolordef__str__(self):returnf{self.typ.upper()}{self.color.lower()}def__repr__(self):returnfQizi(typ{self.typ}, colorColor.{color.name})def__eq__(self,other):ifnotisinstance(other,Qizi):returnFalsereturnself.typother.typandself.colorother.color棋子对象应该知道自己的位置吗答案是否定的。正如程语言创始人ljq所言一个数据只在一个地方存储且可以被调用。既然Game.board这一list已然通过index明确了该棋子对象的位置我们就不应再在内部添加其位置属性这一想法与上篇结尾做出的设计选择不谋而合。但这类“数据容器”类的编写往往重复且繁琐Python 3.7 引入的 dataclasses 模块通过装饰器自动生成常用方法大大简化了代码fromdataclassesimportdataclassdataclass(frozenTrue)classQizi:typ:strcolor:Colordef__str__(self):returnf{self.typ.upper()}{self.color.lower()}if__name____main__:print(Qizi(k,Color.red))# Qizi(typk, colorColor.red: 0)就这么简单而且dataclass会根据字段定义自动生成__init__、__repr__、__eq__等方法。frozenTrue保证了实例化后的对象不可以更改仅作为数据存在非常符合棋子的语境。什么是装饰器dataclass装饰器是 Python 中一种高阶语法它允许在不修改函数/类定义的前提下为其附加额外功能。dataclass会包装我们的类在类定义完成后自动插入一些方法代码。这属于元编程的一种体现——代码在运行时生成代码。四、使用 pydantic.BaseModel 进行数据验证与序列化游戏中的“走子”操作可以抽象为一个Move对象包含走子方、起始坐标和目标坐标。我们需要确保这些数据的合法性和格式正确性并且可能需要与前端进行数据交换。Pydantic (v2) 是一个强大的数据验证库它的 BaseModel 结合了类型注解与运行时验证非常适合定义这类数据模型。frompydanticimportBaseModelclassMove(BaseModel):color:Color from_pos:tuple[int,int]to_pos:tuple[int,int]BaseModel的子类会自动根据字段类型进行验证例如color必须是Color枚举成员from_pos和to_pos必须是包含两个整数的元组。为什么我的程序报错ModuleNotFoundError: No module named pydanticpydantic是第三方库需要先在终端通过pip install pydantic安装。如果遇到安装失败等问题请自行查找相关教程或求助AI排查问题。Pydantic 与普通 dataclass 有何不同dataclass 只提供编译时类型提示但不会在运行时检查类型。Pydantic 则会在__init__时进行严格校验若传入colorx字符串不匹配Color枚举类会立即抛出ValidationError。这使得数据可靠性大大增强。另外Pydantic 内置了强大的序列化/反序列化功能说人话就是把在Move对象和字符串之间相互转换。这一强大能力将会在下篇拓展篇中真正用到届时你将会看到仅需一行代码即可将 Move 对象转为 JSON 字符串在网络上传输或从网络接收的 JSON 还原为对象。游戏通常需要保存或网络传输走子记录Pydantic 提供了便捷的 JSON 序列化方法# 序列化 Move - strjson_strmove.model_dump_json()# 反序列化 str - Movemove2Move.model_validate_json(json_str)我们可以在Game.apply_move中接收一个Move对象直接使用其属性并执行走子逻辑。check_move的实现留给读者练习绝对不是因为我懒得写了。# 常量EXTINCTION_PIECES(k,)classGame:...defapply_move(self,move:Move)-None:ifnotself.check_move(move):raiseValueError# 1. board 实施走子self[move.to_pos]self[move.from_pos]self[move.from_pos]None# 2. history 增加记录self.history.append(move)# 3. turn 切换当前方self.turnColor.blackifself.turnColor.redelseColor.red# 4. phase 检查是否胜利forcolorinColor:ifnotany(any(q.typinEXTINCTION_PIECESandq.colorcolorforqinrow)forrowinself.board):self.phasePhase.endbreakdefcheck_move(self,move:Move)-bool:# 1. 检查游戏是否还在进行# 2. 检查走子方是否为当前回合# 3. 检查棋盘上对应的棋子是否可移动# 4. 检查走法是否有效该步过于复杂可留空raiseNotImplementedErrorif__name____main__:gameGame()game[8,4]Qizi(k,Color.red)game[0,4]Qizi(k,Color.black)print(初始棋盘,game,sep\n)game.apply_move(Move(colorColor.red,from_pos(7,4),to_pos(6,4)))print(走子后棋盘,game,sep\n)结语在本篇中我们通过一个象棋游戏引擎的设计深入学习了 Python 中几个重要的“工具类”Enum用有意义的名称替代魔数提升代码可读性与类型安全性。dataclass自动生成常用方法简化数据容器的定义。pydantic.BaseModel提供运行时数据验证和序列化/反序列化能力是构建健壮 API 和复杂数据流的利器。这些工具的背后是 Python 强大的元编程能力——装饰器、元类等机制让我们可以用声明式的方式描述数据结构而将繁琐的实现细节交给库去处理。掌握它们你将能更高效、更优雅地编写面向对象程序。下篇预告在**下篇拓展篇**中我们将继续探索 Python 高级 OOP 特性如抽象基类ABC、协议Protocol、以及如何利用类型系统设计更灵活的插件架构。敬请期待