什么是 Python 面向对象中的继承特点?
接口返回里突然多了一个 vip_level 字段,老脚本直接炸了。
报错也不绕弯:
AttributeError: 'GuestUser' object has no attribute 'calc_discount'
这种问题我一般不会先去改一堆 if user_type == xxx。先看对象关系。Python 里很多“面向对象写得难受”的代码,最后都是继承没想清楚,子类乱补方法,父类又管太宽。
先看一段比较像业务里的代码。
classAccount:
def__init__(self, uid, name):
self.uid = uid
self.name = namedefdisplay_name(self):
returnf"{self.uid}:{self.name}"
defcan_buy(self):
returnTrue
classMemberAccount(Account):
def__init__(self, uid, name, vip_level):
super().__init__(uid, name)
self.vip_level = vip_level
defcalc_discount(self, amount):
if self.vip_level >= 5:
return amount * 0.85
if self.vip_level >= 2:
return amount * 0.95
return amount
classGuestAccount(Account):
defcan_buy(self):
returnFalse
这里已经能看到 Python 继承的第一个特点:子类天然拥有父类的属性和方法。
MemberAccount 没有写 display_name(),但它能用,因为这个方法在 Account 里。这个设计是为了少写重复代码,不是为了炫技。
member = MemberAccount("u1001", "老周", 5)print(member.display_name())
print(member.can_buy())
print(member.calc_discount(200))
输出大概是:
u1001:老周
True
170.0
这地方别小看 super().__init__(uid, name)。
不少人第一次写继承,子类一上来就自己写 __init__,然后忘了调父类初始化。结果对象跑到后面才报错,字段缺胳膊少腿。
比如这样:
classBadMember(Account):
def__init__(self, uid, name, vip_level):
self.vip_level = vip_level
这代码短,看着也没报错,但一调用父类方法就露馅:
bad = BadMember("u1002", "小陈", 3)
print(bad.display_name())
日志会很直接:
AttributeError: 'BadMember' object has no attribute 'uid'
我看到这种错误,第一眼就会翻子类的 __init__。十有八九是父类没初始化。
继承的第二个特点是:子类可以重写父类方法。
上面的 GuestAccount 就重写了 can_buy()。父类默认可以买,游客账号不允许买。这种覆盖很常见。
guest = GuestAccount("tmp-7", "访客")print(guest.display_name())
print(guest.can_buy())
输出:
tmp-7:访客
False
这里有个小判断:如果只是行为不同,重写方法是干净的;如果到处写 if isinstance(user, GuestAccount),那继承基本就写歪了。
比如这种代码我不太喜欢:
defsubmit_order(account, amount):
if isinstance(account, GuestAccount):
return {"ok": False, "reason": "guest_not_allowed"}return {"ok": True, "pay_amount": amount}
更顺手的写法是让对象自己回答:
defsubmit_order(account, amount):
ifnot account.can_buy():
return {"ok": False, "reason": "account_limited"} pay_amount = amount
if hasattr(account, "calc_discount"):
pay_amount = account.calc_discount(amount)
return {"ok": True, "pay_amount": pay_amount}
不过这里的 hasattr 我也只是临时兜底。真放到项目里,我会把折扣方法放进父类,默认不打折,子类只改自己的规则。
classAccount:
def__init__(self, uid, name):
self.uid = uid
self.name = namedefdisplay_name(self):
returnf"{self.uid}:{self.name}"
defcan_buy(self):
returnTrue
defcalc_discount(self, amount):
return amount
classMemberAccount(Account):
def__init__(self, uid, name, vip_level):
super().__init__(uid, name)
self.vip_level = vip_level
defcalc_discount(self, amount):
if self.vip_level >= 5:
return amount * 0.85
return amount * 0.95
这样 submit_order() 就不用猜对象有没有某个方法。
defsubmit_order(account, amount):
ifnot account.can_buy():
return {"ok": False, "reason": "account_limited"}return {
"ok": True,
"uid": account.uid,
"pay_amount": account.calc_discount(amount)
}
这就是继承里比较舒服的一点:父类给统一入口,子类给不同实现。调用方不用关心具体类型,代码也不容易被 if else 撑爆。
Python 还有一个比较特别的地方:支持多继承。
这东西能用,但我一般比较克制。多继承写顺了很方便,写乱了排查顺序能把人看烦。
比如账号除了会员能力,还想挂一个日志能力:
classAuditMixin:
defwrite_audit(self, action):
print(f"[audit] uid={self.uid}, action={action}")classStaffAccount(Account, AuditMixin):
defcan_buy(self):
returnFalse
defreset_password(self):
self.write_audit("reset_password")
return"reset_done"
调用:
staff = StaffAccount("admin-01", "值班账号")
print(staff.can_buy())
print(staff.reset_password())
输出:
False
[audit] uid=admin-01, action=reset_password
reset_done
这里 AuditMixin 这种类,我一般只放“附加能力”,不要放主业务状态。比如日志、序列化、权限标记还行。你要是把订单金额、库存扣减、支付状态全塞进不同父类里,后面谁先执行、谁覆盖谁,迟早出事。
Python 查找方法有一个顺序,叫 MRO。现场排查多继承问题时,这个东西比概念有用。
print(StaffAccount.__mro__)
会看到类似:
(<class '__main__.StaffAccount'>,
<class '__main__.Account'>,
<class '__main__.AuditMixin'>,
<class 'object'>)
方法查找就是按这个顺序走。子类先找,找不到再找父类,再往后找。多继承下如果两个父类都有同名方法,顺序就很关键。
最后再提一个容易被忽略的点:Python 的继承不是接口继承那么死板,它更偏运行时行为。
你不用提前声明一堆类型,代码能不能跑,很多时候取决于对象有没有对应的方法。这很灵活,也容易写散。
所以我平时写 Python 继承,会留几个规矩:
父类放稳定字段和稳定行为,比如 uid、name、can_buy()、calc_discount()。
子类只改差异点,不要把父类逻辑复制一份再改两行。
多继承只用来挂轻能力,别拿它拼主流程。
super() 该调就调,尤其是 __init__,这地方少一行,后面报错会绕很远。
继承不是为了把类写得像家谱。它真正有用的地方,是把调用方从一堆类型判断里救出来。父类把入口收住,子类把差异处理掉,这代码后面才好改。