面向对象编程面试:类与对象、继承与多态、魔法方法、MRO、抽象基类

面向对象编程,简称 OOP,是 Python 后端面试的必考点。说实话,很多候选人能把概念背得滚瓜烂熟,但一写代码就露馅。我面试过不少人,问「什么是多态」,回答得头头是道。再问「你项目里怎么用的」,就卡住了。

今天咱们把这几个核心知识点彻底捋一遍。从类与对象开始,一路聊到抽象基类。每个点我都会结合项目经验来讲,争取让你面试时能说出「我在实际中是这样用的」。

类与对象:不只是模板和实例

类和对象的关系,说白了就是「图纸和房子」的关系。类定义了一组属性和方法,对象是这些属性和方法的具体实例。

但 Python 的类有个特别的地方——一切皆对象。类本身也是对象,是 type 类的实例。这个特性让 Python 的元编程变得非常灵活。

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def greet(self):
        return f"你好,我是{self.name}"

# 创建对象
user = User("张三", 25)
print(user.greet())  # 你好,我是张三

这里有个小细节:self 不是关键字,只是约定俗成的写法。你写成 this 也能跑,但没人会这么干。我在代码审查时看到过有人用 self 以外的名字,直接打回重写。

面试小技巧: 当被问到「类变量和实例变量的区别」时,别只背定义。可以说「我在项目中用类变量做缓存,所有实例共享一份数据,节省内存」。这样面试官会觉得你有实战经验。

继承与多态:代码复用的艺术

继承解决的是「复用」问题,多态解决的是「替换」问题。两者相辅相成。

举个例子。我做过一个支付系统,有支付宝、微信、银行卡三种支付方式。如果不用继承,每个支付方式写一套独立逻辑,那代码会膨胀到没法维护。

class Payment:
    def pay(self, amount):
        raise NotImplementedError

class Alipay(Payment):
    def pay(self, amount):
        print(f"支付宝支付 {amount} 元")

class WechatPay(Payment):
    def pay(self, amount):
        print(f"微信支付 {amount} 元")

# 多态:统一接口,不同实现
def process_payment(payment_obj, amount):
    payment_obj.pay(amount)

process_payment(Alipay(), 100)   # 支付宝支付 100 元
process_payment(WechatPay(), 200) # 微信支付 200 元

多态的好处是什么?调用方不需要关心具体是哪个支付方式,只要传入的对象有 pay 方法就行。这就是「鸭子类型」——如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。

注意: Python 的多态和 Java 不同。Java 要求显式实现接口,Python 靠的是「协议」和「约定」。我曾经接手过一个项目,子类重写了父类方法但改了参数签名,结果运行时各种报错。所以,重写方法时一定要保持签名一致。

魔法方法:让对象更「Pythonic」

魔法方法,也叫双下划线方法。它们让自定义类能像内置类型一样工作。

我个人最常用的几个魔法方法:

魔法方法 作用 使用场景
__init__ 初始化对象 设置初始状态
__str__ 用户友好的字符串表示 print() 输出
__repr__ 开发者友好的字符串表示 调试、日志
__eq__ 判断相等 对象比较
__lt__ 小于比较 排序
__getitem__ 索引访问 让对象支持 obj[key]

看个实际例子。我写过一个配置类,希望它能像字典一样访问属性:

class Config:
    def __init__(self, data):
        self._data = data
    
    def __getitem__(self, key):
        return self._data.get(key)
    
    def __setitem__(self, key, value):
        self._data[key] = value
    
    def __str__(self):
        return str(self._data)

cfg = Config({"host": "localhost", "port": 8080})
print(cfg["host"])  # localhost
cfg["port"] = 9090
print(cfg)          # {'host': 'localhost', 'port': 9090}

这样写的好处是,调用方不需要知道内部实现细节,直接用熟悉的语法操作就行。

重点: 面试时如果被问到「你用过哪些魔法方法」,别只列名字。挑两三个讲清楚「为什么用」和「怎么用的」。比如「我用 __getattr__ 实现了懒加载属性,只有在第一次访问时才计算值,节省了初始化时间」。

MRO 方法解析顺序:多继承的「交通规则」

多继承是 Python 的特色,也是坑最多的地方。MRO(Method Resolution Order)决定了当多个父类有同名方法时,到底调用哪一个。

Python 3 使用 C3 线性化算法。说白了,就是按照「深度优先,从左到右,保持单调性」的原则,给所有父类排个序。

class A:
    def method(self):
        print("A")

class B(A):
    def method(self):
        print("B")

class C(A):
    def method(self):
        print("C")

class D(B, C):
    pass

d = D()
d.method()  # 输出什么?
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

输出是 B。为什么?因为 MRO 顺序是 D → B → C → A → object。Python 从左到右找,B 排在 C 前面,所以先找到 B 的 method

我曾经在项目中遇到过菱形继承的问题。两个父类都继承自同一个基类,子类又同时继承这两个父类。如果不理解 MRO,很容易写出意料之外的行为。

避坑指南: 我曾经在代码里用多继承实现 mixin 功能,结果因为 MRO 顺序搞错,调用了错误的父类方法。排查了整整一个下午。后来我养成了一个习惯:凡是涉及多继承,先用 类名.__mro__ 打印一下顺序,确认后再写逻辑。

抽象基类:定义「契约」

抽象基类(ABC)的作用是定义接口规范。它强制子类必须实现某些方法,否则实例化时会报错。

Python 的 abc 模块提供了这个能力。用 @abstractmethod 装饰器标记抽象方法。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass
    
    @abstractmethod
    def move(self):
        pass

class Dog(Animal):
    def speak(self):
        return "汪汪"
    
    def move(self):
        return "跑"

# 下面这行会报错,因为没有实现所有抽象方法
# class Cat(Animal):
#     pass

dog = Dog()
print(dog.speak())  # 汪汪

抽象基类在大型项目中特别有用。我参与过一个微服务框架的开发,我们用 ABC 定义了「服务接口」的规范。所有微服务必须实现 startstophealth_check 三个方法。谁要是漏了,启动时就报错,不会等到运行时才发现。

进阶用法: 抽象基类还可以注册虚拟子类。用 register 方法可以让一个类「假装」是某个抽象基类的子类,而不需要显式继承。这在适配第三方库时非常有用。

总结一下

面向对象编程的这几个知识点,面试时经常被串起来问。比如「请设计一个缓存系统,要求支持多种淘汰策略,用面向对象的方式实现」。这时候你就要用到抽象基类定义接口,用继承实现不同策略,用多态让调用方统一使用。

最后说一句:别把 OOP 当成死记硬背的概念。它是一套组织代码的方法论。你在项目中用得越多,理解就越深。面试官想看到的,不是你会背多少定义,而是你能不能把 OOP 用在实际问题中。