Python技术迷

什么是 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 = name

defdisplay_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 = name

defdisplay_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__,这地方少一行,后面报错会绕很远。

继承不是为了把类写得像家谱。它真正有用的地方,是把调用方从一堆类型判断里救出来。父类把入口收住,子类把差异处理掉,这代码后面才好改。