「学习笔记」Python 进阶(二):抽象设计 — 构建优雅系统

文章目录

此篇为 Python 进阶的第二篇——抽象设计篇,聚焦如何用 Python 的面向对象与抽象机制构建可扩展、可维护的系统。当你能写出能跑的程序后,本篇教你写出"能长久的程序"。

前置:语言特质篇 · 后续:工程实战篇

第一章:Python 面向对象 — 你需要知道的进阶细节

Python 基础入门 第五章已覆盖封装/继承/多态基础概念。这里补几个实际开发中绕不开的进阶知识点。

1.1 MRO 的核心算法:C3 线性化

当子类继承多个父类时,Python 使用 C3 线性化算法 确定方法解析顺序(Method Resolution Order)。它的三大核心约束如下:

  1. 子类优先:子类永远在它的父类之前被检查。
  2. 保持声明顺序:如果类 D(B, C) 声明了多个父类,那么 B 及其祖先永远排在 C 及其祖先的前面。
  3. 单调性与无重复:每个类在 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())     # 同上,列表形式

关键规则

  1. 子类优先于父类
  2. 父类顺序按声明顺序(从左到右)
  3. 所有父类必须满足 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() 精细控制字段

当需要为可变对象(如 listdict)设置默认值,或者需要控制字段是否参与初始化、比较时,必须使用 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 更强大的容器类型。它提供了对内置容器数据类型(如 dictlistsettuple)的补充,旨在提供更高效、更专业的数据结构解决方案。

6.1 Counter:计数器

Counterdict 的子类,专门用于统计可哈希对象(如字符串、列表元素)出现的次数。

  • 核心特性:自动将元素映射到其出现次数;如果访问不存在的键,默认返回 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)。

  • 核心优势:相比于 listdeque 在头部插入(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 异常链(raisefrom …)

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 的 importlibinspect 支持运行时动态加载模块和发现类。

实际场景: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 链,调用当前类之后的下一个类的方法。
  • ABC:抽象方法没实现时,什么时候报错?为什么 ABC 比 NotImplementedError 好?
    • 报错时机:在**实例化(调用 __init__)**时抛出 TypeError
    • 优势NotImplementedError 只有在代码真正运行到该方法时才会报错(运行时反馈);而 ABC 在对象创建阶段就能强制拦截,将错误提前暴露,是更严谨的契约设计。
  • Protocol vs ABC:什么时候用 Protocol,什么时候用 ABC?
    • Protocol(结构化类型):强调“能力”。只要对象实现了规定的方法,就自动满足协议,无需显式继承。适合解耦、鸭子类型和静态类型检查。
    • ABC(名义类型):强调“血缘”。强制子类必须显式继承。适合构建框架、强约束的组件体系,并在运行时提供强制保障。
  • dataclassfrozen=True 的效果?可变默认值为什么必须用 field(default_factory=)
    • frozen=True 效果:将实例变为不可变对象。任何对属性的赋值操作都会触发 FrozenInstanceError,使其可安全用作字典的 key 或放入 set。
    • 可变默认值陷阱:如果直接写 tags: list = [],这个空列表会被所有实例共享(因为它是类级别的单一对象)。default_factory=list 确保每次实例化时都会调用 list() 创建一个全新的独立列表。
  • EnumIntEnum 和普通 Enum 的区别?什么时候需要自定义枚举方法?
    • 区别IntEnum 的成员同时也是 int 的子类,可以直接与整数进行大小比较和数学运算;普通 Enum 成员是独立的对象,不支持与整数混用。
    • 自定义方法:当枚举值需要映射到业务逻辑时(例如将状态码转换为中文提示、计算 VIP 等级折扣),在枚举类中添加方法可以极大提升代码的封装性和可读性。
  • 异常体系:如何设计分层异常?raise ... from e 的作用?
    • 分层设计:为项目定义一个基础异常类(继承自 Exception),然后根据不同业务模块或错误类型派生子异常,便于调用方进行精确的 except 捕获。
    • raise ... from e:实现异常链(Exception Chaining)。它将原始异常 e 附加到新异常上,保留完整的错误追踪上下文(Traceback),方便排查底层引发问题的根源。
  • 动态导入importlib.import_moduleimport 语句的区别?插件自动发现的三个步骤?
    • 区别import 语句在编译时是静态的,模块名必须是硬编码的字面量;importlib.import_module 允许在运行时将字符串变量作为模块名进行动态加载。
    • 插件自动发现三步曲
      1. 扫描插件目录,获取所有模块文件名。
      2. 使用 importlib 动态加载这些模块。
      3. 使用 inspect 遍历模块成员,找出所有符合特定接口(如继承自某基类)的类或函数,并注册到系统中。

下一篇:Python 进阶(三):工程实战 — 从脚本到项目

END .

相关系列文章

×