面向对象高级(继承与方法重写)

本文深入探讨Python面向对象中继承机制的本质,验证子类默认继承父类公有方法、方法重写的两种调用方式(super()与显式类名调用),以及私有属性的继承行为。通过FuelCar和ElectricCar子类实例,演示了未重写时自动继承、重写后调用父类逻辑、私有属性__owner不可直接访问但父类方法仍可使用等关键现象,澄清了‘私有属性不被继承’的常见误解。

子类什么都没写,却白捡了父类的方法|Python 面向对象高级(2):继承与方法重写

系列说明:第十一章「面向对象高级」学习笔记第 2 篇。上一篇写了封装(Car 类 + __owner 私有属性 + 脱敏接口),这一篇直接把它当父类来用。


这次要证明的事

继承我早就会写,class 子类(父类): 就完事了。但这一节有个具体的任务,逼我把“继承到底继承了什么”搞清楚。

父类 Car 有一个公共方法 charge(),输出「xxx正在补充燃料」。我要依次验证:

  1. 子类一行代码都不写,能不能直接调 charge()?

  2. 子类把 charge() 重写掉之后,父类那个版本还能不能救回来?

  3. 父类的私有车主名 __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"]

这里有三个细节值得记一下:

  1. pathlib.Path(__file__).with_name(...):用「跟本文件同目录」来定位父类文件,不管你从哪个工作目录运行都能找到。我之前用相对路径,换个工作目录就报 FileNotFoundError。

  2. namespace 里要塞 __name__:exec 执行的是一段源码,它需要一个“模块命名空间”当容器。给一个假名字(fengzhuang_demo)能避免后续踩坑。代价是:执行它会顺带把父类文件里那些演示的 print 全跑一遍,所以运行时开头会先刷出一串封装演示的输出,再接继承演示。

  3. 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 这个名字、在子类作用域里”访问不到。

所以准确的说法是:私有成员不是“没继承”,而是“继承了但子类够不着原名”。 这一字之差我以前一直讲错。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值