
15 - 继承与多态上一章讲了类和对象的基本概念。这章讲面向对象的另外两个核心特性继承和多态。继承是什么继承就是子类继承父类的属性和方法。比如猫和狗都是动物它们共享一些特征有名字、会吃但也有各自特有的行为猫会喵喵叫狗会汪汪叫。classAnimal:def__init__(self,name):self.namenamedefeat(self,food):print(f{self.name}在吃{food})classDog(Animal):defbark(self):print(f{self.name}汪汪)classCat(Animal):defmeow(self):print(f{self.name}喵~)Dog和Cat是子类Animal是父类也叫基类。子类自动拥有父类的所有方法和属性dogDog(旺财)dog.eat(骨头)# 继承自 Animaldog.bark()# Dog 自己的方法catCat(小花)cat.eat(鱼)# 继承自 Animalcat.meow()# Cat 自己的方法方法重写子类可以覆盖父类的方法给出自己的实现classAnimal:def__init__(self,name):self.namenamedefspeak(self):print(f{self.name}发出了声音)classDog(Animal):defspeak(self):print(f{self.name}汪汪)classCat(Animal):defspeak(self):print(f{self.name}喵~)dogDog(旺财)dog.speak()# 旺财汪汪调的是 Dog 的 speak不是 Animal 的super()有时候你不想完全覆盖父类的方法而是在父类的基础上加点东西。用super()调用父类的方法classAnimal:def__init__(self,name,age):self.namename self.ageageclassDog(Animal):def__init__(self,name,age,breed):super().__init__(name,age)# 调用父类的 __init__self.breedbreed# 子类特有的属性dogDog(旺财,3,金毛)print(dog.name,dog.age,dog.breed)# 旺财 3 金毛不用super()的话你得手动调用classDog(Animal):def__init__(self,name,age,breed):Animal.__init__(self,name,age)# 也能用但 super() 更好self.breedbreedsuper()在多继承时优势更明显它会自动处理方法解析顺序所以养成用super()的习惯。多态多态就是同一种操作不同的对象有不同的行为defmake_speak(animal):animal.speak()# 不关心具体是什么动物只要能 speakdogDog(旺财)catCat(小花)make_speak(dog)# 旺财汪汪make_speak(cat)# 小花喵~make_speak函数不需要知道传入的是 Dog 还是 Cat只要对象有speak方法就行。这就是鸭子类型——如果一个东西走起来像鸭子、叫起来像鸭子那它就是鸭子。Python 天然支持鸭子类型不像 Java 需要定义接口。# 任何东西只要有 speak 方法就能用classRobot:defspeak(self):print(你好我是机器人)make_speak(Robot())# 你好我是机器人多重继承Python 支持一个子类继承多个父类classFlyable:deffly(self):print(f{self.name}在飞)classSwimmable:defswim(self):print(f{self.name}在游泳)classDuck(Animal,Flyable,Swimmable):passduckDuck(唐老鸭)duck.eat(面包)# 来自 Animalduck.fly()# 来自 Flyableduck.swim()# 来自 SwimmableMRO方法解析顺序多重继承时如果多个父类有同名方法Python 按什么顺序找用 C3 线性化算法可以通过.__mro__查看print(Duck.__mro__)# (class Duck, class Animal, class Flyable, class Swimmable, class object)Python 会按这个顺序找方法。Duck 自己没有就找 AnimalAnimal 没有就找 Flyable…多重继承用好了很强大用不好就是灾难特别是菱形继承问题。一般建议优先用组合而非继承多重继承主要用于 Mixin 模式下面讲Mixin 模式Mixin 是一种设计模式——定义一些附加功能的类通过多重继承混入到其他类中classJsonMixin:给类添加 JSON 序列化能力defto_json(self):importjsonreturnjson.dumps(self.__dict__,ensure_asciiFalse)classLogMixin:给类添加日志能力deflog(self,message):fromdatetimeimportdatetimeprint(f[{datetime.now():%H:%M:%S}]{self.__class__.__name__}:{message})classUser(JsonMixin,LogMixin):def__init__(self,name,age):self.namename self.ageage userUser(小明,25)print(user.to_json())# {name: 小明, age: 25}user.log(用户创建成功)Mixin 的特点只做一件事单一职责不单独使用总是被其他类继承命名通常以Mixin结尾这种方式比写一个巨大的基类灵活得多功能可以随意组合。抽象基类ABC有时候你想定义一个接口——规定子类必须实现某些方法但不提供具体实现fromabcimportABC,abstractmethodclassShape(ABC):abstractmethoddefarea(self):passabstractmethoddefperimeter(self):passclassCircle(Shape):def__init__(self,radius):self.radiusradiusdefarea(self):return3.14159*self.radius**2defperimeter(self):return2*3.14159*self.radiusclassRectangle(Shape):def__init__(self,width,height):self.widthwidth self.heightheightdefarea(self):returnself.width*self.heightdefperimeter(self):return2*(self.widthself.height)# shape Shape() # TypeError! 不能直接实例化抽象类# 必须通过子类使用cCircle(5)print(c.area())# 78.53975如果有人写了一个 Shape 的子类但忘了实现area或perimeter创建实例时会直接报错。这能在开发阶段帮你发现问题。classmethod 和 staticmethodclassmethod类方法第一个参数是类本身通常叫clsclassUser:def__init__(self,name,age):self.namename self.ageageclassmethoddeffrom_string(cls,s):从字符串创建用户name,ages.split(,)returncls(name.strip(),int(age.strip()))classmethoddeffrom_dict(cls,d):从字典创建用户returncls(d[name],d[age])# 多种创建方式user1User(小明,25)user2User.from_string(小明, 25)user3User.from_dict({name:小明,age:25})from_string和from_dict这种模式叫工厂方法——提供多种创建对象的方式。staticmethod静态方法不需要self或cls参数classMathUtils:staticmethoddefis_prime(n):ifn2:returnFalseforiinrange(2,int(n**0.5)1):ifn%i0:returnFalsereturnTruestaticmethoddefgcd(a,b):whileb:a,bb,a%breturnaprint(MathUtils.is_prime(17))# Trueprint(MathUtils.gcd(12,8))# 4静态方法本质上就是一个普通函数只是逻辑上属于这个类。说实话很多时候你直接写个模块级函数也行不一定非要用 staticmethod。dataclassPython 3.7如果你写一个类主要是为了存数据dataclass能帮你省掉大量样板代码fromdataclassesimportdataclassdataclassclassPoint:x:floaty:floatlabel:str原点就这几行Python 自动帮你生成了__init__、__repr__、__eq__等方法p1Point(3,4)p2Point(3,4)p3Point(0,0,原点)print(p1)# Point(x3, y4, label原点)print(p1p2)# Trueprint(p3)# Point(x0, y0, label原点)不用 dataclass 的话你得手写这些classPoint:def__init__(self,x,y,label原点):self.xx self.yy self.labellabeldef__repr__(self):returnfPoint(x{self.x}, y{self.y}, label{self.label})def__eq__(self,other):ifnotisinstance(other,Point):returnFalsereturnself.xother.xandself.yother.yandself.labelother.label代码多了好几倍。dataclass 在处理数据传输对象DTO、配置类等场景特别方便。组合 vs 继承继承不是万能的。有时候组合把一个对象作为另一个对象的属性比继承更合适。# 继承不太好classCarWithEngine(Car):def__init__(self):super().__init__()self.engineEngine()# 组合更好classCar:def__init__(self,engine,wheels):self.engineengine self.wheelswheels什么时候用继承什么时候用组合继承子类是父类的一种is-a关系。狗是动物。组合对象有某个部件has-a关系。车有引擎。如果你犹豫不决优先用组合。组合更灵活继承容易造成类层次过深。Enum 枚举枚举Enumeration用来定义一组命名的常量。比用魔法数字或字符串靠谱得多。为什么需要枚举不用枚举的话你可能这样写# 用字符串表示状态——容易写错defhandle_status(status):ifstatuspendding:# 写错了应该是 pending...elifstatusactive:...用枚举fromenumimportEnumclassStatus(Enum):PENDINGpendingACTIVEactiveCLOSEDcloseddefhandle_status(status:Status):ifstatusStatus.PENDING:print(等待中)elifstatusStatus.ACTIVE:print(活跃)elifstatusStatus.CLOSED:print(已关闭)handle_status(Status.ACTIVE)# 活跃# handle_status(activ) # 不会发生因为 IDE 会提示拼写错误好处很明显拼错了编辑器会提示不会出现运行时才发现的 bug。基本用法fromenumimportEnumclassColor(Enum):RED1GREEN2BLUE3# 访问print(Color.RED)# Color.REDprint(Color.RED.name)# REDprint(Color.RED.value)# 1# 从值获取枚举print(Color(2))# Color.GREEN# 从名字获取枚举print(Color[RED])# Color.RED# 遍历forcolorinColor:print(color.name,color.value)# 比较print(Color.REDColor.RED)# Trueprint(Color.REDColor.GREEN)# False实用技巧fromenumimportEnum,autoclassPermission(Enum):READauto()# 自动分配值1WRITEauto()# 2DELETEauto()# 3# 用作字典的键role_permissions{admin:{Permission.READ,Permission.WRITE,Permission.DELETE},editor:{Permission.READ,Permission.WRITE},viewer:{Permission.READ},}# 检查权限defcan_edit(role):returnPermission.WRITEinrole_permissions.get(role,set())print(can_edit(admin))# Trueprint(can_edit(viewer))# False枚举成员是单例的所以可以用is比较statusStatus.ACTIVEifstatusisStatus.ACTIVE:# 比 更快print(是活跃状态)本章小结继承让子类自动获得父类的属性和方法super()在子类中调用父类的方法多态同一种操作在不同对象上有不同行为Python 用鸭子类型实现多态不需要接口Mixin 通过多重继承添加附加功能ABC定义抽象基类强制子类实现某些方法classmethod接收类作为参数适合做工厂方法dataclass自动生成数据类的样板代码组合优于继承“has-a” 用组合“is-a” 用继承Enum定义命名常量集合比魔法字符串更安全面试题Q1super()的作用是什么不用它行吗点击查看答案super()用于在子类中调用父类的方法最常见于__init__中classDog(Animal):def__init__(self,name,breed):super().__init__(name)# 调用父类的 __init__self.breedbreed不用super()也行可以直接写Animal.__init__(self, name)。但super()更好因为在多继承时正确处理 MRO方法解析顺序代码不硬编码父类名改了父类不用改 super 调用Q2什么是鸭子类型点击查看答案鸭子类型是 Python 的多态实现方式——不关心对象的类型只关心对象有没有需要的方法/属性。“如果它走起来像鸭子、叫起来像鸭子那它就是鸭子。”defquack(thing):thing.speak()# 不管 thing 是什么类型只要有 speak 方法就行跟 Java/C 的静态多态不同Python 不需要继承同一个父类或实现同一个接口。这降低了耦合度但也意味着类型错误要到运行时才能发现。Q3classmethod和staticmethod的区别点击查看答案classmethodstaticmethod第一个参数cls类本身无特殊参数能访问类属性能不能除非硬编码类名典型用途工厂方法、类级别操作工具函数、纯计算子类继承cls指向子类没有区别classBase:classmethoddefcreate(cls):returncls()# 子类调用时返回子类实例staticmethoddefhelper():return工具方法classmethod更常用特别是做工厂方法时。staticmethod可以用普通函数替代。Q4什么时候用继承什么时候用组合点击查看答案继承用于 “is-a” 关系Dogis anAnimal组合用于 “has-a” 关系Carhas anEngine优先用组合的原因灵活性高可以在运行时替换组件耦合度低修改一个组件不影响其他避免继承层次过深深层继承难以理解和维护避免菱形继承问题反例如果为了让子类获得一个方法而继承一个不相关的类这就是错误的继承。应该把那个功能封装成独立对象通过组合使用。