查理·芒格:
人一辈子做对两件事就可以很富有:寻找什么是有效的,重复它;发现什么是无效的,避免它。
上一章我们讲了继承。你已经知道,子类可以复用父类的能力,也可以在父类基础上继续扩展自己的属性和方法。那再往前走一步,就会碰到一个非常关键的思想:多态。
很多人第一次听到“多态”这两个字,会本能觉得抽象、难、像考试题。其实你把它翻译成人话,它一点都不玄。
多态,说白了就是:
同样的调用方式,落到不同对象身上,可以表现出不同的行为。
注意这句话,非常关键。 不是“方法名不同,行为不同”,那不叫多态。 真正的多态是:
方法名可以一样 调用方式也一样 但对象不同,执行结果不同
这背后体现出来的,就是“统一接口”的设计思想。
一、先别急着背定义,先看现实世界
比如你按下遥控器上的“开机”按钮。
对电视来说,是开电视。 对空调来说,是开空调。 对投影仪来说,是开投影仪。
你按的动作是不是同一个?是。 接口是不是统一的?也是。 但不同设备接到这个动作后,执行出来的结果不一样。
这就是非常典型的多态味道。
再比如你点一个播放器上的“播放”按钮。
播放音乐软件,会播歌。 播放视频软件,会放视频。 语音播放器,可能会播录音。
外部操作是统一的。 内部行为由具体对象自己决定。
这就是多态最接地气的理解方式。
二、为什么多态会在继承后讲
因为多态通常离不开一个前提:
先有一组“同一类”的对象, 它们共享一个相对统一的接口, 然后每个子类再给出自己的实现。
也就是说,多态和继承经常配合出现。
比如:
Animal 是父类Dog、Cat、Bird 是子类
父类里先约定一个方法:
speak()
然后各个子类自己去实现:
狗 speak() 是汪汪汪 猫 speak() 是喵喵喵 鸟 speak() 是啾啾啾
这时候,你外部只需要统一调用:
对象.speak()
至于到底发出什么声音,由具体对象自己决定。
这,就是多态。
三、先看一个最典型的多态例子
classAnimal:defspeak(self): print('动物发出了声音')classDog(Animal):defspeak(self): print('汪汪汪')classCat(Animal):defspeak(self): print('喵喵喵')classBird(Animal):defspeak(self): print('啾啾啾')
创建对象:
dog1 = Dog()cat1 = Cat()bird1 = Bird()dog1.speak()cat1.speak()bird1.speak()
输出结果:
汪汪汪喵喵喵啾啾啾
这段代码特别重要,你一定要看懂。
表面上看,调用方式完全一样:
对象.speak()
但不同对象调用后,结果不同。
这正是多态的核心。
同一个接口,多个不同实现。
四、这里的关键,不是对象不同,而是“调用方式统一”
这一点要单独强调。
因为如果你写成这样:
dog1.bark()cat1.meow()bird1.chirp()
虽然结果也不同,但这不能很好体现多态。
为什么?
因为外部使用方式不统一。 你每处理一种对象,都得记不同的方法名。
而多态更强调的是:
外部只认统一动作 内部由对象自己决定怎么完成
所以多态真正厉害的地方,不是“不同对象会做不同事”,而是:
不同对象可以在同一个接口下,做出不同反应。
这会让程序结构特别灵活。
五、统一接口,到底有什么价值
这是比定义更重要的问题。
统一接口最大的价值,是让外部代码更简单、更通用。
比如你有一个函数,想让传进来的动物都发出声音。
如果没有多态,你可能得这样写:
if isinstance(animal, Dog): animal.bark()elif isinstance(animal, Cat): animal.meow()elif isinstance(animal, Bird): animal.chirp()
这种写法的问题非常明显。
第一,分支越来越多。 第二,每增加一种新动物,都得回来改这里。 第三,外部代码被迫知道太多细节。
但如果有多态,你就可以写成这样:
defmake_animal_speak(animal): animal.speak()
然后:
make_animal_speak(Dog())make_animal_speak(Cat())make_animal_speak(Bird())
这就清爽太多了。
因为外部只要求:
你得会 speak()
至于你具体怎么叫,我不管。 这就是统一接口的强大之处。
六、这其实是在把“变化”关进对象内部
你可以这样理解多态的本质价值:
外部不需要知道每个对象的具体细节。 外部只需要相信,它们都支持同一个接口。
变化放在哪里?
放在每个对象自己内部。
比如:
狗自己决定怎么 speak()猫自己决定怎么 speak()鸟自己决定怎么 speak()
而外部世界只负责统一调用。
这其实和前面讲封装有点一脉相承:
封装,是隐藏内部细节。 多态,是在隐藏细节的基础上,进一步让外部只面对统一接口。
所以你会发现,面向对象这些概念不是零散的,它们其实是互相咬合的。
七、再看一个更像业务开发的例子
假设你在做一个支付系统。
你可能会有:
支付宝支付 微信支付 银行卡支付
如果没有多态,外部逻辑可能会变成这样:
if pay_type == 'alipay': 调用支付宝逻辑elif pay_type == 'wechat': 调用微信逻辑elif pay_type == 'bank': 调用银行卡逻辑
一开始看还行。 但只要支付方式一多,这种写法就会越来越臃肿。
更自然的思路是先定义一个统一接口:
pay()
然后不同支付类各自实现自己的 pay()。
例如:
classPayment:defpay(self, amount): print('执行支付')classAlipay(Payment):defpay(self, amount): print(f'使用支付宝支付了 {amount} 元')classWechatPay(Payment):defpay(self, amount): print(f'使用微信支付了 {amount} 元')classBankPay(Payment):defpay(self, amount): print(f'使用银行卡支付了 {amount} 元')
外部只需要写:
defprocess_payment(payment, amount): payment.pay(amount)
调用时:
process_payment(Alipay(), 100)process_payment(WechatPay(), 200)process_payment(BankPay(), 300)
输出结果:
使用支付宝支付了 100 元使用微信支付了 200 元使用银行卡支付了 300 元
你看,这就是多态在真实业务里非常常见的样子。
八、多态不是“看上去高级”,而是“减少判断分支”
这一点特别值得你抓住。
很多新手第一次学多态时,会把注意力放在“哇,一个方法名能这样用,好神奇”。 其实真正值钱的不是“神奇”,而是它带来的代码结构优化。
多态最大的实际价值之一,就是:
把大量 if...elif...else 分支,转化成统一调用。
这会带来两个直接好处:
代码更短 扩展更方便
以后你新增一个 ApplePay,只要新写一个类并实现 pay(),外部 process_payment() 函数几乎不用改。
这就是多态真正的工程价值。
九、父类在多态里扮演什么角色
父类很多时候像是在定义一份“统一规范”。
比如:
动物类约定都有 speak()支付类约定都有 pay()图形类约定都有 draw()员工类约定都有 work()
注意,父类不一定要把这个方法写得很复杂。 有时候它甚至只是先留一个通用接口位置。
真正关键的是:
子类都遵守这个接口规则,并各自给出自己的实现。
所以你可以把父类在多态里的作用理解成:
先把“大家都该会什么”定下来。
子类再去回答:
我具体怎么做。
十、方法重写,是多态成立的关键基础之一
前一章我们已经接触过方法重写。
比如:
classAnimal:defspeak(self): print('动物发出了声音')classDog(Animal):defspeak(self): print('汪汪汪')
这里的 Dog.speak(),就是在重写父类的 speak()。
为什么多态离不开它?
因为如果所有子类都直接照搬父类实现,那统一接口就统一了,但“不同表现”这件事就没发生。 多态的灵魂恰恰是:
接口相同,实现不同。
而实现不同,通常就是靠子类去重写父类方法。
所以你可以简单记:
继承提供共同接口 重写提供不同实现 两者合起来,就很容易形成多态
十一、再看一个图形绘制的例子
这个例子也非常经典。
classShape:defdraw(self): print('开始绘图')classCircle(Shape):defdraw(self): print('正在画一个圆形')classRectangle(Shape):defdraw(self): print('正在画一个矩形')classTriangle(Shape):defdraw(self): print('正在画一个三角形')
统一调用:
defdraw_shape(shape): shape.draw()draw_shape(Circle())draw_shape(Rectangle())draw_shape(Triangle())
输出结果:
正在画一个圆形正在画一个矩形正在画一个三角形
你会发现,draw_shape() 函数根本不需要知道传进来的是圆、矩形还是三角形。 它只需要知道:
你这个对象支持 draw() 就行。
这就是多态在“提高通用性”这件事上的威力。
十二、多态让“写代码的人”和“扩展代码的人”更容易协作
这是一个稍微偏工程化、但非常真实的价值。
比如有一个公共函数:
defprocess_payment(payment, amount): payment.pay(amount)
这个函数的作者,其实不用知道未来所有支付方式。 他只需要规定:
支付对象必须有 pay(amount) 方法
以后别人要扩展新的支付方式,只要写一个新类并遵守这个接口就行。 原来的公共函数不需要改。
这意味着:
写框架的人负责定义接口 扩展功能的人负责实现接口
这就是多态特别适合大型项目的原因之一。
十三、你可以把多态理解成“同一命令,不同响应”
这个翻译非常好用。
比如:
speak()pay()draw()work()
这些都像是统一命令。
而不同对象收到命令后的响应,可以不一样。
狗收到 speak(),汪汪汪 猫收到 speak(),喵喵喵 鸟收到 speak(),啾啾啾
支付宝收到 pay(),走支付宝流程 微信收到 pay(),走微信流程 银行卡收到 pay(),走银行卡流程
只要你脑子里一直抓住“统一命令,不同响应”,多态就不会再抽象。
十四、Python 里的多态,并不一定强依赖父类
这里先给你一个稍微进阶一点、但很重要的认知。
在很多语言里,多态常常更强调严格的继承体系。 但 Python 本身非常灵活,它更强调“你有没有这个方法”,而不一定强制要求你必须来自某个父类。
比如:
classDog:defspeak(self): print('汪汪汪')classCat:defspeak(self): print('喵喵喵')
然后:
defmake_it_speak(obj): obj.speak()
调用:
make_it_speak(Dog())make_it_speak(Cat())
照样能跑。
因为 Python 更关注:
这个对象有没有 speak() 方法
而不是死抠:
它是不是某个父类的子类
这种思想以后你会听到一个词,叫“鸭子类型”。不过这一章先不展开,只要你先知道:
Python 里的多态,既可以借助继承,也可以借助统一方法约定。
但在入门阶段,先从“继承加方法重写”这条线理解最稳。
十五、统一接口,为什么能让未来扩展更容易
因为外部代码写一次就够了。
比如:
defdraw_shape(shape): shape.draw()
以后你新增 Hexagon、Star、HeartShape,只要这些类都实现了 draw(),这个函数完全不用改。
这就是非常典型的“对扩展开放,对修改关闭”的味道。你现在不用背这个术语,但可以先感受一下:
好的设计,不是每来一个新需求就回头大改旧代码,而是新对象接进来就能用。
多态恰恰特别擅长做到这一点。
十六、一个常见误区:以为方法名一样就自动算多态
不完全对。
如果只是方法名一样,但外部根本没有在统一接口下使用它们,那多态价值并没有真正体现出来。
例如:
dog.speak()cat.speak()bird.speak()
这当然已经有多态味道了。 但真正更体现设计思想的,是把它们放进统一流程里,比如:
animals = [Dog(), Cat(), Bird()]for animal in animals: animal.speak()
或者:
defmake_all_speak(animal_list):for animal in animal_list: animal.speak()
这时候多态的价值才真正冒出来。 因为你开始利用统一接口处理一组不同对象了。
所以别只盯着“方法名一样”,更要看“是不是在统一调用”。
十七、多态和封装、继承的关系,可以这样串起来
这一章其实很适合帮你把前面三章再串一下。
封装,让每个对象把自己的数据和实现细节管起来。 继承,让子类复用父类的公共能力。 多态,让外部在统一接口下使用不同对象,而不必关心每个对象内部到底怎么实现。
你会发现,这三者其实是在一层层往上搭。
先让对象自己成型 再让一类对象建立父子结构 最后让外部统一调用它们
这就是面向对象越来越有味道的地方。
十八、一个完整综合案例
下面我们做一个稍微完整一点的小例子。
classEmployee:defwork(self): print('员工正在工作')classProgrammer(Employee):defwork(self): print('程序员正在写代码')classDesigner(Employee):defwork(self): print('设计师正在画设计稿')classProductManager(Employee):defwork(self): print('产品经理正在写需求文档')
统一调用函数:
defstart_work(employee): employee.work()
调用:
start_work(Programmer())start_work(Designer())start_work(ProductManager())
输出结果:
程序员正在写代码设计师正在画设计稿产品经理正在写需求文档
这个例子特别像真实团队协作。
外部系统只管“安排开始工作”。 具体怎么工作,由不同角色自己决定。
这就是多态在业务建模里的典型应用。
十九、什么时候你应该开始想到多态
当你发现下面这种味道时,就可以开始想到它了:
有多个对象 它们本质上属于同一类体系 它们都应该支持某个统一动作 但具体实现方式各不相同
这时候,多态往往就是很合适的设计思路。
比如:
不同支付方式都要支付 不同图形都要绘制 不同员工都要工作 不同消息渠道都要发送 不同交通工具都要移动
只要这种“统一动作,不同实现”的模式出现,多态就很可能能派上用场。
二十、本章小练习
你可以做两个非常适合巩固的小练习。
第一个练习,定义一个 Shape 父类。 然后定义 Circle、Rectangle、Triangle 三个子类。 都重写 draw() 方法。 再写一个函数 show_shape(shape),统一调用 shape.draw()。
第二个练习,定义一个 MessageSender 父类。 然后定义 EmailSender、SmsSender、WechatSender 三个子类。 都重写 send() 方法。 再写一个函数 send_message(sender),统一调用 sender.send()。
这两个练习,一个练图形,一个练消息发送,都是多态特别经典的使用场景。
二十一、本章总结
这一章最重要的,不是记住“多态”这个术语,而是吃透它背后的设计思想。
多态的核心是:同样的调用方式,落到不同对象身上,可以表现出不同的行为。 它强调的是统一接口,而不是统一实现。 继承提供共同结构,方法重写提供不同实现,这两者经常配合形成多态。 多态最大的实际价值,是让外部代码更通用、更简洁,也更方便扩展。 它特别适合处理那种“统一动作,不同响应”的场景。 在 Python 里,多态既可以借助继承,也可以借助对象是否真的支持某个方法。
到这里,封装、继承、多态这面向对象三大核心思想,你已经都摸到门了。 下一章我们继续进入这一阶段的收束内容:080|面向对象综合案例:用类重构一个真实小项目。