Skip to main content
Version: Next

面向对象

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 的多态有两种实现:

  1. 继承重写:子类重写父类方法,运行时按实际类型调用。
  2. 鸭子类型(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(实例化时报错,更早)