面向对象
OOP 这块的面试题很有规律:实例化过程、MRO、三种方法、鸭子类型、单例,几乎每年都考。这一篇把每个概念背后的"为什么"讲透。
实例化过程:先 __new__ 造,再 __init__ 装
class Foo:
def __new__(cls, *args, **kwargs):
print("1. __new__:造出实例")
instance = super().__new__(cls) # 必须返回实例
return instance
def __init__(self, x):
print("2. __init__:往实例上装属性")
self.x = x
__new__是静态方法,负责创建并返回实例(类被调用时第一个执行)。__init__是实例方法,接收__new__返回的 self,负责初始化属性,不返回任何东西。__new__不返回实例,__init__根本不会被调用。
典型追问:"什么时候需要重写 __new__?"——单例模式、不可变类型子类(如 int 子类)、metaclass 场景。
属性查找顺序
对象 → 类 → 父类(沿 MRO),单向查找。对象上找不到去类上找,类上找不到沿继承链向上找;反过来,通过类名查不到定义在对象上的属性。
class A:
cls_attr = "类属性"
a = A()
a.ins_attr = "实例属性"
a.cls_attr # 能找到:对象 → 类
A.ins_attr # AttributeError:类名查不到对象属性
MRO 与 super()
super() 调的是 MRO(方法解析顺序)里的下一个类,不是"父类"——这是最容易被追问的点。
多继承时新式类用 C3 线性化算法计算 MRO:保证子类在父类前、父类保持声明顺序、同一类只出现一次。查看方式:ClassName.__mro__。
class A:
def hi(self): print("A")
class B(A):
def hi(self):
print("B")
super().hi()
class C(A):
def hi(self):
print("C")
super().hi()
class D(B, C):
def hi(self):
print("D")
super().hi()
D().hi() # D → B → C → A
关键:B 里的 super().hi() 调到的是 C(MRO 中 B 的下一个),不是 B 的父类 A。这就是协作式多继承能各执行一次的原因。经典应用是 __init__ 链:所有类都用 super().__init__(),保证每个父类的初始化逻辑恰好执行一遍。
私有属性:只是改名,不是真私有
__name(双下划线开头)会被 Python 改名成 _类名__name(name mangling),所以外部"直接"访问不到;但知道改后的名字照样能访问:
class Account:
def __init__(self):
self.__balance = 100
a = Account()
a.__balance # AttributeError
a._Account__balance # 100 ← 改名后依然可访问
结论:Python 没有真正的 private,私有是约定 + 改名。单下划线 _x 是"内部使用"的惯例约定,双下划线 __x 触发改名。
三种方法
class Pizza:
count = 0
def __init__(self, size):
self.size = size
Pizza.count += 1
@classmethod
def from_string(cls, s): # 第一个参数是类:常做工厂方法
return cls(int(s.split("寸")[0]))
@staticmethod
def is_valid_size(s): # 无 self/cls:本质是普通函数,逻辑上属于类
return s.replace("寸", "").isdigit()
def describe(self): # 实例方法:第一个参数是实例
return f"{self.size} 寸披萨"
| 方法 | 首参 | 典型用途 |
|---|---|---|
| 实例方法 | self | 操作实例状态,最常见 |
| 类方法 | cls | 工厂方法(多构造入口)、操作/计数类属性 |
| 静态方法 | 无 | 与类相关但不依赖实例或类的工具函数 |
多态与鸭子类型
Python 的多态有两种实现:
- 继承重写:子类重写父类方法,运行时按实际类型调用。
- 鸭子类型(Pythonic 首选):不看类型看行为——"走起来像鸭子、叫起来像鸭子,那它就是鸭子"。任何对象只要实现了
read(),就能传给期待"可读对象"的函数。
class Cat:
def speak(self): return "喵"
class Dog:
def speak(self): return "汪"
def make_speak(animal): # 不检查类型,只要有 speak
print(animal.speak())
强制子类实现用 NotImplementedError 或 abc.ABC:
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def run(self): ...
class Sub(Base):
pass
Sub() # TypeError:实例化时就报错(NotImplementedError 版本要等到调用时才报)
注意区别:raise NotImplementedError 的报错发生在调用时,ABC 的报错发生在实例化时——ABC 更早暴露问题。
反射与元类
反射四函数:getattr(obj, name)、setattr(obj, name, value)、hasattr(obj, name)、delattr(obj, name)。常见于按字符串名字动态调用(如路由分发、插件注册)。
元类是"创建类的类",默认所有类的元类是 type。class Foo: 本质是 Foo = type('Foo', (object,), {...})。用途:ORM 字段注册、接口校验、单例控制。日常 99% 的代码不需要自定义元类——面试能说清"type 的实例是类,类的实例是对象"这层关系即可。
object 与 type 的关系是经典收尾题:type 是所有类的类(包括它自己:type(type) is type);object 是所有类的基类(type 也继承自 object)。两者互为对方体系的最顶端。
单例模式(重写 __new__)
class Config:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "loaded"): # 防 __init__ 重复执行
self.loaded = load_config()
Config() is Config() # True
要点:__new__ 里缓存实例;__init__ 每次都会被调用,要做防重入。线程安全版需要在 __new__ 里加锁。
高频面试题
| 问题 | 答题要点 |
|---|---|
实例化过程?__new__ 与 __init__ 的区别? | 先 __new__ 造(返回实例,静态方法),再 __init__ 装(收 self 挂属性);__new__ 不返回实例则 __init__ 不执行 |
MRO 怎么算?super() 调谁? | 新式类 C3 线性化;super 调 MRO 中的下一个类,不一定是"父类" |
| 私有是真私有吗? | 不是,__x 改名 _类名__x,只是约定 + name mangling |
classmethod / staticmethod / 实例方法的区别? | 首参分别是 cls / 无 / self;类方法做工厂,静态方法是归属类的普通函数 |
| 什么是鸭子类型? | 不看类型看行为,只要实现所需方法就能传入使用;Python 多态的首选方式 |
用 __new__ 写单例? | 类属性缓存实例,__new__ 判断后返回;__init__ 要防重复初始化 |
object 和 type 什么关系? | type 是所有类的类(含自身),object 是所有类的基类,type 也继承 object |
| 强制子类实现方法的两种方式? | raise NotImplementedError(调用时报错)vs abc.ABC + @abstractmethod(实例化时报错,更早) |