面向对象编程面试:类与对象、继承与多态、魔法方法、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 方法就行。这就是「鸭子类型」——如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。
魔法方法:让对象更「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}
这样写的好处是,调用方不需要知道内部实现细节,直接用熟悉的语法操作就行。
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,很容易写出意料之外的行为。
类名.__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 定义了「服务接口」的规范。所有微服务必须实现 start、stop、health_check 三个方法。谁要是漏了,启动时就报错,不会等到运行时才发现。
register 方法可以让一个类「假装」是某个抽象基类的子类,而不需要显式继承。这在适配第三方库时非常有用。
总结一下
面向对象编程的这几个知识点,面试时经常被串起来问。比如「请设计一个缓存系统,要求支持多种淘汰策略,用面向对象的方式实现」。这时候你就要用到抽象基类定义接口,用继承实现不同策略,用多态让调用方统一使用。
最后说一句:别把 OOP 当成死记硬背的概念。它是一套组织代码的方法论。你在项目中用得越多,理解就越深。面试官想看到的,不是你会背多少定义,而是你能不能把 OOP 用在实际问题中。