类与元编程
校招考察频度:☆ 中频(面向对象与进阶拓展) 复习主线:Python 面试在面向对象进阶层面有五大经典考点:日常必用的
property与__slots__性能优化、开箱即用的dataclass数据建模、对象生命周期核心钩子__new__vs__init__、属性访问拦截器__getattr__vs__getattribute__,以及了解级的元类(能说出用途即可,不要求手写)。
1. property:属性即方法
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self): # 读: 像属性一样访问
return 3.14 * self._radius ** 2
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value): # 写: 校验入口
if value <= 0:
raise ValueError("半径必须为正")
self._radius = value- 把方法包装成属性访问,调用方无感知(
c.area不是c.area())。 - setter 是校验和副作用的最佳位置:统一在赋值处拦截非法值。
- 只定义 getter 不定义 setter 就是只读属性。
- 本质是装饰器 + 描述符协议(Descriptor Protocol)。
2. slots:省内存
class Point:
__slots__ = ("x", "y") # 固定属性名
def __init__(self, x, y):
self.x, self.y = x, y- 默认 Python 类自带
__dict__(属性字典),每个实例都包含一个动态哈希表,内存开销大。 __slots__显式声明固定属性,底层用紧凑的 C 数组代替字典存放属性引用,大幅节省内存(在数百万量级对象时节省可达 40%~50%)。- 代价:实例不能随意动态添加未声明的属性(没有
__dict__)、多重继承存在限制。 - 适用:百万级并发数据传输对象(DTO)。常规业务开发无需过度使用。
3. dataclass:现代数据类
from dataclasses import dataclass, field
@dataclass
class User:
id: int
name: str
tags: list[str] = field(default_factory=list) # 可变默认值的正确写法- 自动生成
__init__、__repr__、__eq__,彻底告别重复样板代码。 frozen=True生成不可变数据类(只读且可哈希,可用作字典的 Key)。field(default_factory=...)杜绝共享同一个可变列表/字典的默认参数陷阱。- 业务领域实体优先使用
dataclass;若涉及 HTTP 请求参数严格校验与序列化,换用 Pydantic。
4. new vs init:对象创建全生命周期
在 Python 中,调用 User() 创建对象时,底层经历了 构造(创建实例) 和 初始化(赋予属性) 两个不同阶段:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
# 1. 负责【创建并返回】实例对象(在内存中真正分配空间)
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name=None):
# 2. 负责【初始化】实例属性(拿到 __new__ 返回的 self 进行赋值)
if name:
self.name = name| 维度 | __new__(cls, ...) | __init__(self, ...) |
|---|---|---|
| 角色 | 构造器(Constructor),负责对象分配与诞生 | 初始化器(Initializer),负责属性填充与设置 |
| 方法类型 | 静态方法(首个参数是类对象 cls) | 实例方法(首个参数是实例 self) |
| 返回值 | 必须返回一个实例对象 | 严禁有返回值(必须返回 None) |
| 调用时机 | 先于 __init__ 执行,返回值传递给 __init__ 的 self | 只有当 __new__ 返回当前类的实例时才会被触发 |
| 经典场景 | 单例模式、继承不可变类型(int/str/tuple)定制构造 | 日常 99% 业务对象的属性赋值 |
5. getattr vs getattribute:属性拦截机制
class DynamicProxy:
def __init__(self):
self.real_data = {"host": "127.0.0.1", "port": 8080}
def __getattribute__(self, name):
# 无条件拦截:任何属性访问(包括 real_data)都会先走到这里
# 注意:访问内部属性必须用 super(),否则造成无限递归死循环!
print(f"[Log] 访问属性: {name}")
return super().__getattribute__(name)
def __getattr__(self, name):
# 兜底拦截:只有在常规查找(实例字典、类属性、父类)都找不到该属性时才触发
if name in self.real_data:
return self.real_data[name]
raise AttributeError(f"未找到属性 '{name}'")
p = DynamicProxy()
print(p.host) # 触发 __getattribute__ 打印日志 -> 实例无 host 属性 -> 触发 __getattr__ 返回 "127.0.0.1"__getattribute__:无条件第一优先级。不管属性存不存在都会触发。容易引发无限递归(RecursionError),必须通过super().__getattribute__(name)读取。__getattr__:降级兜底器。只有当常规查找完全失败、即将抛出AttributeError之前才被调用。最适合做 RPC 客户端动态方法路由、ORM 字段动态映射或配置默认值回退。
6. 元类:了解级
元类(metaclass)是“创建类的类”。class 语句在底层执行流程:
- 解释器调用元类(默认
type)。 type.__new__创建类对象(类名、父类元组、类命名空间字典)。type.__init__初始化类对象。
class SingletonMeta(type): # 元类实现单例模式
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]知道什么场景用:在类创建时统一注入行为,如 ORM 的模型自动注册与字段映射(SQLAlchemy / Django Model)、插件自动收集。业务开发中切忌滥用,装饰器与描述符几乎总能更清晰地替代元类。
面试追问
__new__和__init__的区别?__new__是静态方法负责创建并返回对象实例,__init__是实例方法负责属性初始化。单例模式通常在__new__中拦截。__getattr__和__getattribute__什么时候被调用?__getattribute__无条件每次都被调用;__getattr__仅在常规查找找不到属性抛出 AttributeError 之前作为兜底被调用。__slots__为什么能省内存? 禁用了实例自带的__dict__哈希表,用连续定长数组存放属性指针。缺点是无法动态添加额外属性。property解决什么问题? 方法当属性访问,可以在 getter/setter 中加入参数校验与计算逻辑,对外部调用方保持 API 兼容。- 元类是什么?什么时候用? 元类是创建类的类(
type的子类)。常用于大型框架(ORM、RPC)在类被导入和定义时动态扫描字段、生成 SQL 模式。