子类什么都没写,却白捡了父类的方法|Python 面向对象高级(2):继承与方法重写
系列说明:第十一章「面向对象高级」学习笔记第 2 篇。上一篇写了封装(Car 类 + __owner 私有属性 + 脱敏接口),这一篇直接把它当父类来用。
这次要证明的事
继承我早就会写,class 子类(父类): 就完事了。但这一节有个具体的任务,逼我把“继承到底继承了什么”搞清楚。
父类 Car 有一个公共方法 charge(),输出「xxx正在补充燃料」。我要依次验证:
-
子类一行代码都不写,能不能直接调
charge()? -
子类把
charge()重写掉之后,父类那个版本还能不能救回来? -
父类的私有车主名
__owner,到底有没有跟着继承下来?
先说一个卡了我半天的问题:父类 import 不进来
Car 定义在上一章的 1.封装.py 里。我原本打算在新文件里这样写:
from 1.封装 import Car # SyntaxError
直接语法报错。原因是这个文件名太特殊了:有点号,还有中文。
Python 的 import 语句里的点号代表“包层级”,所以 1.封装 会被拆解成「找一个叫 1 的包,里面有个模块叫 封装」——它先去猜 1 是个包,第一秒就崩了,根本轮不到中文出问题。
我不想为了 import 去改文件名(这一章所有文件都是“数字 + 点 + 中文名”的统一命名),所以改成按文件路径读源码、直接执行的方式:
import pathlib
from typing import Any
namespace: dict[str, object] = {"__name__": "fengzhuang_demo"}
parent_file = pathlib.Path(__file__).with_name("1.封装.py")
exec(compile(parent_file.read_text(encoding="utf-8"), str(parent_file), "exec"), namespace)
Car: Any = namespace["Car"]
这里有三个细节值得记一下:
-
pathlib.Path(__file__).with_name(...):用「跟本文件同目录」来定位父类文件,不管你从哪个工作目录运行都能找到。我之前用相对路径,换个工作目录就报FileNotFoundError。 -
namespace里要塞__name__:exec执行的是一段源码,它需要一个“模块命名空间”当容器。给一个假名字(fengzhuang_demo)能避免后续踩坑。代价是:执行它会顺带把父类文件里那些演示的 print 全跑一遍,所以运行时开头会先刷出一串封装演示的输出,再接继承演示。 -
Car: Any = namespace["Car"]这里的Any不是偷懒:namespace声明成dict[str, object],取出来的东西检查器只能认定是object。如果我把注解写成Car: type = ...,反而会引发连锁误判——下面class FuelCar(Car):和super().__init__()会被检查器按元类的重载去匹配,凭空冒出一堆新告警。用Any承接,世界清净。
这里踩的教训是:修类型告警时,最危险的不是原来那条告警,而是你为了消它写进去的新代码又带来了第二条。
正式代码
# by tryer07 | Python 面向对象高级(2)继承与方法重写 · 学习笔记
import pathlib
from typing import Any
namespace: dict[str, object] = {"__name__": "fengzhuang_demo"}
parent_file = pathlib.Path(__file__).with_name("1.封装.py")
exec(compile(parent_file.read_text(encoding="utf-8"), str(parent_file), "exec"), namespace)
Car: Any = namespace["Car"]
# 验证一:一个"什么都没写"的空子类
class TempFuelCar(Car):
pass
temp = TempFuelCar("宝马", "X5", "黑色", "张三")
temp.charge() # 类里一个字都没写,却直接调到了父类的方法
# 正式的燃油车子类:重写 charge(),并在重写里回头调用父类版本
class FuelCar(Car):
# 重写 __init__ 时必须先 super().__init__() 初始化父类属性
def __init__(self, brand, model, color, owner, tank_size):
super().__init__(brand, model, color, owner)
self.tank_size = tank_size
@staticmethod
def refuel():
print("去加油站加油")
# 方式一:super().charge() 先执行父类 charge(),再执行子类代码
def charge(self):
super().charge()
print(f"{self.brand}{self.model}正在加油")
# 方式二:Car.charge(self) 显式点名父类方法,必须把 self 传进去
# (同一个类里不能有两个同名 charge,所以另起一个名字单独演示)
def charge_v2(self):
Car.charge(self)
print(f"{self.brand}{self.model}正在加油(方式二:Car.charge(self))")
def get_owner(self):
# 占位重写:演示方法重写会替换掉父类实现
return f"子类重写的脱敏接口(占位,油箱 {self.tank_size} 升)"
# 电车类也重写 charge(),改成电车逻辑
class ElectricCar(Car):
def __init__(self, brand, model, color, owner, battery):
super().__init__(brand, model, color, owner)
self.battery = battery
def charge(self):
print(f"{self.brand}{self.model}正在充电")
bmw = FuelCar("宝马", "X5", "黑色", "张三", 68)
bmw.charge()
bmw.charge_v2()
fuel = FuelCar("奥迪", "A6", "黑色", "外收内放", 58)
electric = ElectricCar("特斯拉", "Model 3", "白色", "外收内放", 60)
print("车主(fuel 重写版):", fuel.get_owner())
print("车主(electric 继承父类原版):", electric.get_owner())
try:
print(getattr(fuel, "__owner"))
except AttributeError as e:
print("【验证】私有属性不会作为子类属性被继承:", e)
print("fuel 是 Car 的实例吗:", isinstance(fuel, Car))
print("ElectricCar 是 Car 的子类吗:", issubclass(ElectricCar, Car))
# 不同子类各调各的 charge()
fuel.charge()
electric.charge()
真实运行结果
---------- 需求4:未重写时,自动继承父类 charge() ----------
宝马X5正在补充燃料
---------- 需求5/6:重写后的 charge()(方式一 super().charge()) ----------
宝马X5正在补充燃料
宝马X5正在加油
---------- 需求6:重写中调用父类的另一种写法(方式二 Car.charge(self)) ----------
宝马X5正在补充燃料
宝马X5正在加油(方式二:Car.charge(self))
车主(fuel 重写版): 子类重写的脱敏接口(占位,油箱 58 升)
车主(electric 继承父类原版): *收内放
【验证】私有属性不会作为子类属性被继承: 'FuelCar' object has no attribute '__owner'
fuel 是 Car 的实例吗: True
ElectricCar 是 Car 的子类吗: True
---------- 需求8:不同子类各自的 charge() ----------
奥迪A6正在补充燃料
奥迪A6正在加油
特斯拉Model 3正在充电
三个结论
一、空子类真的能白捡方法。
TempFuelCar 的类体就一个 pass,调 temp.charge() 却打出了「宝马X5正在补充燃料」。这就是继承最原始的样子:子类不需要重新实现父类已经写好的能力。
二、重写之后,父类版本靠 super() 或点名抢救。
两种写法我都跑了,输出都是「先补充燃料、再加油」:
-
super().charge()—— 不关心父类叫什么,沿继承链往上找。 -
Car.charge(self)—— 显式点名,但必须手动把self传进去。
第二种是我最容易写错的地方。Car.charge(self) 是“通过类拿到未绑定方法,自己把实例塞进去”,写成 Car.charge() 会直接抱怨缺少参数 self。而 super().charge() 里的 super() 已经替我把当前实例传好了,所以不写 self。这两种方式的参数规则是相反的,记混就报错。
三、私有属性不会作为“子类自己的属性”被继承。
fuel 是 FuelCar 的对象,我用 getattr(fuel, "__owner") 去取,报 'FuelCar' object has no attribute '__owner'。
但注意,electric.get_owner() 明明正常打出了 *收内放。这说明父类的方法内部照样能用 self.__owner,属性本身也确实存在于对象上(它被改名成了 _Car__owner),只是“以 __owner 这个名字、在子类作用域里”访问不到。
所以准确的说法是:私有成员不是“没继承”,而是“继承了但子类够不着原名”。 这一字之差我以前一直讲错。
本文深入探讨Python面向对象中继承机制的本质,验证子类默认继承父类公有方法、方法重写的两种调用方式(super()与显式类名调用),以及私有属性的继承行为。通过FuelCar和ElectricCar子类实例,演示了未重写时自动继承、重写后调用父类逻辑、私有属性__owner不可直接访问但父类方法仍可使用等关键现象,澄清了‘私有属性不被继承’的常见误解。
&spm=1001.2101.3001.5002&articleId=168559367&d=1&t=3&u=34b2346bf63d42ccb1e855e25ae2d01c)
1327

被折叠的 条评论
为什么被折叠?



