「学习笔记」Python 进阶(二):抽象设计 — 构建优雅系统
文章目录
此篇为 Python 进阶的第二篇——抽象设计篇,聚焦如何用 Python 的面向对象与抽象机制构建可扩展、可维护的系统。当你能写出能跑的程序后,本篇教你写出"能长久的程序"。
第一章:Python 面向对象 — 你需要知道的进阶细节
Python 基础入门 第五章已覆盖封装/继承/多态基础概念。这里补几个实际开发中绕不开的进阶知识点。
1.1 MRO 的核心算法:C3 线性化
当子类继承多个父类时,Python 使用 C3 线性化算法 确定方法解析顺序(Method Resolution Order)。它的三大核心约束如下:
- 子类优先:子类永远在它的父类之前被检查。
- 保持声明顺序:如果类 D(B, C) 声明了多个父类,那么 B 及其祖先永远排在 C 及其祖先的前面。
- 单调性与无重复:每个类在 MRO 链中只会出现一次,且必须保证所有父类的局部优先级约束一致。如果继承关系存在无法调和的冲突,Python 会在定义类时直接抛出 TypeError: Cannot create a consistent method resolution order (MRO)。
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C): # 继承顺序决定优先级
pass
d = D()
print(d.method()) # "B" — 因为 B 在 C 之前
print(D.__mro__) # (D, B, C, A, object) — 完整的查找链
print(D.mro()) # 同上,列表形式
关键规则:
- 子类优先于父类
- 父类顺序按声明顺序(从左到右)
- 所有父类必须满足 C3 线性化,否则
TypeError
1.2 super() 的本质
super()实际上调用的是 MRO 链中的“下一个类”,而不是绝对的父类。
class A:
def method(self):
print("A")
class B(A):
def method(self):
super().method() # 等价于 super(B, self).method()
print("B")
class C(A):
def method(self):
super().method() # 等价于 super(C, self).method()
print("C")
class D(B, C):
def method(self):
super().method()
print("D")
D().method() # 输出: (A C B D)
# super() 按 MRO 顺序调用下一个类的同名方法,不是只调用直接父类!
# MRO 链的顺序是:D → B → C → A → object,所以A是最先打印的
实际场景:任何使用多重继承的框架(ORM 模型混入、GUI 组件混入)都需要理解
super()的实际解析顺序。
1.3 __slots__ — 内存优化
class NormalClass:
def __init__(self):
self.name = None
self.value = None
class OptimizedClass:
__slots__ = ('name', 'value') # 只允许这两个属性
def __init__(self):
self.name = None
self.value = None
# __slots__ 的效果:
# 1. 禁止动态添加新属性 → obj.new_attr = 1 会报错 AttributeError
# 2. 节省内存(不用 __dict__ 字典存储属性)
# 3. 属性访问略快
1.4 描述符 — 属性的底层机制
@property 的底层实现就是描述符:
class ValidatedString:
"""自定义描述符:对属性赋值进行校验"""
def __init__(self, min_length=1):
self.min_length = min_length
self.data = {}
def __get__(self, obj, objtype=None):
return self.data.get(id(obj), "")
def __set__(self, obj, value):
if len(value) < self.min_length:
raise ValueError(f"String too short (min {self.min_length})")
self.data[id(obj)] = value
class User:
name = ValidatedString(min_length=2) # 类级别描述符
user = User()
user.name = "Tom" # 触发 __set__,长度 >= 2,通过
# user.name = "X" # ValueError!
第二章:抽象基类 ABC — 定义"必须遵守的契约"
Python 没有 Java 的 interface 关键字,但通过 abc 模块实现抽象基类。
实际场景:所有需要统一接口的场景——LLM 提供商(OpenAI / DeepSeek / 本地模型)、数据源连接器(MySQL / API / 文件)、插件系统——都适合用 ABC 定义契约。
2.1 定义抽象基类
from abc import ABC, abstractmethod
class LLMProvider(ABC):
@abstractmethod
def chat(self, messages: list[dict]) -> str:
"""子类必须实现聊天接口"""
...
@abstractmethod
def embed(self, texts: list[str]) -> list[list[float]]:
"""子类必须实现向量化接口"""
...
# 普通方法,子类可直接继承
def validate_config(self):
return True
# 抽象类不能实例化
# provider = LLMProvider() # TypeError!
# 必须实现所有抽象方法
class OpenAIProvider(LLMProvider):
def chat(self, messages):
return "Response from OpenAI"
def embed(self, texts):
return [[0.1, 0.2] for _ in texts]
2.2 抽象属性与抽象类方法
from abc import ABC, abstractmethod
class Plugin(ABC):
@property
@abstractmethod
def name(self) -> str:
"""子类必须提供此属性"""
...
@classmethod
@abstractmethod
def from_config(cls, config: dict) -> "Plugin":
"""工厂方法"""
...
class MyPlugin(Plugin):
@property
def name(self) -> str:
return "MyPlugin"
@classmethod
def from_config(cls, config: dict):
return cls()
2.3 实战:Embedding 模型接口
这是 ABC 最经典的应用场景——定义"向量化"这一抽象能力,让不同提供商各自实现:
from abc import ABC
class Base(ABC):
"""所有 Embedding 模型的基类"""
def __init__(self, key, model_name, **kwargs):
pass
def encode(self, texts: list) -> list:
"""编码文本列表为向量"""
raise NotImplementedError("Please implement encode method!")
def encode_queries(self, text: str) -> list:
"""编码查询文本为向量"""
raise NotImplementedError("Please implement encode method!")
class OpenAIEmbed(Base):
def encode(self, texts: list) -> list:
resp = self.client.embeddings.create(
input=texts, model=self.model_name
)
return [d.embedding for d in resp.data]
def encode_queries(self, text: str) -> list:
return self.encode([text])[0]
2.4 ABC vs NotImplementedError
# 方式一:ABC(推荐用于接口定义)
class Interface(ABC):
@abstractmethod
def method(self): ...
# 方式二:NotImplementedError(轻量级,不阻止实例化)
class BaseClass:
def method(self):
raise NotImplementedError("Subclasses must implement")
# 区别:
# ABC + @abstractmethod → 实例化时就会报错(更早暴露)
# NotImplementedError → 只有调用时才会报错(更晚暴露)
第三章:Protocol(协议) — 鸭子类型的"类型护照"
Protocol(Python 3.8+)定义结构化子类型:只要对象有相应方法就匹配,不需要显式继承。这是"鸭子类型"的类型提示版。
它强调能力而非继承:只要一个对象实现了 Protocol 中定义的方法,它就自动满足该 Protocol,无需显式继承。
实际场景:为 Agent 系统的工具调用定义接口时,不强制使用者继承特定基类——只要实现了
tool_call方法,类型检查器就认可。这对于需要对接第三方 SDK 的场景特别有用。
3.1 Protocol 基本用法
Protocol 不需要实现者显式继承——只要对象"走了路"(有 walk 方法),它就是 Walkable。这让第三方代码无需修改就能适配你的接口。
from typing import Protocol, Any
class ToolCallSession(Protocol):
"""任何有 tool_call 方法的对象都匹配此 Protocol"""
def tool_call(self, name: str, arguments: dict[str, Any]) -> str:
...
# 不继承任何 Protocol,但结构匹配
class MySession:
def tool_call(self, name: str, arguments: dict[str, Any]) -> str:
return f"Called {name} with {arguments}"
# 类型检查器认为 MySession 匹配 ToolCallSession
def use_session(session: ToolCallSession) -> str:
return session.tool_call("search", {"query": "test"})
result = use_session(MySession()) # 类型检查通过!
3.2 Protocol vs ABC 选择指南
| 场景 | 推荐 | 原因 |
|---|---|---|
| 你拥有整个继承体系,想强制规范 | ABC | 更显式,实例化时就能发现遗漏 |
| 第三方代码,无法修改继承链 | Protocol | 不侵入现有代码 |
需要运行时检查(isinstance) |
ABC | Protocol 的 isinstance 需要 @runtime_checkable |
| Library/框架设计 | Protocol | 更灵活,用户无需继承 |
3.3 Runtime Checkable
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
print(isinstance(Circle(), Drawable)) # True(需要 @runtime_checkable)
第四章:dataclass — 告别手写样板代码
装饰器 @dataclass 会自动生成 __init__、__repr__、__eq__ 等方法,从而大幅减少冗余的样板代码,让代码更简洁、可读性更强。类似 Java 的 Lombok @Data 或 Kotlin 的 data class。
实际场景:配置文件对象、API 请求/响应模型、向量数据、消息体——任何主要用来"装数据"的类都应该用 dataclass,减少样板代码。
4.1 基本用法
只需在类上添加 @dataclass 装饰器,并加上类型注解,即可自动生成构造函数和友好的字符串表示:
from dataclasses import dataclass, field
@dataclass
class User:
name: str # 必填字段
age: int = 0 # 带默认值
tags: list = field(default_factory=list) # 可变默认值必须用 field
user = User(name="Tom", age=25)
print(user) # User(name='Tom', age=25, tags=[])
print(user == User(name="Tom", age=25, tags=[])) # True
4.2 使用 field() 精细控制字段
当需要为可变对象(如 list、dict)设置默认值,或者需要控制字段是否参与初始化、比较时,必须使用 field() 函数:
from dataclasses import dataclass, field
@dataclass
class Team:
name: str
members: list = field(default_factory=list) # 避免多个实例共享同一个列表
_id: int = field(repr=False, compare=False) # 隐藏内部ID,不参与打印和相等比较
4.3 装饰器全局参数配置
@dataclass 装饰器本身接受多个参数,用于控制整个类的行为:
order=True:自动生成__lt__、__le__、__gt__、__ge__方法,支持对象间的排序比较。默认按字段顺序逐个对比,可通过field(compare=False)排除特定字段。frozen=True:将实例变为“不可变”(类似namedtuple)。任何对字段的赋值操作都会触发FrozenInstanceError,非常适合用作字典的 key 或放入 set 中。eq=False:不自动生成__eq__方法,所有实例默认不相等。
@dataclass(frozen=True)
class ExternalAccess:
external_user_emails: set[str]
is_public: bool = False
access = ExternalAccess({"a@b.com"}, True)
# access.is_public = False # FrozenInstanceError!
4.4 初始化后验证 (__post_init__)
如果需要在自动生成 __init__ 之后,执行一些数据校验、类型转换或关联字段的计算,可以定义 __post_init__ 方法。它会在初始化完成后立即执行,且能访问所有已初始化的字段:
@dataclass
class Person:
name: str
age: int
def __post_init__(self):
if self.age < 0:
raise ValueError("年龄不能为负数")
4.5 常见陷阱与注意事项
- 可变默认值陷阱:千万不要直接写
tags: list = [],这会导致所有实例共享同一个列表对象。必须使用field(default_factory=list)。 - 继承时的字段顺序:
dataclass支持继承,但子类中带有默认值的字段必须排在无默认值字段的后面,否则会引发TypeError。 - 适用场景:
dataclass最适合“纯数据”结构(如配置管理、API 响应处理、数据库模型等)。如果你的类包含大量复杂的业务逻辑或需要高度自定义的初始化,传统的类定义可能更合适。
4.6 asdict / astuple
asdict() 和 astuple() 是两个非常实用的内置函数,专门用于将 dataclass 实例转换为标准的字典(dict)或元组(tuple)结构。常用在数据序列化(如转 JSON)或数据解包。
from dataclasses import asdict, astuple
@dataclass
class GraphChange:
removed_nodes: set[str] = field(default_factory=set)
added_nodes: set[str] = field(default_factory=set)
change = GraphChange(removed_nodes={"A", "B"})
print(asdict(change)) # {'removed_nodes': {'A', 'B'}, 'added_nodes': set()}
print(astuple(change)) # ({'A', 'B'}, set())
第五章:Enum — 告别魔法数字
Enum(枚举)为相关常量提供命名空间和类型安全,避免魔法数字和字符串。
实际场景:API 返回码、任务状态(pending/running/done/failed)、订单状态、日志级别——任何有"有限的几个可能值"的概念都应用 Enum 约束,而不是散落魔法数字和字符串。
5.1 基本枚举
创建枚举最简单的方式是定义一个继承自 enum.Enum 的类。按照最佳实践,枚举类名建议使用大驼峰命名,成员名建议全大写。
from enum import Enum, IntEnum, auto
class Color(Enum):
RED = "#FF0000"
GREEN = "#00FF00"
BLUE = "#0000FF"
# 访问
print(Color.RED) # Color.RED
print(Color.RED.value) # '#FF0000'
print(Color.RED.name) # 'RED'
print(Color("#FF0000")) # Color.RED(通过值查找)
# 遍历
for color in Color:
print(color.name, color.value)
5.2 实用进阶特性
- 自动赋值 (
auto):如果不关心具体的整数值,可以使用auto()让 Python 自动分配值(默认从 1 开始递增):
from enum import Enum, auto
class Status(Enum):
PENDING = auto()
RUNNING = auto()
DONE = auto()
- 确保值唯一 (
@unique)
from enum import Enum, unique
@unique
class Shape(Enum):
SQUARE = 1
CIRCLE = 2
# TRIANGLE = 1 # 这会报错,因为值 1 已被 SQUARE 使用
- 添加自定义方法:枚举本质上是类,因此可以为其添加普通方法和类方法,实现更强大的功能:
class VIP(Enum):
BRONZE = 1
GOLD = 3
def level(self):
return {1: '初级', 3: '高级'}.get(self.value, '未知')
print(VIP.GOLD.level()) # 输出: '高级'
5.3 特殊枚举类型
根据不同的业务场景,enum 模块还提供了多种特殊基类:
- IntEnum:成员同时也是
int的子类,可以直接与整数进行比较和运算,支持排序。适用于需要兼容旧代码或进行数值比较的场景。
from enum import IntEnum
class RetCode(IntEnum):
SUCCESS = 0
NOT_EFFECTIVE = 10
EXCEPTION_ERROR = 100
ARGUMENT_ERROR = 101
UNAUTHORIZED = 401
FORBIDDEN = 403
SERVER_ERROR = 500
print(RetCode.SUCCESS == 0) # True
print(RetCode.UNAUTHORIZED > 100) # True(IntEnum 支持大小比较)
- StrEnum (Python 3.11+):成员同时也是
str的子类,支持字符串操作。 - Flag / IntFlag:用于处理位标志(Bitwise Flags),允许将多个成员组合在一起。成员的值通常应为 2 的幂(1, 2, 4, 8…),非常适合表示权限组合。
from enum import Flag
class Permission(Flag):
READ = 1
WRITE = 2
EXECUTE = 4
# 组合权限
user_perms = Permission.READ | Permission.WRITE
print(Permission.READ in user_perms) # True
第六章:collections — 高级容器
collections 模块提供比内置 dict/list 更强大的容器类型。它提供了对内置容器数据类型(如 dict、list、set、tuple)的补充,旨在提供更高效、更专业的数据结构解决方案。
6.1 Counter:计数器
Counter 是 dict 的子类,专门用于统计可哈希对象(如字符串、列表元素)出现的次数。
- 核心特性:自动将元素映射到其出现次数;如果访问不存在的键,默认返回
0而不是抛出KeyError。 - 常用方法:
most_common(n):返回出现次数最多的前n个元素及其计数。elements():返回一个迭代器,按元素重复的次数展开(忽略 0 和负数计数)。update()/subtract():支持计数器之间的加减操作。
from collections import Counter
c = Counter('abracadabra')
print(c.most_common(3)) # 输出: [('a', 5), ('b', 2), ('r', 2)]
# 找重复元素
ids = ["id_1", "id_2", "id_1", "id_3", "id_2", "id_1"]
dups = [item for item, count in Counter(ids).items() if count > 1]
# ['id_1', 'id_2']
# 数学运算
c1, c2 = Counter(a=3, b=1), Counter(a=1, b=2)
print(c1 + c2) # Counter({'a': 4, 'b': 3})
6.2 defaultdict:带默认值的字典
defaultdict 也是 dict 的子类,它的核心优势在于:当请求的键不存在时,会自动调用一个工厂函数(default_factory)来生成默认值,从而完美避免 KeyError。
- 常见工厂函数:
int:常用于计数(初始值为 0)。list:常用于构建字典,值为列表(初始值为空列表)。set:用于构建去重字典(初始值为空集合)。
from collections import defaultdict
# 统计字符出现次数
d = defaultdict(int)
for char in 'hello':
d[char] += 1
print(d) # 输出: defaultdict(<class 'int'>, {'h': 1, 'e': 1, 'l': 2, 'o': 1})
# 按首字母分组(不需要先检查键是否存在)
words = ["apple", "banana", "avocado", "blueberry", "cherry"]
groups = defaultdict(list)
for word in words:
groups[word[0]].append(word) # 键不存在时自动创建空列表
print(dict(groups)) # 输出: {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
# 实际使用:聚合实体,按名称分组
maybe_nodes = defaultdict(list)
for chunk in chunks:
for entity in extract_entities(chunk):
maybe_nodes[entity.name].append(entity)
6.3 deque:双向队列
deque(发音为 “deck”)是双端队列(double-ended queue)的缩写。它支持从两端高效地添加和弹出元素,时间复杂度均为 O(1)。
- 核心优势:相比于
list,deque在头部插入(insert(0, v))或弹出(pop(0))时,不需要移动整个列表的元素,极大节省了内存和时间开销。 - 适用场景:实现队列(FIFO)、栈(LIFO)或广度优先搜索(BFS)。
- 限制长度:可以通过
maxlen参数限制队列最大长度,满时新元素入队会自动挤出另一端的旧元素。
from collections import deque
d = deque([1, 2, 3], maxlen=3)
d.appendleft(0) # 队头添加
d.pop() # 队尾弹出
6.4 OrderedDict:带顺序的字典
from collections import OrderedDict
# Python 3.7+ 普通 dict 也保持插入顺序
# 但 OrderedDict 提供额外方法:move_to_end / popitem(按位置)
# 实现 LRU 缓存
class LRUCache:
def __init__(self, capacity=100):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return None
self.cache.move_to_end(key) # 移到末尾(标记为最近使用)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # 弹出最旧的(头部)
6.5 NamedTuple:带名字的元组
from typing import NamedTuple
class ExtractionResult(NamedTuple):
text_content: str
embedded_images: list
metadata: dict
result = ExtractionResult("hello", [], {"page": 1})
print(result.text_content) # hello
print(result._fields) # ('text_content', 'embedded_images', 'metadata')
第七章:异常处理进阶 — 构建健壮的错误体系
7.1 自定义异常体系
class AppError(Exception):
"""项目基础异常"""
class DocumentParseError(AppError):
"""文档解析失败"""
def __init__(self, doc_name, reason):
self.doc_name = doc_name
self.reason = reason
super().__init__(f"Parse failed for '{doc_name}': {reason}")
class ModelCallError(AppError):
"""模型调用失败"""
def __init__(self, model_name, status_code, detail):
self.model_name = model_name
self.status_code = status_code
self.detail = detail
super().__init__(f"Model '{model_name}' returned {status_code}: {detail}")
7.2 异常链(raise … from …)
def parse_document(filepath):
try:
content = read_file(filepath)
return process(content)
except FileNotFoundError as e:
raise DocumentParseError(filepath, "file not found") from e
# from e 保留原始异常链,traceback 会显示完整调用栈
7.3 上下文管理器的异常处理
from contextlib import contextmanager
@contextmanager
def safe_db_transaction(db):
"""自动提交/回滚的事务管理器"""
try:
yield
db.commit()
except Exception:
db.rollback()
raise # 异常继续向上传播
第八章:动态导入与插件系统
Python 的 importlib 和 inspect 支持运行时动态加载模块和发现类。
实际场景:LLM 工厂自动注册、插件系统的热加载、API 路由自动发现——这是构建"可扩展系统"的核心技术,新增功能时无需修改已有代码。
8.1 importlib 动态导入
import 语句在编译时是静态的,而 importlib 允许你在运行时以编程的方式动态导入模块。
import importlib
from importlib.util import module_from_spec, spec_from_file_location
# 按模块名导入
module = importlib.import_module("myapp.llm.chat_model")
ChatClass = getattr(module, "OpenAIChat")
# 从文件路径动态导入(API 路由自动发现的常见模式)
def load_module_from_file(file_path, module_name):
spec = spec_from_file_location(module_name, file_path)
module = module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
8.2 inspect — Python 反射
inspect 模块提供了多种实用函数,用于获取活动对象(如模块、类、方法、函数、调用堆栈等)的详细信息。它主要提供四大功能:类型检查、获取源代码、检查类与函数、检查解释器的调用堆栈。
import inspect
class MyClass:
"""文档字符串"""
def method_a(self, x: int, y: str = "hello") -> bool:
"""方法A"""
return True
# 获取所有成员
for name, obj in inspect.getmembers(MyClass, predicate=inspect.isfunction):
print(f" {name}: {obj}")
# 获取函数签名
sig = inspect.signature(MyClass.method_a)
# (self, x: int, y: str = 'hello') -> bool
for param_name, param in sig.parameters.items():
print(f" {param_name}: type={param.annotation}, default={param.default}")
8.3 实战:LLM 工厂自动注册
这是构建可扩展系统最值得学习的架构模式——通过反射 + 约定实现零配置的组件发现:
import importlib, inspect
ChatModel = {} # {"OpenAI": OpenAIChat, "Ollama": OllamaChat, ...}
# 步骤1:导入模块
module = importlib.import_module("myapp.llm.chat_model")
# 步骤2:在模块中找到 Base 基类
base_class = None
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and name == "Base":
base_class = obj
# 步骤3:自动发现所有 Base 子类并注册
for name, obj in inspect.getmembers(module):
if (inspect.isclass(obj) # 是类
and issubclass(obj, base_class) # 是 Base 的子类
and obj is not base_class # 不是 Base 自身
and hasattr(obj, "_FACTORY_NAME")): # 有注册名
ChatModel[obj._FACTORY_NAME] = obj
# 使用:通过名称获取模型类
model_class = ChatModel["OpenAI"]
model = model_class(api_key="sk-xxx", model_name="gpt-4")
设计精华:新增 LLM 提供商时,只需在
chat_model.py中添加一个类,不需要修改任何注册代码——这是开闭原则的典范。
8.4 pkgutil — 包遍历
import pkgutil, importlib
from pathlib import Path
# 递归遍历包中所有子模块
pkg_dir = Path(__file__).resolve().parent
for modinfo in pkgutil.walk_packages([str(pkg_dir)], prefix=__name__ + "."):
module = importlib.import_module(modinfo.name)
# 处理发现的模块...
本篇自检清单
学完本篇后确认以下问题能答上来:
- MRO:类
D(B, C)的方法解析顺序是什么规则?super()按什么顺序调用?- 规则:遵循 C3 线性化算法。核心原则是:子类永远在父类之前被检查;如果类
D(B, C)声明了多个父类,B 及其祖先永远排在 C 及其祖先前面;每个类只出现一次。 - 调用顺序:
super()并不是调用“直接父类”,而是严格按照当前实例的 MRO 链,调用当前类之后的下一个类的方法。
- 规则:遵循 C3 线性化算法。核心原则是:子类永远在父类之前被检查;如果类
- ABC:抽象方法没实现时,什么时候报错?为什么 ABC 比
NotImplementedError好?- 报错时机:在**实例化(调用
__init__)**时抛出TypeError。 - 优势:
NotImplementedError只有在代码真正运行到该方法时才会报错(运行时反馈);而 ABC 在对象创建阶段就能强制拦截,将错误提前暴露,是更严谨的契约设计。
- 报错时机:在**实例化(调用
- Protocol vs ABC:什么时候用 Protocol,什么时候用 ABC?
- Protocol(结构化类型):强调“能力”。只要对象实现了规定的方法,就自动满足协议,无需显式继承。适合解耦、鸭子类型和静态类型检查。
- ABC(名义类型):强调“血缘”。强制子类必须显式继承。适合构建框架、强约束的组件体系,并在运行时提供强制保障。
- dataclass:
frozen=True的效果?可变默认值为什么必须用field(default_factory=)?frozen=True效果:将实例变为不可变对象。任何对属性的赋值操作都会触发FrozenInstanceError,使其可安全用作字典的 key 或放入 set。- 可变默认值陷阱:如果直接写
tags: list = [],这个空列表会被所有实例共享(因为它是类级别的单一对象)。default_factory=list确保每次实例化时都会调用list()创建一个全新的独立列表。
- Enum:
IntEnum和普通Enum的区别?什么时候需要自定义枚举方法?- 区别:
IntEnum的成员同时也是int的子类,可以直接与整数进行大小比较和数学运算;普通Enum成员是独立的对象,不支持与整数混用。 - 自定义方法:当枚举值需要映射到业务逻辑时(例如将状态码转换为中文提示、计算 VIP 等级折扣),在枚举类中添加方法可以极大提升代码的封装性和可读性。
- 区别:
- 异常体系:如何设计分层异常?
raise ... from e的作用?- 分层设计:为项目定义一个基础异常类(继承自
Exception),然后根据不同业务模块或错误类型派生子异常,便于调用方进行精确的except捕获。 raise ... from e:实现异常链(Exception Chaining)。它将原始异常e附加到新异常上,保留完整的错误追踪上下文(Traceback),方便排查底层引发问题的根源。
- 分层设计:为项目定义一个基础异常类(继承自
- 动态导入:
importlib.import_module和import语句的区别?插件自动发现的三个步骤?- 区别:
import语句在编译时是静态的,模块名必须是硬编码的字面量;importlib.import_module允许在运行时将字符串变量作为模块名进行动态加载。 - 插件自动发现三步曲:
- 扫描插件目录,获取所有模块文件名。
- 使用
importlib动态加载这些模块。 - 使用
inspect遍历模块成员,找出所有符合特定接口(如继承自某基类)的类或函数,并注册到系统中。
- 区别:
相关系列文章
- 「学习笔记」Python 进阶(三):工程实战 — 从脚本到项目
- 「学习笔记」Python 进阶(二):抽象设计 — 构建优雅系统
- 「学习笔记」Python 进阶(一):语言特质 — 写出地道 Python
- 「学习笔记」Python基础入门