
1. 面向对象不是语法糖是帮你管理关系的工程手段先讲个真实场景。我早年在甲方做后台系统时带过一个刚转 Python 的同事。他学完类、对象、继承之后写完一个订单模块能写 2000 行——因为每个类里都在重复写日志函数、校验函数、格式化函数。我问他为什么不把这些重复逻辑抽出来他说抽出来要怎么共享又不想每个人都去实例化一个工具类。这就是典型的知道概念但没吃透。这一篇要讲的东西不是让你背定义。你以后写爬虫、写数据分析脚本、写 Web 后端真正决定代码能不能撑到三五个版本迭代的不是算法多精妙而是你用的对象关系是否合理。多继承、多态、鸭子类型、组合、私有成员——这五件事在 Python 里其实是一套完整的关系管理体系一个对象能继承什么、能替换什么、能组合什么、哪些脏东西不能给别人看各有各的边界。把这五个东西串起来理解零基础的人能直接写出结构靠谱的类有基础但一直在抄代码的人也能突然看懂很多开源框架为什么那么设计。我会尽量用能跑、能改、能踩坑的代码说话每个部分都配了我在实际项目中遇到过的翻车案例。你最好把代码敲一遍不是复制粘贴是手敲——手敲的过程才会逼你注意到细节比如super()带不带参数、双下划线改名到底改成了什么这些细节才是面试和实战的分水岭。2. 多继承从钻石问题到 MRO 线性化2.1 多继承的基本写法和第一个隐患多继承其实很简单一个类名后面括号里放多个父类就行class A: def say(self): print(A says hello) class B: def say(self): print(B says hello) class C(A, B): pass c C() c.say() # 输出什么答案是A says hello。原因很直接Python 在方法查找时按照C(A, B)里的声明顺序从左往右找第一个有say的类。A 有就用 A 的B 的say根本没机会被调用。看到这里如果你心里想的是这有什么难的从左往右不就完了那恭喜你接下来你马上要踩坑了——因为一旦父类之间还有继承关系事情就完全不是从左往右这么简单了。2.2 MROsuper() 不是父类而是下一个协作类先看一个经典例子class Base: def hello(self): print(Base.hello) print(Base done) class Left(Base): def hello(self): print(Left.hello) super().hello() print(Left done) class Right(Base): def hello(self): print(Right.hello) super().hello() print(Right done) class Child(Left, Right): pass child Child() child.hello()大部分人拿着 Java 思维会觉得输出应该是Left.hello → Base.hello → Base done → Left done因为super()不就找父类吗Left 的父类是 Base所以super().hello()就调 Base 的。但实际输出是Left.hello Right.hello Base.hello Base done Right done Left done这就有意思了。Left 里的super().hello()调到的不是 Base而是 Right。这就是 Python 多继承最核心的概念super() 调用的是 MRO方法解析顺序Method Resolution Order列表里的下一个类而不是自己的父类。MRO 是怎么算出来的Python 用 C3 线性化算法它保证两点每个类在 MRO 里只出现一次且子类永远排在父类前面。保持所有父类自身的继承顺序。想要看具体 MRO直接打出来print(Child.mro()) # [class __main__.Child, class __main__.Left, class __main__.Right, class __main__.Base, class object]所以child.hello()的执行路径就是Child 里没有 hello → Left 的 hello → super() 调到 MRO 中 Left 的下一个类 Right 的 hello → Right 里 super() 调到 Base 的 hello → 打印 Base done → 回到 Right done → 回到 Left done。整个调用链是一枚洋葱从外往里一层层剥再从里往外一层层弹回来。这带来一个非常重要的实用结论如果你在多继承链路里用 super()整个继承体系里的每个相关类都必须按照同样的风格写要么都用 super()要么都不用什么千万别混着来。你想想Left 里如果用Base.hello(self)硬编码调 Base那 Right 这段就直接被跳过了代码的协作性当场归零。2.3 钻石继承一个真实的重名冲突案例我在做数据处理框架时遇到过这种问题。框架允许用户定义多个数据处理器类结构大致是这样class DataSource: def connect(self): print(连接数据源) class LoggingMixin: def log(self, msg): print(f[LOG] {msg}) class FileSource(DataSource, LoggingMixin): def load(self): self.connect() self.log(正在加载文件) return file data class DbSource(DataSource, LoggingMixin): def load(self): self.connect() self.log(正在连接数据库) return db data class CacheSource(FileSource, DbSource): def load(self): # 想做个智能缓存先查内存没有再调用父类加载 print(尝试走缓存) return super().load()这段代码看着没问题实际上CacheSource.mro()会是这样CacheSource → FileSource → DbSource → DataSource → LoggingMixin → object注意三个点DataSource在 MRO 里只出现一次。这是 C3 算法在钻石继承里最漂亮的设计——公用的基类不会被执行两次。如果connect()在两边都被调用也不会重复触发连接数据源。LoggingMixin跑到了DataSource后面。这意味着虽然DbSource在括号里先写了DataSource但实际上DataSource先执行MixIn 最后执行。如果你在中间某个类里用super()调用顺序有问题日志可能会打不出来或者连接数据源跑到日志后面整个初始化任务顺序被打乱。这里我给你一个五年实战总结的多继承设计经验用 MixIn 类并且 MixIn 类里尽量不要定义__init__也尽量少用super()只提供纯功能方法。把 MixIn 放在继承列表的最后面。这样即使 MRO 再复杂MixIn 也不会参与你核心类的初始化顺序最多是被调用时打印日志、做点统计之类的事。当然更稳妥的选择是前面提到的组合。什么时候必须多继承我的判断标准很简单如果这两个父类之间的共性——也就是它俩都是同一个东西的部分——不值得维护一个共同基类那就不值得用多继承。这个标准能挡住绝大多数花拳绣腿的多继承设计。3. 多态与鸭子类型不看你是什么只看你会什么3.1 Python 多态和 Java/C 多态的本质差异很多面向对象教材讲多态先画 UML 图再写一个抽象基类Animal然后两个子类猫和狗各实现一个speak()调用的时候传不同子类对象表现出不同的行为。这种讲法没错但它会给你一个错觉——多态必须有继承关系才能实现。在 Java 里确实如此。Animal animal new Cat()这行代码左边的类型是Animal右边是Cat类型系统保证Cat是Animal的子类你才能用父类型变量去引用子类对象。如果缺了继承关系编译期直接报错。但 Python 是动态语言它不搞编译期检查。看这一段class Dog: def speak(self): return 汪汪 class Cat: def speak(self): return 喵喵 class Bird: def speak(self): return 叽叽 def make_sound(animal): print(animal.speak()) make_sound(Dog()) # 汪汪 make_sound(Cat()) # 喵喵 make_sound(Bird()) # 叽叽注意Dog、Cat、Bird三个类之间没有任何继承关系。make_sound函数唯一的要求是传进来的对象有一个speak()方法。至于它是不是某种动物根本不关心。这就是 Python 的多态多态不是靠继承体系保证的而是靠对象实际具备的方法能力决定的。继承在 Python 里更像是顺便复用代码和统一接口的辅助工具而不是多态的前提。3.2 鸭子类型不检查你是什么只看你会什么鸭子类型这句话来自一首诗如果它走起来像鸭子、叫起来像鸭子那它就是鸭子。放到 Python 里意思就是不为类型专门写 if 判断而是直接调用你需要的方法。它能不能走路、能不能叫是类自己的责任。写一个小例子class TempSensor: def read(self): return 26.5 class TokenFetcher: def read(self): return Bearer xyz123 def get_data(device): data device.read() # 不管 device 是温度传感器还是令牌管理器只要它 read() 能返回数据就行 return data print(get_data(TempSensor())) print(get_data(TokenFetcher()))这个能力在标准库里到处都是。比如with open()、with request它们都支持上下文管理器协议只要实现了__enter__和__exit__你就能用with语句。len()函数只要对象实现了__len__哪怕你自己写一个统计学样本量的类也能直接len(obj)。所以你在写函数时应该养成一个习惯不要写if isinstance(obj, SomeClass):这种前置判断除非你确实需要它做某种特殊处理。你先假设对象有这个能力然后直接调用或者 try 一下。配合上协议式设计代码的可扩展性会强非常多——下次加一个新类只要它有对应的方法老函数一行都不用改。3.3 isinstance 的用法边界与项目级约束我不卖关子直接说结论鸭子类型不是让你完全禁用isinstance而是让你分清场景。什么时候可以用在入口处设置类型防线。比如外部传入 JSON 字符串或字典你需要在入口识别到底传的是哪种结构防止把dict当str处理。区分两类行为差异很大的对象。比如一个对象是零配置、直接能跑的测试桩另一个对象是真实环境依赖数据库的类这两者的协议完全不同用 isinstance 分开处理是可以接受的。处理第三方库的类型限制比如sqlite3数据库连接你想判断它到底是内存数据库还是文件数据库直接看类型或看属性都行isinstance 更稳妥。什么时候不要用为了安全在工作函数里层层判断。我在代码评审里看过这样的函数一个发送通知的函数进来先判断是不是用户对象、是不是管理对象、是不是 VIP 对象然后一路 branch。这种代码其实就是把多态要做的事手写了一遍。正确做法是你给这些对象定义一个共同的notify()方法直接调用或者用前面的组合方案去统一处理。判断一个对象是不是某个具体类而非是否具备某种能力的场景。当你的判断条件应该抽象成有没有.send()或有没有.items()时isinstance 就是错的方向。一句话总结我的使用经验鸭子类型定义的是函数的默认宽容态度而 isinstance 是特殊入口和特殊分支时才用的显式检查工具。让默认路径保持干净让特殊情况在边缘显式处理两者并行不悖。4. 类的组合has-a 关系比继承稳得多的工程解4.1 has-a 与 is-a 如何选择继承表达的是 is-a 关系学生是人、轿车是车。组合表达的是 has-a 关系汽车拥有发动机、班级拥有学生列表。在实际业务里is-a 关系往往比想象中少得多。你仔细想一下一个OrderService是一个什么它不是一个Logger不是一个Database更不是一个UserRepository但它可能会用到 Logger、用到 Database。所以正确写法是class OrderService: def __init__(self, repository, logger): self.repository repository self.logger logger def create_order(self, data): self.logger.log(开始创建订单) # 业务逻辑省略 return self.repository.save(data)这段代码里OrderService 和 repository、logger 之间是组合关系。好处非常明显想换掉数据库传一个新的 repository 对象进来就行想换日志实现传一个别的 logger 对象进来就行。类与类之间不用通过继承强行绑定。用一句话教你怎么判断问自己B 是不是A 的一种如果是用继承如果只是B 是 A 要用到的一个部件用组合。NotificationManager 是一种什么——它什么都不是它是拿着各种发送渠道一起干活的东西那自然就是组合。4.2 一个组合的例子订单系统重构假设你要做一个订单通知系统最初需求只有邮件通知class EmailSender: def send(self, message): print(f发送邮件: {message}) class OrderNotifier: def __init__(self): self.sender EmailSender() def notify_order_created(self, order_id): self.sender.send(f订单 {order_id} 已创建)两周后需求变了要加短信通知。如果你用的是继承思路大概率会写出class OrderNotifierWithSms(OrderNotifier)这种东西。组合提供了更干净的升级路径class SmsSender: def send(self, message): print(f发送短信: {message}) notifier OrderNotifier(SmsSender()) notifier.notify_order_created(1001)注意OrderNotifier的__init__需要改造让它接收一个 senderclass OrderNotifier: def __init__(self, sender): self.sender sender def notify_order_created(self, order_id): self.sender.send(f订单 {order_id} 已创建)已经不需要继承只需要把依赖通过__init__传进去。随着业务变得复杂你还能进一步做组合的层层嵌套一个通知任务对象里组合多个 sender然后 scheduler 里组合通知任务。整个系统像积木一样每块都是独立的类彼此靠构造函数连接加需求时不做伤筋动骨的改动。再举一个实际到不能再实际的例子写爬虫时一个Spider类往往需要下载网页、解析 HTML、清洗数据、去重。你可以用继承写四个抽象层但更好的做法是class Spider: def __init__(self, downloader, parser, deduplicator): self.downloader downloader self.parser parser self.deduplicator deduplicator def run(self, url): html self.downloader.fetch(url) items self.parser.parse(html) return self.deduplicator.deduplicate(items)测试的时候你可以传一个不会真的发请求的假 downloader生产环境再换真的下载组件。后端工程师看到这个写法应该马上有共鸣——这不就是依赖注入嘛。是的组合在 Python 中最典型的工程形态就是依赖注入它让代码可替换、可测试、可扩展这三样东西是继承咬着牙也做不到的。5. 私有属性和私有方法Python 的君子协定5.1 单下划线 _private 与双下划线 __private 的区别Python 和其他语言不同它没有真正意义上的 private 关键字。它靠的是两个习惯约定单下划线_name约定俗成地表示这是类内部使用的成员外部不要直接访问。这只是君子协定外部想访问照样能访问class User: def __init__(self, name): self._name name user User(张三) print(user._name) # 输出张三 不推荐但代码不会报错双下划线__namePython 解释器会玩一个名字重整的把戏它会让这种属性名在类内部被改写成_类名__name。看这个class BankAccount: def __init__(self, money): self.__balance money account BankAccount(1000) # print(account.__balance) # AttributeError 直接报错 print(account._BankAccount__balance) # 输出1000 —— 用改名后的名字还是能访问所以双下划线不是真正的不能访问而是我不想让你轻易访问。它主要防止的是意外覆盖和被子类误用。5.2 名称重整的真相想访问还是能访问名称重整是 Python 私有机制里最容易造成困惑的地方。我画一个真实面试题常问的场景class Parent: def __init__(self): self.__secret parent secret def get_secret(self): return self.__secret class Child(Parent): def __init__(self): super().__init__() self.__secret child secret # 注意这创建的是一个新属性 child Child() print(child.get_secret()) # 输出 parent secret print(child.__dict__) # {_Parent__secret: parent secret, _Child__secret: child secret}这个例子很能说明问题子类里的__secret并没有覆盖父类的__secret因为它们在编译期准确说是类定义体执行时就已经被改成了两个不同的名字——_Parent__secret和_Child__secret。这既保护了父类的内部数据也制造了一个新手最容易懵的特性双下划线开头的属性和方法在不同类里面其实互不相干。再强调一个小坑名字重整只发生在类定义体内部。你在类外面定义一个名字是不会被自动改写的。这个特性很多人不知道但面试问双下划线和单下划线的区别是什么时能详细说出双下划线会发生名称重整改名规则是_ClassName__attr主要目标是防止子类意外覆盖就能拿不错的印象分。5.3 实战中该怎么用私有成员我的建议如下对外只暴露必要的方法和属性。能用单下划线就不要用双下划线。大部分业务代码里单下划线足够表达内部使用这个意图。双下划线用于真正不想被子类覆盖的关键内部逻辑。比如框架里有个__build_request方法你不想让用户随手重写它那就写成双下划线。不要花大力气设计绝对访问不到的私有属性。Python 的设计哲学是大家都是成年人要自重。有人想访问你的私有属性他通过_Class__attr一定能访问到你的任务只是明确告诉他不该碰。用property控制读写权限往往比直接暴露属性更优雅class Temperature: def __init__(self, celsius): self._celsius celsius property def fahrenheit(self): return self._celsius * 9 / 5 32 fahrenheit.setter def fahrenheit(self, value): self._celsius (value - 32) * 5 / 9fahrenheit对外看起来是普通属性但底层有计算逻辑。这种组合私有属性 property 公共接口的模式比直接暴露__celsius再写一堆 getter/setter 更符合 Python 的习惯。还有一个经验调试时如果属性存在但报 AttributeError先检查一下你是不是漏了双下划线。我曾经花二十分钟找明明在__init__里定义了self.__name外部调用时却 AttributeError结果就是名字重整导致外部必须用_ClassName__name。这类问题一旦知道原因之后就是一眼的事。6. 把五样东西串起来一个完整的小项目实战到了这一步概念都能看懂但综合运用才是真正的分水岭。我写一个稍微综合的小例子主题是员工薪资和通知系统。场景系统要对不同类型的员工进行月度计薪然后发送工资通知。员工有正式员工、外包员工、实习生。计薪逻辑不同通知渠道也可能不同。这个例子会把多态、鸭子类型、组合、私有成员全部用上。先定义员工基类class Employee: def __init__(self, name, base_salary): self._name name self._base_salary base_salary def calculate_salary(self): # 子类负责实现 raise NotImplementedError def get_name(self): return self._name接下来定义三种员工它们用多态的方式各自实现calculate_salary()class RegularEmployee(Employee): def __init__(self, name, base_salary, bonus): super().__init__(name, base_salary) self._bonus bonus def calculate_salary(self): return self._base_salary self._bonus class OutsourcedEmployee(Employee): def __init__(self, name, base_salary, service_fee): super().__init__(name, base_salary) self._service_fee service_fee def calculate_salary(self): return self._base_salary - self._service_fee class InternEmployee(Employee): def __init__(self, name, daily_rate, days): super().__init__(name, 0) self._daily_rate daily_rate self._days days def calculate_salary(self): return self._daily_rate * self._days注意这里InternEmployee和RegularEmployee没有任何继承于彼此的硬性关系但它们都是Employee的子类所以在一个薪资计算函数里可以被统一调用def generate_payroll(employees): payroll [] for emp in employees: payroll.append((emp.get_name(), emp.calculate_salary())) return payroll现在加入通知功能。这里我要体现组合而不是让员工类自己拥有通知渠道。同时利用鸭子类型——通知器不需要知道具体员工类型是什么只要它有*args能接收名字和薪资就行class EmailNotifier: def __init__(self): self.__sent_count 0 def __send_email(self, address, content): print(f发送邮件到 {address}: {content}) self.__sent_count 1 def notify(self, name, salary): self.__send_email(f{name}company.com, f{name} 本月的薪资是 {salary}) def get_sent_count(self): return self.__sent_count class SmsNotifier: def notify(self, name, salary): print(f发送短信给 {name}: 薪资 {salary} 已到账) class NotificationManager: def __init__(self, notifier): self.notifier notifier # 组合管理器持有一个通知渠道 def send_all(self, payroll): for name, salary in payroll: self.notifier.notify(name, salary) employees [ RegularEmployee(王小明, 8000, 3000), OutsourcedEmployee(李华, 6000, 1000), InternEmployee(赵新, 200, 22), ] payroll generate_payroll(employees) print(payroll) email_manager NotificationManager(EmailNotifier()) sms_manager NotificationManager(SmsNotifier()) email_manager.send_all(payroll) sms_manager.send_all(payroll)这个综合例子里值得深度回味的点有三个第一generate_payroll用的是多态它给了一个员工列表但不知道列表里具体是什么员工。第二NotificationManager不关心你给的是邮箱还是短信只要传入的 notifier 有notify方法就行这是鸭子类型和组合的完美配合。第三EmailNotifier里的__send_email和__sent_count是私有成员外部无法直接访问只能通过get_sent_count()来拿统计数量——这加了一层保护不至于被外部代码把计数器改坏。再往前跨一步如果未来新增一个微信通知渠道你只需要再写一个类提供一个notify方法NotificationManager本身完全不用动。这就是稳定架构的味道核心流程稳定扩展点开放细节实现自由替换。7. 我的实操体会与约定最后讲几条真正从项目里泡出来的经验。第一不要为了展示技术去用多继承。每次我想多继承时先问自己这个类是不是真的同时是两种东西如果不是那就组合。真实项目里多继承出现频率远低于组合但每一个合法的多继承都出现在框架设计层或者 MixIn 场景这大概率是经验值决定的。第二私有属性最大的价值其实不是保密而是划定维护边界。现在很多团队都强调代码评审你在类里标_private或者__private本质上是在告诉其他开发者这块你别碰改坏了算我的你要改走我的公共方法出了问题我负责。 这套边界管理能大幅减少多人协作时的互相踩踏。第三鸭子类型一定要配合文档或类型注解来用。如果团队项目里到处是协议式调用却不写任何说明别人看代码很容易一头雾水。我通常会在类 docstring 里单独写一段这个类实现了 Notifier 协议提供 notify(name, salary) 方法。 这样既保住了 Python 的动态灵活性又让下一个接手的程序员有迹可循。第四多态和组合是重构的好朋友。发现代码里有一大堆if type(obj) A ... elif type(obj) B ...的循环逻辑时最快的重构方向就是把每个分支的行为提取成类方法然后循环体里只留一行调用。这招我用了十年几乎没有失手过。最后再分享一个调试小技巧当继承链和 MRO 让你晕头转向时直接打印.mro()不要靠猜。这个列表永远是最权威的执行顺序说明。而当你不知道一个对象有哪些私有属性时直接打印obj.__dict__所有经过名字重整的属性都会以_类名__属性名的形态暴露出来。带着这个视角再回去看那五个概念函数你会发现它们并不是五个孤立的知识点而是一套完整的、互相配合的对象关系工具。理解了这层关系Python 赠与你的就不光是语法而是一种怎么把复杂业务拆清楚的能力。