Python面向对象设计实战:鸭子类型、协议与组合式架构
1. 这不是又一本“类和对象”的入门课——它是一份你写Python时真正会翻的案头手册
我带过几十个从零起步的Python学习小组,也给三年以上经验的工程师做过代码评审。每次聊到面向对象,总有人皱着眉头说:“我知道class怎么写,__init__怎么用,但一到真实项目里,就卡在‘到底该不该拆成类’‘这个方法放哪儿才不别扭’‘为什么别人代码看着清爽,我的越写越像一锅炖’。”这本《Object-Oriented Programming in Python: A Complete Guide》标题里的“Complete”,不是指覆盖所有语法糖,而是指覆盖你从写第一行class到重构一个20万行服务时,每一步真实发生的决策、权衡与踩坑现场。它解决的是“知道语法却不会设计”的断层问题——比如为什么Django的Model类把数据库操作封装进实例方法,而FastAPI的Pydantic模型却坚决禁止任何业务逻辑;为什么requests库用Session类管理状态,而logging模块偏偏用模块级函数+全局配置;为什么你照着教程写的“银行账户类”在单元测试里总要mock一堆依赖,而生产系统里一个订单服务却能天然隔离变化。这些不是风格偏好,是OOP在Python语境下被反复验证过的生存法则。本文不讲UML图、不画继承树、不背SOLID口诀,只还原我在电商中台重构支付网关、在IoT平台设计设备抽象层、在数据管道中封装ETL任务时,手写每一行代码前脑子里的真实推演过程:这个类该有几条生命线?它的边界在哪里?谁该为它的错误负责?当需求明天就变,哪部分代码今天就得留好缝?如果你正被“类爆炸”“方法散落”“继承链断裂”困扰,或者想跳过教科书陷阱,直接拿到可落地的设计心法,这篇就是为你写的。
2. 为什么Python的OOP不是Java/C++的平移?核心设计哲学的底层差异
2.1 “鸭子类型”不是一句口号,而是整个设计体系的基石
很多初学者学完Python OOP后,第一反应是:“哦,原来就是把Java的public class改成class,把void method()改成def method(self):”。这种理解会直接导致灾难性后果。关键差异在于:Java/C++的OOP是“契约先行”——你必须先声明接口(interface)或抽象类(abstract class),所有实现者必须严格遵守签名;而Python的OOP是“行为后验”——只要对象有某个方法且能正确执行,它就“是”那个类型,无需提前约定。这听起来很自由,但实际设计时,它彻底改变了你的思考路径。
举个真实例子:我们曾为物流系统设计一个“运单处理器”。按Java思路,你会先定义一个ShipmentProcessor接口,规定validate(),calculate_fee(),generate_label()三个抽象方法,再让DomesticProcessor,InternationalProcessor去实现。但在Python里,我们最终只写了两个独立函数:
def process_domestic_shipment(shipment: dict) -> dict: if not shipment.get("weight_kg"): raise ValueError("Weight required for domestic") fee = shipment["weight_kg"] * 8.5 return {"fee": fee, "label_url": f"https://label/dom/{shipment['id']}"} def process_international_shipment(shipment: dict) -> dict: if not shipment.get("customs_form"): raise ValueError("Customs form required for international") fee = shipment["weight_kg"] * 22.0 + 150.0 return {"fee": fee, "label_url": f"https://label/int/{shipment['id']}"}然后用一个策略字典统一调度:
SHIPMENT_PROCESSORS = { "domestic": process_domestic_shipment, "international": process_international_shipment } # 调用方完全不关心具体实现,只认函数签名 result = SHIPMENT_PROCESSORS[shipment["type"]](shipment)提示:这里没有类,没有继承,甚至没有显式类型注解(虽然加了dict提示)。但它完美满足了OOP的核心目标——封装变化、解耦调用与实现、支持运行时替换。而如果强行套用Java模式写成类,你会立刻陷入“要不要共用父类?”“validate方法参数要不要统一?”“异常类型要不要继承自BaseShipmentError?”等无意义争论。Python的鸭子类型让你把精力聚焦在行为是否一致上,而不是结构是否相似上。这就是为什么Python标准库中,
json.load()接受任意具有read()方法的对象(文件、StringIO、网络流),itertools.chain()接受任意可迭代对象(列表、生成器、自定义类)——它们根本不关心你是不是“FileLike”或“Iterable”类,只看你有没有那个方法。
2.2 “一切都是对象”带来的隐性约束:不可变性与身份标识的微妙平衡
Python中整数、字符串、元组是不可变对象,列表、字典、自定义类实例是可变对象。这个基础事实直接决定了OOP设计中的关键取舍。比如,我们设计一个Point类表示坐标:
# 错误示范:可变属性引发意外共享 class BadPoint: def __init__(self, x, y): self.x = x self.y = y p1 = BadPoint(1, 2) p2 = p1 # p2只是p1的另一个名字,指向同一内存 p2.x = 999 print(p1.x) # 输出999!p1被意外修改这个问题在Java里同样存在,但Python的“一切皆对象”让陷阱更隐蔽。因为连int、str都是对象,你很容易忽略引用传递的威力。正确做法是明确设计意图:
如果
Point代表数学概念(应不可变),就用@dataclass(frozen=True)或namedtuple:from dataclasses import dataclass @dataclass(frozen=True) class Point: x: float y: float p1 = Point(1, 2) # p1.x = 999 # 运行时报错:Cannot assign to field 'x'如果
Point代表物理位置(需实时更新),就用普通类,但必须在文档中强调其可变性,并提供清晰的更新接口:@dataclass class MutablePoint: x: float y: float def move_to(self, new_x: float, new_y: float): self.x = new_x self.y = new_y def __repr__(self): return f"Point({self.x}, {self.y})"
注意:
@dataclass自动生成的__eq__和__hash__方法,会基于所有字段值计算。对不可变Point,Point(1,2) == Point(1,2)返回True;对可变Point,如果你后续修改了x,它的哈希值就变了——这会导致它在dict或set中“消失”。所以可变对象绝不能作为字典键或集合元素,除非你重写__hash__并确保它不依赖可变字段。这是Python OOP中必须刻进DNA的铁律。
2.3 “魔法方法”不是炫技工具,而是定义对象社会关系的宪法
__str__,__repr__,__len__,__getitem__这些方法,常被教程称为“特殊方法”或“魔术方法”。但它们的真实角色,是让自定义类融入Python内置生态的通行证。一个没实现__len__的类,你就不能对它用len();没实现__iter__,就不能用for item in obj:;没实现__add__,两个实例相加就会报TypeError。这不是语法限制,而是Python设计哲学:内置操作符和函数应该对所有“合理”的对象一视同仁。
我们曾开发一个TimeWindow类表示时间区间,最初只实现了基本属性:
class TimeWindow: def __init__(self, start: datetime, end: datetime): self.start = start self.end = end结果在业务代码中不断出现硬编码判断:
# 到处都是这样的重复代码 if window.end - window.start > timedelta(hours=24): send_alert() if len(window.events) > 100: # events是列表,但window本身没长度概念 ...后来我们补全了关键魔法方法:
from datetime import timedelta class TimeWindow: def __init__(self, start: datetime, end: datetime): self.start = start self.end = end self._events = [] # 内部存储 def __len__(self): """窗口内事件数量""" return len(self._events) def __contains__(self, event_time: datetime): """判断时间点是否在窗口内""" return self.start <= event_time <= self.end def __add__(self, other: 'TimeWindow') -> 'TimeWindow': """合并两个窗口(取并集)""" new_start = min(self.start, other.start) new_end = max(self.end, other.end) return TimeWindow(new_start, new_end) def __repr__(self): return f"TimeWindow({self.start!r}, {self.end!r})" def __str__(self): return f"[{self.start.strftime('%H:%M')}-{self.end.strftime('%H:%M')}]"立刻,业务代码变得像自然语言:
# 清晰、简洁、符合直觉 if len(window) > 100: send_alert() if now in window: # 不再是 window.contains(now) trigger_realtime_processing() merged = window_a + window_b # 不再是 merge_windows(window_a, window_b) print(str(window)) # 输出 "[09:00-17:00]"实操心得:不要等到报错才补魔法方法。在定义新类时,立刻问自己:“用户最可能对它做什么操作?”——查长度?遍历?比较大小?格式化打印?相加?然后优先实现对应的
__len__,__iter__,__lt__,__str__,__add__。这是让代码“长出Python味”的最快方式。记住:__repr__的目标是“让开发者一眼看懂对象本质”,所以包含所有关键字段和类型名;__str__的目标是“让终端用户看得舒服”,所以用易读格式,可省略技术细节。
3. 从零开始构建一个真实可用的OOP系统:以“智能灌溉控制器”为例
3.1 需求解构:剥离业务逻辑与技术实现的模糊地带
假设我们要为农场开发一套智能灌溉系统。硬件已确定:土壤湿度传感器(返回0-100%)、水泵(开关控制)、天气API(提供未来24小时降雨概率)。业务规则如下:
- 当土壤湿度低于30%且未来24小时无降雨(概率<10%)时,启动水泵灌溉10分钟;
- 每次灌溉后,需记录日志(时间、湿度值、是否灌溉);
- 系统需支持手动覆盖自动模式(如农民主动开启/关闭);
- 所有组件必须可独立测试,避免硬件依赖。
初学者常犯的错误是直接开写IrrigationController类,把传感器读取、天气查询、水泵控制、日志记录全塞进去。这样做的后果是:无法在没接传感器的笔记本上测试;一次修改可能影响所有功能;更换天气API供应商时要改遍整个类。真正的OOP设计,第一步是识别抽象边界:
| 边界名称 | 职责 | 为什么独立? |
|---|---|---|
| SensorReader | 读取当前土壤湿度值 | 硬件接口,易出错,需单独mock |
| WeatherService | 查询未来24小时降雨概率 | 外部API,网络不稳定,需重试/缓存 |
| PumpActuator | 控制水泵开关 | 物理操作,有延迟和失败风险 |
| Logger | 记录操作日志 | 可能输出到文件、数据库、远程服务,需灵活切换 |
| IrrigationPolicy | 封装灌溉决策逻辑(湿度阈值、降雨概率阈值、灌溉时长) | 业务规则核心,最易变更 |
注意:这里没有“控制器”类!
IrrigationController将是组合上述组件的协调者,而非功能实现者。这是Python OOP区别于传统OOP的关键——优先组合(Composition),而非继承(Inheritance)。因为继承意味着“是一个”(is-a)关系,而组合意味着“有一个”(has-a)关系。水泵不是灌溉控制器的子类,灌溉控制器“有一个”水泵。
3.2 接口定义:用协议(Protocol)代替抽象基类(ABC)
Python 3.8+ 引入了typing.Protocol,它是实现鸭子类型的正式机制。相比abc.ABC,Protocol更轻量、更灵活,且不强制继承。我们为每个边界定义协议:
from typing import Protocol, Optional from datetime import datetime class SensorReader(Protocol): def read_moisture(self) -> float: """返回0.0-100.0范围的湿度百分比""" ... class WeatherService(Protocol): def get_rain_probability(self) -> float: """返回0.0-100.0范围的未来24小时降雨概率""" ... class PumpActuator(Protocol): def turn_on(self) -> None: """启动水泵""" ... def turn_off(self) -> None: """关闭水泵""" ... class Logger(Protocol): def log(self, message: str, level: str = "INFO") -> None: """记录日志""" ... # 策略协议:允许不同决策算法 class IrrigationPolicy(Protocol): def should_irrigate( self, current_moisture: float, rain_probability: float ) -> bool: """返回是否应灌溉""" ... def get_irrigation_duration(self) -> int: """返回灌溉时长(秒)""" ...现在,任何实现了这些方法的类,无论是否继承自某个基类,都自动成为该协议的“实现者”。例如,一个模拟传感器:
import random class MockSensorReader: def read_moisture(self) -> float: # 模拟真实传感器的波动,返回20-80之间的随机值 return round(random.uniform(20, 80), 1) # 它没有继承任何东西,但完全符合 SensorReader 协议 sensor: SensorReader = MockSensorReader() # 类型检查通过实操心得:Protocol是Python OOP的“柔性契约”。它让你在IDE中获得类型提示,在mypy中获得静态检查,同时保持运行时的鸭子类型自由度。对于外部依赖(传感器、API、数据库),永远先定义Protocol,再写具体实现。这样,你的核心逻辑(如
IrrigationController)只依赖协议,不依赖具体类,测试时只需传入mock实现即可。
3.3 核心协调者:IrrigationController的职责与实现
IrrigationController是系统的“大脑”,但它不做任何具体工作,只做三件事:获取输入、应用策略、触发动作、记录结果。它的构造函数接收所有依赖的协议实例:
from dataclasses import dataclass from typing import Optional @dataclass class IrrigationController: sensor: SensorReader weather: WeatherService pump: PumpActuator logger: Logger policy: IrrigationPolicy # 可选的手动覆盖状态 manual_override: Optional[bool] = None def run_cycle(self) -> None: """执行一次灌溉决策周期""" try: # 1. 获取输入 moisture = self.sensor.read_moisture() rain_prob = self.weather.get_rain_probability() # 2. 应用策略 should_irrigate = self._check_irrigation_needed(moisture, rain_prob) # 3. 执行动作 if should_irrigate: self._activate_pump() self.logger.log( f"Irrigation started. Moisture: {moisture}%, " f"Rain prob: {rain_prob}%", level="INFO" ) else: self.logger.log( f"No irrigation needed. Moisture: {moisture}%, " f"Rain prob: {rain_prob}%", level="DEBUG" ) except Exception as e: self.logger.log(f"Cycle failed: {e}", level="ERROR") raise def _check_irrigation_needed(self, moisture: float, rain_prob: float) -> bool: """检查是否需要灌溉,支持手动覆盖""" if self.manual_override is not None: return self.manual_override return self.policy.should_irrigate(moisture, rain_prob) def _activate_pump(self) -> None: """启动水泵并等待指定时长""" duration = self.policy.get_irrigation_duration() self.pump.turn_on() # 这里应使用异步等待或线程sleep,为简化用time.sleep import time time.sleep(duration / 10) # 模拟10秒真实灌溉(实际用10分钟) self.pump.turn_off()关键设计点解析:
- 依赖注入(Dependency Injection):所有外部依赖(sensor, weather等)都在初始化时传入,而非在方法内创建。这使
IrrigationController完全无状态、可复用、易测试。- 单一职责:
run_cycle只负责流程编排;_check_irrigation_needed只负责决策逻辑;_activate_pump只负责执行。每个方法不超过10行,职责清晰。- 错误处理集中化:所有异常在
run_cycle顶层捕获并记录,避免分散的try-except污染业务逻辑。- 手动覆盖的优雅处理:用
Optional[bool]表示“未设置”状态,比用True/False/None三态更安全,且类型检查明确。
3.4 具体实现与集成:从模拟到真实硬件的无缝切换
现在,我们为每个协议提供具体实现。首先是模拟版本,用于开发和测试:
# 模拟天气服务:固定返回低降雨概率 class MockWeatherService: def get_rain_probability(self) -> float: return 5.0 # 总是5%,确保灌溉触发 # 模拟水泵:只打印日志 class MockPumpActuator: def turn_on(self) -> None: print("💧 Pump turned ON") def turn_off(self) -> None: print("⏹️ Pump turned OFF") # 模拟日志器:打印到控制台 class ConsoleLogger: def log(self, message: str, level: str = "INFO") -> None: print(f"[{level}] {datetime.now().strftime('%H:%M:%S')} - {message}") # 默认灌溉策略 class DefaultIrrigationPolicy: def __init__(self, moisture_threshold: float = 30.0, rain_prob_threshold: float = 10.0, duration_sec: int = 600): self.moisture_threshold = moisture_threshold self.rain_prob_threshold = rain_prob_threshold self.duration_sec = duration_sec def should_irrigate(self, current_moisture: float, rain_probability: float) -> bool: return (current_moisture < self.moisture_threshold and rain_probability < self.rain_prob_threshold) def get_irrigation_duration(self) -> int: return self.duration_sec集成测试只需几行:
# 创建所有依赖 controller = IrrigationController( sensor=MockSensorReader(), weather=MockWeatherService(), pump=MockPumpActuator(), logger=ConsoleLogger(), policy=DefaultIrrigationPolicy() ) # 运行一次 controller.run_cycle() # 输出: # [INFO] 14:22:05 - Irrigation started. Moisture: 25.3%, Rain prob: 5.0% # 💧 Pump turned ON # ⏹️ Pump turned OFF当硬件到位时,只需替换具体实现,核心控制器代码一行不改:
# 真实传感器(使用RPi.GPIO库) class RPiSensorReader: def __init__(self, pin: int): self.pin = pin def read_moisture(self) -> float: # 真实GPIO读取逻辑 import RPi.GPIO as GPIO # ... 读取ADC值并转换为百分比 return 28.7 # 真实天气API(使用requests) class OpenWeatherService: def __init__(self, api_key: str, city_id: str): self.api_key = api_key self.city_id = city_id def get_rain_probability(self) -> float: import requests url = f"http://api.openweathermap.org/data/2.5/forecast?id={self.city_id}&appid={self.api_key}" resp = requests.get(url).json() # 解析未来24小时预报,计算平均降雨概率 return 12.5 # 真实水泵控制(继电器) class RelayPumpActuator: def __init__(self, relay_pin: int): self.relay_pin = relay_pin def turn_on(self) -> None: import RPi.GPIO as GPIO GPIO.output(self.relay_pin, GPIO.HIGH) def turn_off(self) -> None: import RPi.GPIO as GPIO GPIO.output(self.relay_pin, GPIO.LOW)实操心得:这种设计让“硬件即插即用”成为现实。测试阶段用Mock,上线用真实实现,切换成本为零。更重要的是,每个组件都可以独立压测:你可以用
pytest对RPiSensorReader做1000次读取测试,验证其稳定性;可以对OpenWeatherService模拟网络超时,测试重试逻辑;而IrrigationController的单元测试永远只mock协议,不碰任何硬件。这才是OOP在工程实践中的真正价值——可控的复杂度。
4. 高阶技巧与避坑指南:那些只有踩过才知道的Python OOP真相
4.1 继承的“死亡之谷”:什么情况下绝对不要用继承?
继承在Python中远不如在Java中常用,滥用会导致“脆弱基类问题”(Fragile Base Class Problem)。简单说:当你修改父类时,完全无法预测子类会怎样崩溃。我们曾因一个看似无害的修改引发连锁故障:
# 原始父类(工作正常) class BaseService: def __init__(self, timeout: int = 30): self.timeout = timeout def fetch_data(self) -> dict: # 模拟网络请求 return {"status": "ok"} # 子类 class PaymentService(BaseService): def __init__(self, timeout: int = 30, merchant_id: str = ""): super().__init__(timeout) self.merchant_id = merchant_id def process_payment(self) -> bool: data = self.fetch_data() return data["status"] == "ok" # 问题来了:为了增加重试机制,我们修改父类 class BaseService: def __init__(self, timeout: int = 30, max_retries: int = 3): # 新增参数! self.timeout = timeout self.max_retries = max_retries def fetch_data(self) -> dict: # 加入重试逻辑 for _ in range(self.max_retries): try: return {"status": "ok"} except Exception: continue raise RuntimeError("Fetch failed after retries")结果PaymentService的初始化直接报错:TypeError: __init__() takes 2 positional arguments but 3 were given。因为PaymentService.__init__仍调用super().__init__(timeout),但父类现在需要timeout和max_retries两个参数。
避坑方案:
- 用组合替代继承:
PaymentService持有BaseService实例,而非继承它。这样父类修改不影响子类。- 如果必须继承,用
*args, **kwargs兜底:class PaymentService(BaseService): def __init__(self, *args, merchant_id: str = "", **kwargs): super().__init__(*args, **kwargs) # 安全传递所有参数 self.merchant_id = merchant_id- 优先使用Mixin:为特定能力(如日志、缓存)创建小而专注的Mixin类,用多重继承组合,避免大而全的基类。例如:
class LoggingMixin: def log(self, msg): print(f"[{self.__class__.__name__}] {msg}") class PaymentService(LoggingMixin): # 只混入日志能力,不改变核心结构 def process_payment(self): self.log("Starting payment...") # ...
4.2__slots__不是性能银弹,而是内存纪律的宣言
__slots__常被宣传为“提升性能的神器”,但它的核心价值是强制属性声明,防止意外创建新属性。我们曾维护一个高频交易系统,其中Order类被创建数百万次。最初:
class Order: def __init__(self, id: str, symbol: str, price: float, qty: int): self.id = id self.symbol = symbol self.price = price self.qty = qty # 后来业务扩展,有人随手加了 self.fee = 0.0 # 交易手续费 self.status = "pending" # 订单状态问题在于,每个Order实例都携带一个__dict__字典,即使只用4个字段,内存占用也很大。更糟的是,不同开发者添加的字段五花八门(self.tags,self.source,self.retry_count),导致内存碎片化,GC压力剧增。
引入__slots__后:
class Order: __slots__ = ("id", "symbol", "price", "qty", "fee", "status") def __init__(self, id: str, symbol: str, price: float, qty: int): self.id = id self.symbol = symbol self.price = price self.qty = qty self.fee = 0.0 self.status = "pending"效果立竿见影:
- 内存占用减少约40%(每个实例节省一个
__dict__字典的开销); - 尝试
order.new_field = 123会立即报AttributeError,杜绝了随意添加属性的混乱; __slots__还禁用了__weakref__,如需弱引用,必须显式加入__slots__。
注意事项:
__slots__只作用于当前类,子类不会自动继承,需显式声明;- 如果类需要动态添加属性(如
setattr(obj, name, value)),则不能用__slots__;- 对于极少创建的类(如配置类),
__slots__收益微乎其微,反而增加维护成本。
4.3 类方法(@classmethod)与静态方法(@staticmethod)的精准用法
很多教程把@classmethod和@staticmethod混为一谈,其实它们解决完全不同的问题:
@staticmethod:就是一个普通函数,恰好放在类里。它不访问类或实例的任何状态,纯粹是逻辑分组。例如:class StringUtils: @staticmethod def is_valid_email(email: str) -> bool: # 纯字符串验证,不依赖类变量或实例变量 return "@" in email and "." in email.split("@")[-1]@classmethod:第一个参数是cls(类本身),用于创建类的替代构造函数或访问类变量。这是Python OOP中极其重要的模式。例如:from datetime import datetime class Order: STATUS_CHOICES = ("pending", "confirmed", "shipped", "delivered") def __init__(self, id: str, status: str = "pending"): if status not in self.STATUS_CHOICES: raise ValueError(f"Invalid status: {status}") self.id = id self.status = status @classmethod def from_json(cls, json_data: dict) -> 'Order': """从JSON字典创建Order实例""" return cls( id=json_data["order_id"], status=json_data.get("status", "pending") ) @classmethod def create_pending_order(cls, id: str) -> 'Order': """创建一个待处理订单的快捷方式""" return cls(id=id, status="pending") # 使用 order1 = Order.from_json({"order_id": "ORD-001", "status": "confirmed"}) order2 = Order.create_pending_order("ORD-002")
关键区别:
@classmethod的cls参数保证了子类调用时,返回的是子类实例,而非父类实例。如果我们用@staticmethod实现from_json,子类调用时会返回父类实例,破坏了继承关系。这是@classmethod不可替代的价值。
4.4 属性(@property)的黄金法则:何时用,何时不用?
@property让方法调用看起来像属性访问(obj.name而非obj.name()),但它不是免费的。滥用会导致严重问题:
不要用@property做耗时操作:
@property暗示“获取值是廉价的”。如果obj.total_price需要查询数据库或调用API,用户会误以为它是O(1)操作,导致性能瓶颈。不要用@property做副作用操作:
@property应是纯函数式的,不修改对象状态。obj.is_active = True(setter)可以触发邮件通知,但obj.is_active(getter)绝不该发送邮件。用@property封装内部状态,而非暴露字段:这是它的核心价值。例如:
class BankAccount: def __init__(self, balance: float): self._balance = balance # 内部存储,以下划线标记为私有 @property def balance(self) -> float: """只读属性,防止外部直接修改""" return self._balance @balance.setter def balance(self, value: float): """带校验的赋值""" if value < 0: raise ValueError("Balance cannot be negative") self._balance = value @property def is_overdrawn(self) -> bool: """计算属性,基于内部状态""" return self._balance < 0
实操心得:
@property是Python OOP的“访问控制阀”。它让你在不破坏现有API的情况下,随时插入校验、日志、缓存逻辑。比如明天要加余额变动审计,只需在@balance.setter里加一行self._log_audit("balance_set", old_value, new_value),所有调用方代码无需改动。这是OOP封装思想的完美体现。
5. 常见问题与排查技巧实录:来自真实项目的12个血泪教训
5.1 问题速查表:快速定位OOP设计缺陷
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 类的方法越来越多,超过20个 | 职责扩散,违反单一职责原则 | 用pylint --disable=all --enable=too-many-instance-attributes,too-many-public-methods扫描 | 拆分为多个小类,用组合关联;提取通用逻辑到工具函数或Mixin |
| 修改一个类,多个测试失败 | 类间耦合过紧,缺乏接口隔离 | 检查失败测试的mock对象,看是否依赖了不该依赖的具体类 | 为被依赖类定义Protocol,测试中只mock协议;用依赖注入解耦 |
isinstance(obj, SomeClass)到处都是 | 过度依赖类型检查,违背鸭子类型 | 搜索代码库中的isinstance,分析其用途 | 用hasattr(obj, 'method_name')或try/except替代;重构为多态调用 |
| 继承链超过3层 | 过度设计,增加理解成本 | 画出类继承图,标出每层新增的字段和方法 | 用组合替代深层继承;将共用功能提取为独立服务类 |
__init__方法超过10行,参数超过5个 | 构造逻辑复杂,违反构造函数应轻量原则 | 检查__init__中是否有业务逻辑(如连接数据库、加载配置) | 将复杂初始化移到工厂方法或@classmethod中;用dataclass简化简单构造 |
大量使用global或模块级变量 | 状态管理混乱,难以测试 | 搜索global关键字,检查变量用途 | 将状态封装进类实例;用单例模式(谨慎)或依赖注入传递 |
5.2 血泪教训1:__del__不是析构函数,别指望它清理资源
我们曾在一个文件处理器中这样写:
class FileProcessor: def __init__(self, filepath: str): self.file = open(filepath, "r") def __del__(self): self.file.close() # 错误!结果在压力测试中,程序内存暴涨后崩溃。因为__del__的调用时机完全不确定:它依赖垃圾回收器(GC)的运行,而GC可能在程序退出前都不触发。更糟的是,如果对象间有循环引用,__del__可能永远不会被调用。
正确做法:用上下文管理器(
with语句)确保资源释放:
class FileProcessor: def __init__(self, filepath: str): self.filepath = filepath def __enter__(self): self.file = open(self.filepath, "r") return self def __exit__(self, exc_type, exc_val, exc_tb): if hasattr(self, 'file'): self.file.close()