前言
造一辆燃油车,再写一辆电动车。如果两个类各写一遍,品牌、型号、颜色的赋值要重复,启动、行驶、停止三段 print 也要重复。哪天想给所有车统一加个鸣笛,还得挨个类改。重复的代码越多,漏改的地方就越多。
继承处理的就是这类重复。父类写共性,子类在类名后面的括号里写上父类,父类的属性和方法就自动带过来了。class FuelCar(Car) 里,Car 写在括号里,FuelCar 继承 Car。
车与车之间不同的部分才需要落在子类:燃油车补充燃料,电动车充电。这份代码就是按这个思路拆的。
父类写共性
python
class Car:
def __init__(self, brand, model, color, owner):
self.brand = brand # 品牌(公有属性)
self.model = model # 型号(公有属性)
self.color = color # 颜色(公有属性)
self.__owner = owner # 拥有者(私有属性)
def start(self): # 启动
print(f'{self.brand} {self.model} 正在启动...')
def run(self): # 行驶
print(f'{self.__owner}:{self.brand} {self.model} 正在行驶...')
def stop(self): # 停止
print(f'{self.brand} {self.model} 停止行驶...')
def __control_fuel(self): # 私有方法
print(f'{self.brand} {self.model} 正在控制油门...')
def get_owner(self):
return self.__owner
def charge(self):
print(f'{self.brand} {self.model} 正在补充燃料...')
Car 的构造方法收四个参数:品牌、型号、颜色、拥有者。前三者存成公有属性,拥有者存成私有属性 __owner。start、run、stop 描述共有动作,charge 表示补充燃料,__control_fuel 是私有方法,get_owner 是读取拥有者的唯一入口。
这些成员都放在 Car 里,是因为所有车都有:不管烧油还是用电,都要启动、行驶、停止,也都有品牌和型号。共性写在这里,子类一行都不用重复。
子类括号里写父类
python
class FuelCar(Car):
def charge(self):
super().charge()
print(f'{self.brand} {self.model} 正在充电...')
class ElectricCar(Car):
pass
FuelCar 的括号里写了 Car,继承声明就此完成,它只重写了 charge,先调父类实现,再打印自己的充电动作。ElectricCar 括号里同样写 Car,方法体是 pass,不写任何方法也仍然是合法子类,Car 的成员全都能用。
用 FuelCar 建一个实例并调用 charge:
python
c1 = FuelCar("BMW", "X5", "黑色", "张三")
c1.charge()
实际输出是两行:
text
BMW X5 正在补充燃料...
BMW X5 正在充电...
第一行由 Car 的 charge 打印,第二行由 FuelCar 的 charge 打印。两个类都有 charge,最后执行的是子类那一份,这就是方法重写。
子类实例上的属性查找
FuelCar 没有定义构造方法,实例化时却能照样传四个参数,值也全部挂在 c1 上。属性查找沿着继承链向上进行,FuelCar 里找不到,就用 Car 的构造方法完成初始化。
方法遵循同一套规则。c1.start() 可以调用,是因为 Car 里定义了 start;子类实例执行父类方法时,self 仍然是 c1 这个 FuelCar 对象,所以方法体里的 self.brand 取到的是 c1 自己的数据。run 里那句 self.__owner 同理,属性由 Car 的构造方法写进 c1,FuelCar 只是继承了读取它的方法。
继承拿到的是成员的定义和查找路径,实例数据始终属于子类对象本身。
方法重写
重写指子类定义与父类同名的方法,通过子类实例调用时执行子类的版本。FuelCar 的 charge 覆盖了 Car 的 charge,c1.charge() 走的是 FuelCar 那一份。
输出里的"正在补充燃料"来自 FuelCar 内部主动调用的 super().charge()。删掉这一行,结果就只剩"正在充电"。
决定执行哪个版本的是实例的实际类型,跟变量名怎么写无关。把 c1 换成 Car("BMW", "X5", "黑色", "张三"),charge 只会打印补充燃料,因为对象本身就是 Car,没有子类版本可以找。
ElectricCar 的 pass 说明子类可以不写任何方法,继承照样完整生效,构造方法、start、run、stop、charge 全部可用。等充电这件事需要独立语义时,重写才有价值。
super() 与父类名调用
调用父类方法有两种写法:
python
super().charge()
Car.charge(self)
第二种是早期 Python 的写法,必须显式传入 self,否则调用时缺少实例参数。super() 不传参数时会按当前类和实例自动定位父类,等价于 super(FuelCar, self)。
差别在多继承场景下才会显现。super() 沿着 MRO(方法解析顺序)继续往下查找,而 Car.charge(self) 是写死跳到 Car,会跳过 MRO 中排在 Car 之前的类。单继承里两者结果一致,日常写作 super() 更稳妥。
构造方法也有同样的要求。子类自己定义了构造方法时,要调用 super().init(...) 把参数交回父类,否则父类的属性不会被初始化,后续访问会抛 AttributeError。
私有属性在子类里为什么碰不到
__owner 以双下划线开头,Python 会对这类名字做名称改写,在 Car 中实际存成 _Car__owner。子类方法里写 self.__owner,会被改写成 _FuelCar__owner,读到的是一个从未赋过值的属性,直接抛 AttributeError。
get_owner 因此成了必需品。c1.get_owner() 返回"张三",方法定义在 Car 里,执行的是父类代码,读到的正是构造方法当初写进去的那份数据。
双下划线带来的是语言层面的隔离,单下划线只是一种约定。_owner 这类名字只提示"这是内部属性",Python 不会改写它,子类可以直接访问。私有方法 __control_fuel 的处理方式与私有属性一致,子类里同样无法通过 self.__control_fuel() 调到它。
总结
继承把公共的属性和方法放在父类,子类在括号里声明父类后全部拿到;改行为用重写;复用父类逻辑用 super() 往下走;双下划线成员在子类里不可见,只能由父类提供读取入口。
看一段继承写得对不对,就看这四件事能否在代码里对上:子类实例身上有没有父类的属性、同名方法执行的是谁、super() 有没有漏、私有成员有没有被跨类直接访问。