第 4 卷:面向对象、数据建模与类型系统

第 4 卷:面向对象、数据建模与类型系统

学习方式:严格按顺序阅读。每个代码块只使用当前单元或此前单元已经讲过的语法。

本卷项目线:taskflow/milestones/v04_modeled_tasks.py --demo

完成本卷后:进入第 5 卷。

本卷说明

本卷把字典和松散函数升级为有明确不变量的对象、协议和类型别名。你会先学类如何工作,再学注解如何表达接口;类型检查通过仍不等于运行时校验。

本卷知识单元

  • [4.1 类、实例与方法](#4.1 类、实例与方法 "#41-%E7%B1%BB%E5%AE%9E%E4%BE%8B%E4%B8%8E%E6%96%B9%E6%B3%95")
  • [4.2 类型注解与函数接口](#4.2 类型注解与函数接口 "#42-%E7%B1%BB%E5%9E%8B%E6%B3%A8%E8%A7%A3%E4%B8%8E%E5%87%BD%E6%95%B0%E6%8E%A5%E5%8F%A3")
  • [4.3 继承、MRO 与 super](#4.3 继承、MRO 与 super "#43-%E7%BB%A7%E6%89%BFmro-%E4%B8%8E-super")
  • [4.4 魔术方法与对象协议](#4.4 魔术方法与对象协议 "#44-%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95%E4%B8%8E%E5%AF%B9%E8%B1%A1%E5%8D%8F%E8%AE%AE")
  • [4.5 dataclass、不可变对象与默认工厂](#4.5 dataclass、不可变对象与默认工厂 "#45-dataclass%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%AF%B9%E8%B1%A1%E4%B8%8E%E9%BB%98%E8%AE%A4%E5%B7%A5%E5%8E%82")
  • [4.6 property、描述符与属性校验](#4.6 property、描述符与属性校验 "#46-property%E6%8F%8F%E8%BF%B0%E7%AC%A6%E4%B8%8E%E5%B1%9E%E6%80%A7%E6%A0%A1%E9%AA%8C")
  • [4.7 ABC、Protocol 与组合](#4.7 ABC、Protocol 与组合 "#47-abcprotocol-%E4%B8%8E%E7%BB%84%E5%90%88")
  • [4.8 枚举、常量与状态建模](#4.8 枚举、常量与状态建模 "#48-%E6%9E%9A%E4%B8%BE%E5%B8%B8%E9%87%8F%E4%B8%8E%E7%8A%B6%E6%80%81%E5%BB%BA%E6%A8%A1")
  • [4.9 泛型、TypedDict、Literal 与静态检查](#4.9 泛型、TypedDict、Literal 与静态检查 "#49-%E6%B3%9B%E5%9E%8Btypeddictliteral-%E4%B8%8E%E9%9D%99%E6%80%81%E6%A3%80%E6%9F%A5")
  • [4.10 复制、相等、哈希与 slots](#4.10 复制、相等、哈希与 slots "#410-%E5%A4%8D%E5%88%B6%E7%9B%B8%E7%AD%89%E5%93%88%E5%B8%8C%E4%B8%8E-slots")
  • [4.11 OOP 与类型项目线:TaskFlow v04](#4.11 OOP 与类型项目线:TaskFlow v04 "#411-oop-%E4%B8%8E%E7%B1%BB%E5%9E%8B%E9%A1%B9%E7%9B%AE%E7%BA%BFtaskflow-v04")
  • [4.12 dataclass 进阶:校验、字段策略与继承](#4.12 dataclass 进阶:校验、字段策略与继承 "#412-dataclass-%E8%BF%9B%E9%98%B6%E6%A0%A1%E9%AA%8C%E5%AD%97%E6%AE%B5%E7%AD%96%E7%95%A5%E4%B8%8E%E7%BB%A7%E6%89%BF")
  • [4.13 对象协议进阶:可调用、索引与上下文](#4.13 对象协议进阶:可调用、索引与上下文 "#413-%E5%AF%B9%E8%B1%A1%E5%8D%8F%E8%AE%AE%E8%BF%9B%E9%98%B6%E5%8F%AF%E8%B0%83%E7%94%A8%E7%B4%A2%E5%BC%95%E4%B8%8E%E4%B8%8A%E4%B8%8B%E6%96%87")
  • [4.14 typing 进阶:Callable、TypeVar 与 overload](#4.14 typing 进阶:Callable、TypeVar 与 overload "#414-typing-%E8%BF%9B%E9%98%B6callabletypevar-%E4%B8%8E-overload")
  • [4.15 泛型接口与依赖注入组合](#4.15 泛型接口与依赖注入组合 "#415-%E6%B3%9B%E5%9E%8B%E6%8E%A5%E5%8F%A3%E4%B8%8E%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%BB%84%E5%90%88")

4.1 类、实例与方法

前置知识:第 1~3 卷;尤其是函数、异常和文件。

概念 :类把状态和行为组织在一起。实例属性属于每个对象,类属性由所有实例共享;实例方法显式接收 self,__init__ 负责初始化。Python 没有真正的私有字段,双下划线只会触发名称改写。

本单元新语法

语法形式 含义
class Name: 定义类
def __init__(self, ...): 初始化实例
self.attr = value 设置实例属性
@classmethod 接收类而不是实例
@staticmethod 不接收 self/cls 的普通方法

最小示例

最小示例 1:任务对象
python 复制代码
# 示例目标:任务对象
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:完整可运行

class Task:
    def __init__(self, title):
        self.title = title
        self.done = False

    def complete(self):
        self.done = True

task = Task("Read")
task.complete()
print(task.title, task.done)
# 输出: Read True

# 示例类型:最小示例
最小示例 2:类方法构造实例
python 复制代码
# 示例目标:类方法构造实例
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:完整可运行

class Task:
    def __init__(self, title):
        self.title = title

    @classmethod
    def empty(cls):
        return cls("untitled")

print(Task.empty().title)
# 输出: untitled

# 示例类型:最小示例

实际场景示例

任务、用户、订单等有身份和行为的领域对象适合类;仅承载字段的内部数据优先用 dataclass。

python 复制代码
# 示例目标:带校验的任务对象
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    def __init__(self, title):
        cleaned = title.strip()
        if not cleaned:
            raise ValueError("title required")
        self.title = cleaned

task = Task("  Read  ")
print(task.title)
# 输出: Read

# 示例类型:实际场景

关联/组合

组合 1:静态方法
python 复制代码
# 示例目标:静态方法
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    @staticmethod
    def normalize(title):
        return title.strip().lower()

print(Task.normalize(" Read "))
# 输出: read

# 示例类型:关联/组合
组合 2:实例属性独立
python 复制代码
# 示例目标:实例属性独立
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    tags = []

    def __init__(self):
        self.items = []

a = Task()
b = Task()
a.items.append("x")
print(a.items, b.items)
# 输出: ['x'] []

# 示例类型:关联/组合

易错/易混

易错 1:用可变类属性保存实例状态

错误代码:

python 复制代码
# 示例目标:用可变类属性保存实例状态(错误示范)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    tags = []

a = Task()
b = Task()
a.tags.append("work")
print(b.tags)

# 示例类型:错误示范

实际结果:输出 'work';两个实例共享同一个类属性列表。

修正代码:

python 复制代码
# 示例目标:用可变类属性保存实例状态(修正)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    def __init__(self):
        self.tags = []

a = Task()
b = Task()
a.tags.append("work")
print(b.tags)
# 输出: []

# 示例类型:易错修正

原因 :每个实例的状态在 init 中创建。

易错 2:忘记 self

错误代码:

python 复制代码
# 示例目标:忘记 self(错误示范)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    def show():
        print("task")

Task().show()

# 示例类型:错误示范

实际结果:抛出 TypeError:实例被当作参数传入但方法不接收。

修正代码:

python 复制代码
# 示例目标:忘记 self(修正)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行

class Task:
    def show(self):
        print("task")

Task().show()
# 输出: task

# 示例类型:易错修正

原因:实例方法至少接收 self。

练习

  1. 写一个 Task 类,包含 title 和 done,并实现 complete 方法。
  2. 写一个类方法 empty() 创建标题为 untitled 的实例。

4.2 类型注解与函数接口

前置知识:4.1 类;第 2 卷函数;第 3 卷异常。

概念 :注解描述期望类型,主要服务阅读、IDE 和静态检查;运行时默认不会自动校验。内置泛型如 list[str] 从 3.9 起可用,X | None 从 3.10 起可用。

本单元新语法

| 语法形式 | 含义 |
|-------------------------|--------------|-----------|
| x: int | 变量/属性注解 |
| def f(x: int) -> str: | 参数与返回值注解 |
| list[str] | 元素为字符串的列表 |
| `str | None` | 字符串或 None |
| Optional[str] | 旧的等价写法 |
| reveal_type(x) | 让类型检查器显示推断类型 |

最小示例

最小示例 1:函数注解
python 复制代码
# 示例目标:函数注解
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:完整可运行

def title_length(title: str) -> int:
    return len(title)

print(title_length("read"))
# 输出: 4

# 示例类型:最小示例
最小示例 2:可选返回值
python 复制代码
# 示例目标:可选返回值
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:完整可运行

def find(tasks: list[str], target: str) -> str | None:
    return target if target in tasks else None

print(find(["read"], "read"))
print(find([], "read"))
# 输出: read
# 输出: None

# 示例类型:最小示例

实际场景示例

公共函数、服务接口和 DTO 使用注解;外部 JSON 仍要通过 Pydantic 或显式校验后才能进入有类型的世界。

python 复制代码
# 示例目标:带注解的任务创建函数
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def create_task(title: str, tags: list[str]) -> dict[str, object]:
    return {"title": title.strip(), "tags": tags}

print(create_task(" Read ", ["study"]))
# 输出: {'title': 'Read', 'tags': ['study']}

# 示例类型:实际场景

关联/组合

组合 1:容器注解
python 复制代码
# 示例目标:容器注解
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def average(values: list[float]) -> float:
    return sum(values) / len(values)

print(average([1.0, 3.0]))
# 输出: 2.0

# 示例类型:关联/组合

易错/易混

易错 1:把注解当运行时校验

错误代码:

python 复制代码
# 示例目标:把注解当运行时校验(错误示范)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def create(age: int):
    return age

print(create("18"))

# 示例类型:错误示范

实际结果:输出 18 字符串,注解不会拒绝它。

修正代码:

python 复制代码
# 示例目标:把注解当运行时校验(修正)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def create(age: int):
    if not isinstance(age, int):
        raise TypeError("age must be int")
    return age

try:
    create("18")
except TypeError as exc:
    print(exc)
# 输出: age must be int

# 示例类型:易错修正

原因:运行时输入需要显式校验。

易错 2:用 None 表示所有缺失

错误代码:

python 复制代码
# 示例目标:用 None 表示所有缺失(错误示范)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def greet(name: str | None) -> str:
    return "hi " + name

# 示例类型:错误示范

实际结果:name 为 None 时拼接失败。

修正代码:

python 复制代码
# 示例目标:用 None 表示所有缺失(修正)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def greet(name: str | None) -> str:
    if name is None:
        name = "guest"
    return "hi " + name

print(greet(None))
# 输出: hi guest

# 示例类型:易错修正

原因:可选类型必须在逻辑中处理 None 分支。

易错 3:注解不阻止运行时错误

错误代码:

python 复制代码
# 示例目标:注解不阻止运行时错误(错误示范)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def length(value: str) -> int:
    return len(value)

print(length(123))

# 示例类型:错误示范

实际结果:运行时抛出 TypeError;注解没有做转换或校验。

修正代码:

python 复制代码
# 示例目标:注解不阻止运行时错误(修正)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行

def length(value: str) -> int:
    if not isinstance(value, str):
        raise TypeError("value must be str")
    return len(value)

print(length(123))

# 示例类型:易错修正

原因:边界需要显式校验,注解只是静态契约。

练习

  1. 给 double(value) 加参数和返回值注解。
  2. 写一个返回 str | None 的查找函数。

4.3 继承、MRO 与 super

前置知识:4.1 类;4.2 注解;3.7 异常。

概念 :子类继承父类的属性和方法,方法查找沿 C3 线性化的 MRO 进行。super() 调用 MRO 中的下一个实现,适合协作式继承;跨模块复用优先组合,避免深继承。

本单元新语法

语法形式 含义
class Child(Parent): 继承
super().__init__() 调用 MRO 下一个初始化
isinstance(obj, Parent) 检查类型关系
MRO 方法解析顺序
mixin 提供局部能力的辅助类

最小示例

最小示例 1:子类扩展父类
python 复制代码
# 示例目标:子类扩展父类
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:完整可运行

class BaseTask:
    def describe(self) -> str:
        return "task"

class UrgentTask(BaseTask):
    def describe(self) -> str:
        return "urgent " + super().describe()

print(UrgentTask().describe())
# 输出: urgent task

# 示例类型:最小示例
最小示例 2:isinstance
python 复制代码
# 示例目标:isinstance
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:完整可运行

class BaseTask:
    pass

class UrgentTask(BaseTask):
    pass

print(isinstance(UrgentTask(), BaseTask))
# 输出: True

# 示例类型:最小示例

实际场景示例

框架扩展点、领域对象层次和测试替身可能使用继承;如果只是需要复用行为,优先把对象作为属性组合进来。

python 复制代码
# 示例目标:抽象任务接口的早期形态
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class Task:
    def run(self) -> str:
        raise NotImplementedError

class ReadTask(Task):
    def run(self) -> str:
        return "read"

print(ReadTask().run())
# 输出: read

# 示例类型:实际场景

关联/组合

组合 1:调用父类初始化
python 复制代码
# 示例目标:调用父类初始化
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class Base:
    def __init__(self, title: str) -> None:
        self.title = title

class Child(Base):
    def __init__(self, title: str, priority: str) -> None:
        super().__init__(title)
        self.priority = priority

child = Child("Read", "high")
print(child.title, child.priority)
# 输出: Read high

# 示例类型:关联/组合
组合 2:MRO 查看
python 复制代码
# 示例目标:MRO 查看
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class A:
    pass
class B(A):
    pass
print([cls.__name__ for cls in B.__mro__])
# 输出: ['B', 'A', 'object']

# 示例类型:关联/组合

易错/易混

易错 1:用继承代替组合

错误代码:

python 复制代码
# 示例目标:用继承代替组合(错误示范)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class Task:
    def save(self) -> None:
        pass

class CachedTask(Task):
    def get(self, key: str) -> str:
        return key

# 示例类型:错误示范

实际结果:CachedTask 被迫继承无关的 save,类型层次变混乱。

修正代码:

python 复制代码
# 示例目标:用继承代替组合(修正)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class Cache:
    def get(self, key: str) -> str:
        return key

class Task:
    def __init__(self, cache: Cache) -> None:
        self.cache = cache

print(Task(Cache()).cache.get("k"))
# 输出: k

# 示例类型:易错修正

原因:能力通过组合注入,避免把无关行为塞进继承树。

易错 2:省略 super 初始化

错误代码:

python 复制代码
# 示例目标:省略 super 初始化(错误示范)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class Base:
    def __init__(self) -> None:
        self.ready = True

class Child(Base):
    def __init__(self) -> None:
        pass

print(Child().ready)

# 示例类型:错误示范

实际结果:抛出 AttributeError:父类初始化没有执行。

修正代码:

python 复制代码
# 示例目标:省略 super 初始化(修正)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

class Base:
    def __init__(self) -> None:
        self.ready = True

class Child(Base):
    def __init__(self) -> None:
        super().__init__()

print(Child().ready)
# 输出: True

# 示例类型:易错修正

原因:子类覆盖初始化时显式调用协作式 super。

练习

  1. 写一个父类和子类,子类在 describe 中调用 super。
  2. 打印一个类的 MRO 名称。

4.4 魔术方法与对象协议

前置知识:4.1 类;4.3 继承。

概念 :Python 的内置语法通过魔术方法接入对象:len 调用 __len__,== 调用 __eq__,print 优先使用 __str__,调试使用 __repr__,with 使用 __enter__/__exit__。

本单元新语法

语法形式 含义
__repr__(self) 开发者可读表示
__str__(self) 用户可读表示
__eq__(self, other) 相等性
__len__(self) 长度
__iter__(self) 迭代协议
__contains__(self, item) 成员测试协议
__hash__(self) 哈希协议

最小示例

最小示例 1:repr 与 eq
python 复制代码
# 示例目标:repr 与 eq
# 前置知识:4.1 类;4.3 继承。
# 代码状态:完整可运行

class Task:
    def __init__(self, title: str) -> None:
        self.title = title

    def __repr__(self) -> str:
        return f"Task({self.title!r})"

    def __eq__(self, other: object) -> bool:
        return isinstance(other, Task) and self.title == other.title

print(Task("Read"))
print(Task("Read") == Task("Read"))
# 输出: Task('Read')
# 输出: True

# 示例类型:最小示例
最小示例 2:len 与 contains
python 复制代码
# 示例目标:len 与 contains
# 前置知识:4.1 类;4.3 继承。
# 代码状态:完整可运行

class Tags:
    def __init__(self, items: list[str]) -> None:
        self.items = items

    def __len__(self) -> int:
        return len(self.items)

    def __contains__(self, item: str) -> bool:
        return item in self.items

tags = Tags(["work"])
print(len(tags), "work" in tags)
# 输出: 1 True

# 示例类型:最小示例

实际场景示例

值对象需要可读 repr 和明确相等性,容器需要 len/iter,上下文管理器需要 enter/exit。

python 复制代码
# 示例目标:可哈希的值对象
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def __eq__(self, other: object) -> bool:
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

    def __hash__(self) -> int:
        return hash((self.x, self.y))

print({Point(1, 2), Point(1, 2)})
# 输出包含: Point object 或自定义表示

# 示例类型:实际场景

关联/组合

组合 1:可迭代对象
python 复制代码
# 示例目标:可迭代对象
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Countdown:
    def __init__(self, n: int) -> None:
        self.n = n

    def __iter__(self):
        for value in range(self.n, 0, -1):
            yield value

print(list(Countdown(3)))
# 输出: [3, 2, 1]

# 示例类型:关联/组合
组合 2:调试表示
python 复制代码
# 示例目标:调试表示
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Task:
    def __repr__(self) -> str:
        return "Task()"

print([Task()])
# 输出: [Task()]

# 示例类型:关联/组合

易错/易混

易错 1:实现 eq 却忽略 hash

错误代码:

python 复制代码
# 示例目标:实现 eq 却忽略 hash(错误示范)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Task:
    def __eq__(self, other: object) -> bool:
        return True

task = Task()
print(hash(task))

# 示例类型:错误示范

实际结果:抛出 TypeError:unhashable type。

修正代码:

python 复制代码
# 示例目标:实现 eq 却忽略 hash(修正)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Task:
    def __eq__(self, other: object) -> bool:
        return isinstance(other, Task)

    def __hash__(self) -> int:
        return 1

print(hash(Task()))
# 输出: 1

# 示例类型:易错修正

原因 :定义 eq 后要明确对象是否可哈希;值对象通常同时实现 hash。

易错 2:repr 返回非字符串

错误代码:

python 复制代码
# 示例目标:repr 返回非字符串(错误示范)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Task:
    def __repr__(self) -> str:
        return 1

print(Task())

# 示例类型:错误示范

实际结果 :抛出 TypeError:repr returned non-string。

修正代码:

python 复制代码
# 示例目标:repr 返回非字符串(修正)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行

class Task:
    def __repr__(self) -> str:
        return "Task()"

print(Task())
# 输出: Task()

# 示例类型:易错修正

原因:魔术方法必须满足协议返回类型。

练习

  1. 让 Task 的 == 按 title 比较。
  2. 实现 __len__,让 len(Tags(["a"])) 返回 1。

4.5 dataclass、不可变对象与默认工厂

前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。

概念 :@dataclass 根据字段自动生成初始化、repr 和相等性。frozen=True 防止重新绑定,slots=True 减少实例字典开销;可变字段使用 default_factory,默认值不能直接写列表。

本单元新语法

语法形式 含义
@dataclass 自动生成数据类方法
field(default_factory=list) 为每个实例创建默认容器
frozen=True 禁止重新绑定字段
slots=True 限制实例属性并节省内存
replace(obj, ...) 生成修改后的新对象

最小示例

最小示例 1:任务数据类
python 复制代码
# 示例目标:任务数据类
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:完整可运行

from dataclasses import dataclass, field

@dataclass
class Task:
    id: str
    title: str
    tags: list[str] = field(default_factory=list)

task = Task("a", "Read")
task.tags.append("study")
print(task)
# 输出包含: Task(id='a', title='Read', tags=['study'])

# 示例类型:最小示例
最小示例 2:不可变点
python 复制代码
# 示例目标:不可变点
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:完整可运行

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

point = Point(1, 2)
print(point == Point(1, 2))
# 输出: True

# 示例类型:最小示例

实际场景示例

DTO、配置快照、值对象和测试数据适合 dataclass;外部不可信输入仍需运行时校验。

python 复制代码
# 示例目标:修改生成新任务
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Task:
    id: str
    title: str
    done: bool = False

original = Task("a", "Read")
completed = replace(original, done=True)
print(original.done, completed.done)
# 输出: False True

# 示例类型:实际场景

关联/组合

组合 1:列表默认工厂
python 复制代码
# 示例目标:列表默认工厂
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, field

@dataclass
class Box:
    items: list[str] = field(default_factory=list)

a = Box()
b = Box()
a.items.append("x")
print(a.items, b.items)
# 输出: ['x'] []

# 示例类型:关联/组合
组合 2:slots
python 复制代码
# 示例目标:slots
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass(slots=True)
class Task:
    title: str

print(Task("Read").title)
# 输出: Read

# 示例类型:关联/组合

易错/易混

易错 1:直接把列表作为默认值

错误代码:

python 复制代码
# 示例目标:直接把列表作为默认值(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass
class Task:
    tags: list[str] = []

# 示例类型:错误示范

实际结果:当前 Python 会直接拒绝这个定义,因为可变默认值不安全。

修正代码:

python 复制代码
# 示例目标:直接把列表作为默认值(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, field

@dataclass
class Task:
    tags: list[str] = field(default_factory=list)

print(Task().tags)
# 输出: []

# 示例类型:易错修正

原因:可变默认值必须用 default_factory 为每个实例创建新对象。

易错 2:frozen 不等于深不可变

错误代码:

python 复制代码
# 示例目标:frozen 不等于深不可变(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass(frozen=True)
class Task:
    tags: list[str]

task = Task(["work"])
task.tags.append("home")
print(task.tags)

# 示例类型:错误示范

实际结果:输出 'work', 'home';frozen 只阻止字段重新绑定。

修正代码:

python 复制代码
# 示例目标:frozen 不等于深不可变(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass(frozen=True)
class Task:
    tags: tuple[str, ...]

task = Task(("work",))
print(task.tags)
# 输出: ('work',)

# 示例类型:易错修正

原因:需要深层不可变时使用元组等不可变字段。

练习

  1. 定义包含 id、title、tags 的 dataclass,tags 使用默认工厂。
  2. 用 replace 从 frozen Task 生成 done=True 的新对象。

4.6 property、描述符与属性校验

前置知识:4.1 类;4.4 魔术方法;3.7 异常。

概念:property 把方法包装成属性访问,适合校验和派生值。描述符是更底层的属性协议,property 本身就是描述符;ORM 字段和框架配置常使用描述符。

本单元新语法

语法形式 含义
@property 只读计算属性
@name.setter 属性赋值校验
cached_property 首次计算后缓存
__get__/__set__ 描述符协议
__set_name__ 描述符获知字段名

最小示例

最小示例 1:只读派生属性
python 复制代码
# 示例目标:只读派生属性
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:完整可运行

class Task:
    def __init__(self, title: str) -> None:
        self.title = title

    @property
    def slug(self) -> str:
        return self.title.lower().replace(" ", "-")

print(Task("Read Docs").slug)
# 输出: read-docs

# 示例类型:最小示例
最小示例 2:setter 校验
python 复制代码
# 示例目标:setter 校验
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:完整可运行

class Task:
    def __init__(self, priority: int) -> None:
        self.priority = priority

    @property
    def priority(self) -> int:
        return self._priority

    @priority.setter
    def priority(self, value: int) -> None:
        if not 1 <= value <= 5:
            raise ValueError("priority out of range")
        self._priority = value

print(Task(3).priority)
# 输出: 3

# 示例类型:最小示例

实际场景示例

输入校验、派生字段、惰性计算和框架字段声明都可以使用属性协议;属性读取不应偷偷执行昂贵 I/O。

python 复制代码
# 示例目标:缓存派生值
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

from functools import cached_property

class Task:
    def __init__(self, title: str) -> None:
        self.title = title

    @cached_property
    def display(self) -> str:
        return self.title.upper()

task = Task("read")
print(task.display)
# 输出: READ

# 示例类型:实际场景

关联/组合

组合 1:setter 使用内部字段
python 复制代码
# 示例目标:setter 使用内部字段
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

class Box:
    def __init__(self) -> None:
        self.value = 1

    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, number: int) -> None:
        self._value = number

box = Box()
box.value = 2
print(box.value)
# 输出: 2

# 示例类型:关联/组合
组合 2:只读属性
python 复制代码
# 示例目标:只读属性
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

class Task:
    @property
    def kind(self) -> str:
        return "task"

print(Task().kind)
# 输出: task

# 示例类型:关联/组合

易错/易混

易错 1:setter 递归赋值

错误代码:

python 复制代码
# 示例目标:setter 递归赋值(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

class Task:
    @property
    def priority(self) -> int:
        return self.priority

    @priority.setter
    def priority(self, value: int) -> None:
        self.priority = value

# 示例类型:错误示范

实际结果:赋值时无限递归,最终 RecursionError。

修正代码:

python 复制代码
# 示例目标:setter 递归赋值(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

class Task:
    @property
    def priority(self) -> int:
        return self._priority

    @priority.setter
    def priority(self, value: int) -> None:
        self._priority = value

task = Task()
task.priority = 2
print(task.priority)
# 输出: 2

# 示例类型:易错修正

原因:setter 必须写入不同的内部字段。

易错 2:属性读取做网络请求

错误代码:

python 复制代码
# 示例目标:属性读取做网络请求(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

class User:
    @property
    def profile(self):
        return fetch_profile()

# 示例类型:错误示范

实际结果:每次访问属性都可能触发网络,调用方无法预期。

修正代码:

python 复制代码
# 示例目标:属性读取做网络请求(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行

class User:
    def load_profile(self):
        return fetch_profile()

# 示例类型:易错修正

原因:有 I/O 的操作使用显式方法名,让调用方知道成本。

练习

  1. 给 Task 增加只读属性 slug。
  2. 写一个 priority setter,拒绝 0 以下的值。

4.7 ABC、Protocol 与组合

前置知识:4.1~4.6;4.2 注解。

概念:ABC 是显式继承的抽象基类;Protocol 是结构化类型,只要对象有匹配方法就兼容。组合通过持有其他对象复用能力,通常比继承更灵活。

本单元新语法

语法形式 含义
class X(ABC): 名义抽象基类
@abstractmethod 强制子类实现
Protocol 结构化接口
runtime_checkable 允许 isinstance 检查成员
组合 通过属性持有依赖

最小示例

最小示例 1:Protocol 仓储
python 复制代码
# 示例目标:Protocol 仓储
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:完整可运行

from typing import Protocol

class Repository(Protocol):
    def save(self, title: str) -> str: ...

class MemoryRepository:
    def save(self, title: str) -> str:
        return "saved:" + title

def run(repo: Repository) -> str:
    return repo.save("Read")

print(run(MemoryRepository()))
# 输出: saved:Read

# 示例类型:最小示例
最小示例 2:ABC 约束
python 复制代码
# 示例目标:ABC 约束
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:完整可运行

from abc import ABC, abstractmethod

class Notifier(ABC):
    @abstractmethod
    def send(self, message: str) -> None:
        raise NotImplementedError

class ConsoleNotifier(Notifier):
    def send(self, message: str) -> None:
        print(message)

ConsoleNotifier().send("hello")
# 输出: hello

# 示例类型:最小示例

实际场景示例

领域层定义 Protocol,基础设施层实现它,服务通过构造器接收依赖;这样测试可注入内存实现。

python 复制代码
# 示例目标:服务依赖协议
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

from typing import Protocol

class Clock(Protocol):
    def now(self) -> int: ...

class Service:
    def __init__(self, clock: Clock) -> None:
        self.clock = clock

    def stamp(self) -> int:
        return self.clock.now()

class FixedClock:
    def now(self) -> int:
        return 42

print(Service(FixedClock()).stamp())
# 输出: 42

# 示例类型:实际场景

关联/组合

组合 1:组合替代继承
python 复制代码
# 示例目标:组合替代继承
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

class Logger:
    def log(self, message: str) -> None:
        print(message)

class Service:
    def __init__(self, logger: Logger) -> None:
        self.logger = logger

    def run(self) -> None:
        self.logger.log("run")

Service(Logger()).run()
# 输出: run

# 示例类型:关联/组合
组合 2:runtime_checkable
python 复制代码
# 示例目标:runtime_checkable
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

from typing import Protocol, runtime_checkable

@runtime_checkable
class Named(Protocol):
    name: str

class User:
    name = "ann"

print(isinstance(User(), Named))
# 输出: True

# 示例类型:关联/组合

易错/易混

易错 1:把 Protocol 当运行时校验

错误代码:

python 复制代码
# 示例目标:把 Protocol 当运行时校验(错误示范)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

from typing import Protocol

class Repo(Protocol):
    def save(self, title: str) -> str: ...

class Bad:
    def save(self, number: int) -> int:
        return number

# 示例类型:错误示范

实际结果:静态检查器可能报错,但运行时不会自动拒绝 Bad。

修正代码:

python 复制代码
# 示例目标:把 Protocol 当运行时校验(修正)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

class Bad:
    def save(self, title: str) -> str:
        return title

print(Bad().save("ok"))
# 输出: ok

# 示例类型:易错修正

原因:Protocol 主要服务静态检查,边界仍需运行时验证。

易错 2:继承具体类只为复用

错误代码:

python 复制代码
# 示例目标:继承具体类只为复用(错误示范)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

class Logger:
    def log(self, message: str) -> None:
        print(message)

class Service(Logger):
    pass

# 示例类型:错误示范

实际结果:Service 获得了 Logger 的接口,但两者没有真正的 is-a 关系。

修正代码:

python 复制代码
# 示例目标:继承具体类只为复用(修正)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行

class Logger:
    def log(self, message: str) -> None:
        print(message)

class Service:
    def __init__(self, logger: Logger) -> None:
        self.logger = logger

Service(Logger()).logger.log("ok")
# 输出: ok

# 示例类型:易错修正

原因:组合表达 has-a 关系,继承表达 is-a 关系。

练习

  1. 定义 Repository Protocol 和 MemoryRepository 实现。
  2. 写一个抽象 Notifier,并实现 ConsoleNotifier。

4.8 枚举、常量与状态建模

前置知识:4.5 dataclass;4.2 注解。

概念:Enum 用命名成员表达有限集合,避免魔法字符串。StrEnum 适合 JSON/API,IntEnum 与整数兼容,Flag 适合组合权限;非法成员值会在转换时尽早失败。

本单元新语法

语法形式 含义
class Status(Enum): 枚举成员
StrEnum 字符串兼容枚举
IntEnum 整数兼容枚举
Flag 可组合位标志
Member.value 成员底层值
Status(raw) 按值查找成员

最小示例

最小示例 1:任务状态
python 复制代码
# 示例目标:任务状态
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:完整可运行

from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

status = Status("done")
print(status.value)
# 输出: done

# 示例类型:最小示例
最小示例 2:枚举在匹配中
python 复制代码
# 示例目标:枚举在匹配中
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:完整可运行

from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

def label(status: Status) -> str:
    match status:
        case Status.DONE:
            return "finished"
        case _:
            return "open"

print(label(Status.DONE))
# 输出: finished

# 示例类型:最小示例

实际场景示例

订单状态、任务状态、用户角色和事件类型使用枚举;数据库历史值变更仍需迁移和兼容处理。

python 复制代码
# 示例目标:状态字段转换
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

def normalize(raw: str) -> Status:
    return Status(raw)

print(normalize("pending"))
# 输出: pending

# 示例类型:实际场景

关联/组合

组合 1:IntFlag 权限
python 复制代码
# 示例目标:IntFlag 权限
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

from enum import IntFlag

class Permission(IntFlag):
    READ = 1
    WRITE = 2

print(Permission.READ | Permission.WRITE)
# 输出: Permission.READ|WRITE

# 示例类型:关联/组合
组合 2:成员名与值
python 复制代码
# 示例目标:成员名与值
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

from enum import StrEnum

class Status(StrEnum):
    DONE = "done"

print(Status.DONE.name, Status.DONE.value)
# 输出: DONE done

# 示例类型:关联/组合

易错/易混

易错 1:用字符串比较代替枚举

错误代码:

python 复制代码
# 示例目标:用字符串比较代替枚举(错误示范)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

status = "done"
if status == "Done":
    print("finished")

# 示例类型:错误示范

实际结果:大小写不一致导致逻辑静默失败。

修正代码:

python 复制代码
# 示例目标:用字符串比较代替枚举(修正)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

from enum import StrEnum

class Status(StrEnum):
    DONE = "done"

status = Status("done")
print(status is Status.DONE)
# 输出: True

# 示例类型:易错修正

原因:枚举集中管理合法值和比较语义。

易错 2:把未知值强行当成员

错误代码:

python 复制代码
# 示例目标:把未知值强行当成员(错误示范)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

from enum import StrEnum

class Status(StrEnum):
    DONE = "done"

print(Status("unknown"))

# 示例类型:错误示范

实际结果:抛出 ValueError。

修正代码:

python 复制代码
# 示例目标:把未知值强行当成员(修正)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行

from enum import StrEnum

class Status(StrEnum):
    DONE = "done"

try:
    Status("unknown")
except ValueError as exc:
    print("invalid status", exc)
# 输出: invalid status 'unknown' is not a valid Status

# 示例类型:易错修正

原因:外部值转换应捕获并翻译错误。

练习

  1. 定义 Status 枚举,包含 pending 和 done。
  2. 用 match 对 Status.DONE 返回 finished。

4.9 泛型、TypedDict、Literal 与静态检查

前置知识:4.2 注解;4.7 Protocol;4.8 枚举。

概念:泛型保持容器和函数中的类型关系;TypedDict 描述字典键;Literal 限定字面量;Annotated 附加元数据。它们主要服务静态检查,运行时通常不验证。

本单元新语法

语法形式 含义
TypeVar("T") 类型变量
Generic[T] 泛型类
TypedDict 带键类型的字典
Literal[...] 字面量集合
Annotated[T, metadata] 给类型附加元数据
TYPE_CHECKING 仅静态检查时导入

最小示例

最小示例 1:泛型 first
python 复制代码
# 示例目标:泛型 first
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:完整可运行

from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

print(first([1, 2]))
print(first(["a"]))
# 输出: 1
# 输出: a

# 示例类型:最小示例
最小示例 2:TypedDict
python 复制代码
# 示例目标:TypedDict
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:完整可运行

from typing import TypedDict

class TaskPayload(TypedDict):
    title: str
    done: bool

payload: TaskPayload = {"title": "Read", "done": False}
print(payload["title"])
# 输出: Read

# 示例类型:最小示例

实际场景示例

公共 SDK、通用仓储和 API 数据结构需要精确类型;运行时请求校验仍交给 Pydantic。

python 复制代码
# 示例目标:Literal 工作流
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

from typing import Literal

def priority(value: Literal["low", "high"]) -> str:
    return value

print(priority("high"))
# 输出: high

# 示例类型:实际场景

关联/组合

组合 1:TYPE_CHECKING 导入
python 复制代码
# 示例目标:TYPE_CHECKING 导入
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from decimal import Decimal

def amount(value: "Decimal") -> str:
    return str(value)

# 示例类型:关联/组合
组合 2:NotRequired
python 复制代码
# 示例目标:NotRequired
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

from typing import NotRequired, TypedDict

class Payload(TypedDict):
    title: str
    notes: NotRequired[str]

payload: Payload = {"title": "Read"}
print(payload)
# 输出: {'title': 'Read'}

# 示例类型:关联/组合

易错/易混

易错 1:把 TypedDict 当运行时校验

错误代码:

python 复制代码
# 示例目标:把 TypedDict 当运行时校验(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

from typing import TypedDict

class Payload(TypedDict):
    title: str

data: Payload = {"title": 123}
print(data)

# 示例类型:错误示范

实际结果:运行时不会报错,类型检查器才会提示。

修正代码:

python 复制代码
# 示例目标:把 TypedDict 当运行时校验(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

from typing import TypedDict

class Payload(TypedDict):
    title: str

data = {"title": 123}
if not isinstance(data["title"], str):
    raise TypeError("title must be str")
print(data)

# 示例类型:易错修正

原因:静态类型不等于运行时校验。

易错 2:用 Any 逃避检查

错误代码:

python 复制代码
# 示例目标:用 Any 逃避检查(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

from typing import Any

def parse(value: Any) -> Any:
    return value

# 示例类型:错误示范

实际结果:任何调用都通过,错误推迟到运行时。

修正代码:

python 复制代码
# 示例目标:用 Any 逃避检查(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行

def parse(value: str) -> str:
    return value.strip()

print(parse(" hi "))
# 输出: hi

# 示例类型:易错修正

原因:尽量缩小 Any 范围,边界使用明确类型。

练习

  1. 写一个泛型 first(items: list[T]) -> T。
  2. 用 TypedDict 描述 {title: str, done: bool}。

4.10 复制、相等、哈希与 slots

前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。

概念 :相等性比较值,身份比较对象,哈希用于快速查找。值对象若可哈希,应保证相等对象哈希相同;__slots__ 限制实例属性并减少内存,但牺牲动态属性和部分缓存能力。

本单元新语法

语法形式 含义
copy.copy 浅复制
copy.deepcopy 递归复制
__eq__ 相等性
__hash__ 哈希
__slots__ 固定实例属性
weakref 不阻止回收的弱引用

最小示例

最小示例 1:deepcopy 隔离嵌套
python 复制代码
# 示例目标:deepcopy 隔离嵌套
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:完整可运行

from copy import deepcopy

original = {"tags": ["a"]}
copy = deepcopy(original)
copy["tags"].append("b")
print(original, copy)
# 输出: {'tags': ['a']} {'tags': ['a', 'b']}

# 示例类型:最小示例
最小示例 2:slots 减少属性字典
python 复制代码
# 示例目标:slots 减少属性字典
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:完整可运行

class Point:
    __slots__ = ("x", "y")
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

print(Point(1, 2).x)
# 输出: 1

# 示例类型:最小示例

实际场景示例

缓存键、集合成员和领域值对象需要正确哈希;大量小对象可用 slots 降低内存。

python 复制代码
# 示例目标:可哈希值对象
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

class Point:
    __slots__ = ("x", "y")
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y
    def __eq__(self, other: object) -> bool:
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
    def __hash__(self) -> int:
        return hash((self.x, self.y))

print(len({Point(1, 2), Point(1, 2)}))
# 输出: 1

# 示例类型:实际场景

关联/组合

组合 1:浅复制嵌套
python 复制代码
# 示例目标:浅复制嵌套
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

from copy import copy

original = {"items": [1]}
clone = copy(original)
clone["items"].append(2)
print(original)
# 输出: {'items': [1, 2]}

# 示例类型:关联/组合
组合 2:弱引用
python 复制代码
# 示例目标:弱引用
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

import weakref

class Task:
    pass

task = Task()
ref = weakref.ref(task)
print(ref() is task)
# 输出: True

# 示例类型:关联/组合

易错/易混

易错 1:自定义 eq 不自定义 hash

错误代码:

python 复制代码
# 示例目标:自定义 eq 不自定义 hash(错误示范)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

class Task:
    def __eq__(self, other: object) -> bool:
        return True

print(hash(Task()))

# 示例类型:错误示范

实际结果:抛出 TypeError:unhashable type。

修正代码:

python 复制代码
# 示例目标:自定义 eq 不自定义 hash(修正)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

class Task:
    def __eq__(self, other: object) -> bool:
        return isinstance(other, Task)
    def __hash__(self) -> int:
        return 1
print(hash(Task()))
# 输出: 1

# 示例类型:易错修正

原因:相等性和哈希必须保持一致。

易错 2:对 slots 对象添加未声明属性

错误代码:

python 复制代码
# 示例目标:对 slots 对象添加未声明属性(错误示范)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

class Task:
    __slots__ = ("title",)
    def __init__(self) -> None:
        self.title = "Read"

task = Task()
task.owner = "ann"

# 示例类型:错误示范

实际结果:抛出 AttributeError:没有 owner 槽位。

修正代码:

python 复制代码
# 示例目标:对 slots 对象添加未声明属性(修正)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行

class Task:
    __slots__ = ("title", "owner")
    def __init__(self) -> None:
        self.title = "Read"
        self.owner = "ann"

task = Task()
print(task.owner)
# 输出: ann

# 示例类型:易错修正

原因:slots 必须提前列出所有实例属性。

练习

  1. 用 deepcopy 隔离嵌套列表。
  2. 用 slots 定义只有 x、y 的 Point。

4.11 OOP 与类型项目线:TaskFlow v04

前置知识:4.1~4.10。

概念:项目线把任务从字典升级为 frozen dataclass,把状态升级为 StrEnum,把仓储需求表达为 Protocol;服务只依赖协议,不依赖具体存储。

本单元新语法

语法形式 含义
@dataclass(frozen=True) 不可变任务
StrEnum 有限状态
Protocol 仓储接口
replace 派生新状态
类型注解 接口契约

最小示例

最小示例 1:任务模型
python 复制代码
# 示例目标:任务模型
# 前置知识:4.1~4.10。
# 代码状态:完整可运行

from dataclasses import dataclass
from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

@dataclass(frozen=True)
class Task:
    id: str
    title: str
    status: Status = Status.PENDING

    def complete(self) -> "Task":
        from dataclasses import replace
        return replace(self, status=Status.DONE)

print(Task("a", "Read").complete())
# 输出包含: status=<Status.DONE: 'done'>

# 示例类型:最小示例
最小示例 2:仓储协议
python 复制代码
# 示例目标:仓储协议
# 前置知识:4.1~4.10。
# 代码状态:完整可运行

from typing import Protocol

class Repository(Protocol):
    def add(self, title: str) -> str: ...

class MemoryRepository:
    def add(self, title: str) -> str:
        return "stored:" + title

print(MemoryRepository().add("Read"))
# 输出: stored:Read

# 示例类型:最小示例

实际场景示例

这是从"脚本 + 字典"到"领域模型 + 接口"的关键跃迁;后续标准库和 Web 层都依赖这组核心类型。

python 复制代码
# 示例目标:服务依赖协议
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

from typing import Protocol

class Repository(Protocol):
    def add(self, title: str) -> str: ...

class Service:
    def __init__(self, repo: Repository) -> None:
        self.repo = repo
    def create(self, title: str) -> str:
        return self.repo.add(title.strip())

class MemoryRepository:
    def add(self, title: str) -> str:
        return title

print(Service(MemoryRepository()).create(" Read "))
# 输出: Read

# 示例类型:实际场景

关联/组合

组合 1:用枚举状态
python 复制代码
# 示例目标:用枚举状态
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

print(Status.PENDING)
# 输出: pending

# 示例类型:关联/组合
组合 2:用 replace 完成任务
python 复制代码
# 示例目标:用 replace 完成任务
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, replace
from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

@dataclass(frozen=True)
class Task:
    title: str
    status: Status = Status.PENDING

print(replace(Task("Read"), status=Status.DONE).status)
# 输出: done

# 示例类型:关联/组合

易错/易混

易错 1:服务直接依赖具体仓储

错误代码:

python 复制代码
# 示例目标:服务直接依赖具体仓储(错误示范)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

class Service:
    def __init__(self) -> None:
        self.repo = SqliteRepository()

# 示例类型:错误示范

实际结果:测试时无法替换存储,服务与数据库耦合。

修正代码:

python 复制代码
# 示例目标:服务直接依赖具体仓储(修正)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

class Service:
    def __init__(self, repo) -> None:
        self.repo = repo

# 示例类型:易错修正

原因:依赖从构造器注入,生产/测试可替换。

易错 2:状态仍用裸字符串

错误代码:

python 复制代码
# 示例目标:状态仍用裸字符串(错误示范)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

task = {"status": "done"}
if task["status"] == "Done":
    print("ok")

# 示例类型:错误示范

实际结果:大小写不一致导致条件失败。

修正代码:

python 复制代码
# 示例目标:状态仍用裸字符串(修正)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行

from enum import StrEnum
class Status(StrEnum):
    DONE = "done"
task = {"status": Status.DONE}
print(task["status"] is Status.DONE)
# 输出: True

# 示例类型:易错修正

原因:枚举把合法值和比较语义集中管理。

练习

  1. 用 frozen dataclass 定义 Task,并在 complete 中返回新对象。
  2. 定义 Repository Protocol 和 MemoryRepository。

4.12 dataclass 进阶:校验、字段策略与继承

前置知识:4.5 dataclass;4.2 注解;3.7 异常。

概念 :dataclass 的字段可以分别控制默认值、比较、repr 和初始化方式。__post_init__ 适合派生字段和轻量不变量校验;kw_only 让构造参数必须使用关键字,减少位置错误。

本单元新语法

语法形式 含义
field(default_factory=...) 每实例默认值
field(compare=False) 排除相等性比较
__post_init__ 初始化后的派生/校验
kw_only=True 仅关键字构造
inheritance 子类字段继承
asdict(obj) 递归转字典

最小示例

最小示例 1:post_init 派生字段
python 复制代码
# 示例目标:post_init 派生字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:完整可运行

from dataclasses import dataclass, field

@dataclass
class Task:
    title: str
    tags: list[str] = field(default_factory=list)
    tag_count: int = field(init=False)

    def __post_init__(self) -> None:
        self.tag_count = len(self.tags)

print(Task("Read", ["work"]).tag_count)
# 输出: 1

# 示例类型:最小示例
最小示例 2:仅关键字字段
python 复制代码
# 示例目标:仅关键字字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:完整可运行

from dataclasses import dataclass

@dataclass(kw_only=True)
class Task:
    title: str
    urgent: bool = False

print(Task(title="Read", urgent=True))
# 输出: Task(title='Read', urgent=True)

# 示例类型:最小示例
最小示例 3:asdict
python 复制代码
# 示例目标:asdict
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:完整可运行

from dataclasses import asdict, dataclass

@dataclass
class Task:
    id: str
    title: str

print(asdict(Task("a", "Read")))
# 输出: {'id': 'a', 'title': 'Read'}

# 示例类型:最小示例

实际场景示例

API DTO、配置快照和测试数据常用 dataclass;外部 JSON 仍要先验证类型,再转换成不可信字段受限的领域对象。

python 复制代码
# 示例目标:比较忽略内部字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, field

@dataclass
class Task:
    title: str
    revision: int = field(default=0, compare=False)

print(Task("Read", 1) == Task("Read", 2))
# 输出: True

# 示例类型:实际场景

关联/组合

组合 1:继承字段
python 复制代码
# 示例目标:继承字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass
class Base:
    title: str

@dataclass
class Urgent(Base):
    priority: int = 1

print(Urgent("Read", 2))
# 输出: Urgent(title='Read', priority=2)

# 示例类型:关联/组合
组合 2:默认值依赖
python 复制代码
# 示例目标:默认值依赖
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, field

@dataclass
class Box:
    items: tuple[str, ...] = field(default_factory=tuple)

print(Box().items)
# 输出: ()

# 示例类型:关联/组合
组合 3:只读派生字段
python 复制代码
# 示例目标:只读派生字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, field

@dataclass
class Task:
    title: str
    slug: str = field(init=False)
    def __post_init__(self) -> None:
        self.slug = self.title.lower().replace(" ", "-")

print(Task("Read Docs").slug)
# 输出: read-docs

# 示例类型:关联/组合

易错/易混

易错 1:在 post_init 中修改 frozen 字段

错误代码:

python 复制代码
# 示例目标:在 post_init 中修改 frozen 字段(错误示范)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass(frozen=True)
class Task:
    title: str
    slug: str = ""
    def __post_init__(self) -> None:
        self.slug = self.title.lower()

# 示例类型:错误示范

实际结果:抛出 FrozenInstanceError:frozen 对象不能直接赋值。

修正代码:

python 复制代码
# 示例目标:在 post_init 中修改 frozen 字段(修正)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass, field

@dataclass(frozen=True)
class Task:
    title: str
    slug: str = field(init=False)
    def __post_init__(self) -> None:
        object.__setattr__(self, "slug", self.title.lower())

print(Task("Read").slug)
# 输出: read

# 示例类型:易错修正

原因 :frozen 派生字段使用 object.setattr,并把它限制在初始化阶段。

易错 2:依赖子类字段顺序

错误代码:

python 复制代码
# 示例目标:依赖子类字段顺序(错误示范)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass
class Base:
    title: str

@dataclass
class Child(Base):
    title: str = "default"

# 示例类型:错误示范

实际结果:可能触发 TypeError:默认值字段覆盖无默认值父类字段。

修正代码:

python 复制代码
# 示例目标:依赖子类字段顺序(修正)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行

from dataclasses import dataclass

@dataclass
class Base:
    title: str

@dataclass
class Child(Base):
    title: str
    priority: int = 1

print(Child("Read"))
# 输出: Child(title='Read', priority=1)

# 示例类型:易错修正

原因:继承时保持字段顺序和默认值规则一致。

练习

  1. 用 dataclass 和 post_init 计算 tag_count。
  2. 定义一个 kw_only dataclass,包含 title 和 urgent。

4.13 对象协议进阶:可调用、索引与上下文

前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。

概念 :除 __eq__ 和 __repr__ 外,对象还可以实现调用、索引、成员测试、加法和上下文管理协议。这些协议让自定义对象自然融入 Python 语法,但每个协议都要保持行为一致和可预测。

本单元新语法

语法形式 含义
__call__(self, ...) 对象可像函数一样调用
__getitem__(self, key) 支持索引/切片
__contains__(self, item) 支持 in
__add__(self, other) 支持 +
__enter__/__exit__ 支持 with
total_ordering 生成比较方法

最小示例

最小示例 1:可调用对象
python 复制代码
# 示例目标:可调用对象
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:完整可运行

class Multiplier:
    def __init__(self, factor: int) -> None:
        self.factor = factor
    def __call__(self, value: int) -> int:
        return value * self.factor

triple = Multiplier(3)
print(triple(4))
# 输出: 12

# 示例类型:最小示例
最小示例 2:索引协议
python 复制代码
# 示例目标:索引协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:完整可运行

class Numbers:
    def __init__(self, values: list[int]) -> None:
        self.values = values
    def __getitem__(self, index: int) -> int:
        return self.values[index]

print(Numbers([10, 20])[1])
# 输出: 20

# 示例类型:最小示例
最小示例 3:上下文管理协议
python 复制代码
# 示例目标:上下文管理协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:完整可运行

class Resource:
    def __enter__(self):
        print("open")
        return "value"
    def __exit__(self, exc_type, exc, tb):
        print("close")

with Resource() as value:
    print(value)
# 输出: open
# 输出: value
# 输出: close

# 示例类型:最小示例

实际场景示例

领域对象、查询结果包装器、连接池和上下文工具都可能实现这些协议;接口越像内置对象,越要保证错误和边界行为一致。

python 复制代码
# 示例目标:任务集合可迭代
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Tasks:
    def __init__(self, items: list[str]) -> None:
        self.items = items
    def __iter__(self):
        return iter(self.items)
    def __len__(self) -> int:
        return len(self.items)

tasks = Tasks(["a", "b"])
print(len(tasks), list(tasks))
# 输出: 2 ['a', 'b']

# 示例类型:实际场景

关联/组合

组合 1:加法协议
python 复制代码
# 示例目标:加法协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Money:
    def __init__(self, cents: int) -> None:
        self.cents = cents
    def __add__(self, other: "Money") -> "Money":
        return Money(self.cents + other.cents)

print(Money(100) + Money(50))
# 输出: <__main__.Money object at ...>

# 示例类型:关联/组合
组合 2:成员测试
python 复制代码
# 示例目标:成员测试
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Tags:
    def __init__(self, values: list[str]) -> None:
        self.values = values
    def __contains__(self, item: str) -> bool:
        return item in self.values

print("work" in Tags(["work"]))
# 输出: True

# 示例类型:关联/组合
组合 3:排序协议
python 复制代码
# 示例目标:排序协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, number: int) -> None:
        self.number = number
    def __eq__(self, other: object) -> bool:
        return isinstance(other, Version) and self.number == other.number
    def __lt__(self, other: "Version") -> bool:
        return self.number < other.number

print(Version(2) > Version(1))
# 输出: True

# 示例类型:关联/组合

易错/易混

易错 1:只实现 getitem 却假设长度

错误代码:

python 复制代码
# 示例目标:只实现 getitem 却假设长度(错误示范)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Values:
    def __getitem__(self, index):
        return index

print(len(Values()))

# 示例类型:错误示范

实际结果 :抛出 TypeError:对象没有 len。

修正代码:

python 复制代码
# 示例目标:只实现 getitem 却假设长度(修正)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Values:
    def __getitem__(self, index):
        return index
    def __len__(self):
        return 3

print(len(Values()))
# 输出: 3

# 示例类型:易错修正

原因:每个语法协议对应明确的魔术方法。

易错 2:exit 返回 True 吞掉异常

错误代码:

python 复制代码
# 示例目标:__exit__ 返回 True 吞掉异常(错误示范)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Hidden:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc, tb):
        return True

with Hidden():
    raise ValueError("hidden")

# 示例类型:错误示范

实际结果:主体异常被静默吞掉,调用方无法感知失败。

修正代码:

python 复制代码
# 示例目标:__exit__ 返回 True 吞掉异常(修正)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行

class Visible:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc, tb):
        return False

try:
    with Visible():
        raise ValueError("visible")
except ValueError as exc:
    print(exc)
# 输出: visible

# 示例类型:易错修正

原因 :只有明确需要抑制时才让 exit 返回 True。

练习

  1. 实现 call,让对象把数字翻倍。
  2. 实现 contains,让对象支持成员测试。

4.14 typing 进阶:Callable、TypeVar 与 overload

前置知识:4.2 注解;4.7 Protocol;4.9 泛型。

概念 :类型系统不仅要描述值,还要描述函数、类型之间的关系和重载。Callable 表示函数签名,TypeVar 保留输入输出关系,overload 只提供静态签名,最后一个实现仍然是运行时真正执行的函数。

本单元新语法

语法形式 含义
Callable[[A], B] 函数类型
TypeVar(bound=Base) 受限类型变量
NewType("Id", str) 语义化基础类型
@overload 静态重载声明
ParamSpec 转发参数签名
Literal[...] 限定字面量

最小示例

最小示例 1:Callable 参数
python 复制代码
# 示例目标:Callable 参数
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:完整可运行

from typing import Callable

def apply(fn: Callable[[int], int], value: int) -> int:
    return fn(value)

print(apply(abs, -3))
# 输出: 3

# 示例类型:最小示例
最小示例 2:NewType
python 复制代码
# 示例目标:NewType
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:完整可运行

from typing import NewType

TaskId = NewType("TaskId", str)
task_id = TaskId("t1")
print(task_id)
# 输出: t1

# 示例类型:最小示例
最小示例 3:TypeVar bound
python 复制代码
# 示例目标:TypeVar bound
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:完整可运行

from typing import TypeVar

class Base:
    pass

T = TypeVar("T", bound=Base)

def identity(value: T) -> T:
    return value

print(identity(Base()) is not None)
# 输出: True

# 示例类型:最小示例

实际场景示例

公共 SDK、函数式管道和通用仓储需要精确函数签名;业务内部不要为了类型而堆叠泛型。

python 复制代码
# 示例目标:重载的静态描述
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import overload

@overload
def parse(value: str) -> int: ...
@overload
def parse(value: bytes) -> int: ...
def parse(value):
    return int(value)

print(parse("3"))
# 输出: 3

# 示例类型:实际场景

关联/组合

组合 1:Callable 列表
python 复制代码
# 示例目标:Callable 列表
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import Callable

handlers: list[Callable[[], str]] = [lambda: "a", lambda: "b"]
print([handler() for handler in handlers])
# 输出: ['a', 'b']

# 示例类型:关联/组合
组合 2:TypeVar 保持关系
python 复制代码
# 示例目标:TypeVar 保持关系
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import TypeVar

T = TypeVar("T")
def first(items: list[T]) -> T:
    return items[0]

print(first(["a"]))
# 输出: a

# 示例类型:关联/组合
组合 3:字面量类型
python 复制代码
# 示例目标:字面量类型
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import Literal

def priority(value: Literal["low", "high"]) -> str:
    return value

print(priority("high"))
# 输出: high

# 示例类型:关联/组合

易错/易混

易错 1:把 cast 当转换

错误代码:

python 复制代码
# 示例目标:把 cast 当转换(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import cast

def parse(value: str) -> int:
    return cast(int, value)

# 示例类型:错误示范

实际结果:cast 运行时仍返回原字符串,后续运算会失败。

修正代码:

python 复制代码
# 示例目标:把 cast 当转换(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

def parse(value: str) -> int:
    return int(value)

print(parse("3"))
# 输出: 3

# 示例类型:易错修正

原因:cast 只是静态承诺,不执行转换。

易错 2:为 overload 写多个实现

错误代码:

python 复制代码
# 示例目标:为 overload 写多个实现(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import overload

@overload
def parse(value: str) -> int: ...
def parse(value):
    return int(value)

# 示例类型:错误示范

实际结果:可以运行,但 overload 函数体应是省略号,只有一个真实实现。

修正代码:

python 复制代码
# 示例目标:为 overload 写多个实现(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行

from typing import overload

@overload
def parse(value: str) -> int: ...
@overload
def parse(value: bytes) -> int: ...
def parse(value):
    return int(value)

print(parse("3"))
# 输出: 3

# 示例类型:易错修正

原因:重载是静态接口,不是运行时多分派。

练习

  1. 定义一个接收 Callable[[int], int] 的高阶函数。
  2. 用 TypeVar 编写返回列表首元素的泛型函数。

4.15 泛型接口与依赖注入组合

前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。

概念:协议可以带类型变量,表达"仓储保存某种实体";服务依赖协议而不依赖具体实现。泛型接口适合通用组件,但业务用例仍应保持简单、显式。

本单元新语法

语法形式 含义
Generic[T] 泛型接口
Protocol + TypeVar 带类型的结构化接口
constructor injection 构造器注入依赖
Repository[T] 泛型仓储
fake implementation 测试替身
type narrowing 按分支缩小类型

最小示例

最小示例 1:泛型协议
python 复制代码
# 示例目标:泛型协议
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:完整可运行

from typing import Protocol, TypeVar

T = TypeVar("T")

class Store(Protocol[T]):
    def save(self, value: T) -> None: ...

class MemoryStore:
    def __init__(self) -> None:
        self.values = []
    def save(self, value: int) -> None:
        self.values.append(value)

store = MemoryStore()
store.save(3)
print(store.values)
# 输出: [3]

# 示例类型:最小示例
最小示例 2:服务依赖协议
python 复制代码
# 示例目标:服务依赖协议
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:完整可运行

from typing import Protocol

class Repo(Protocol):
    def add(self, title: str) -> str: ...

class Service:
    def __init__(self, repo: Repo) -> None:
        self.repo = repo
    def create(self, title: str) -> str:
        return self.repo.add(title.strip())

class FakeRepo:
    def add(self, title: str) -> str:
        return "fake:" + title

print(Service(FakeRepo()).create(" Read "))
# 输出: fake:Read

# 示例类型:最小示例
最小示例 3:测试替身
python 复制代码
# 示例目标:测试替身
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:完整可运行

class FakeClock:
    def now(self) -> int:
        return 42

class Service:
    def __init__(self, clock) -> None:
        self.clock = clock
    def stamp(self) -> int:
        return self.clock.now()

print(Service(FakeClock()).stamp())
# 输出: 42

# 示例类型:最小示例

实际场景示例

分层应用通过协议隔离数据库/HTTP/时钟;测试注入内存实现,生产注入真实适配器。

python 复制代码
# 示例目标:泛型仓储的职责
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

from typing import Protocol, TypeVar

T = TypeVar("T")
class Reader(Protocol[T]):
    def get(self, key: str) -> T | None: ...

class DictReader:
    def __init__(self) -> None:
        self.data = {"a": 1}
    def get(self, key: str) -> int | None:
        return self.data.get(key)

reader: Reader[int] = DictReader()
print(reader.get("a"))
# 输出: 1

# 示例类型:实际场景

关联/组合

组合 1:依赖注入函数
python 复制代码
# 示例目标:依赖注入函数
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

def build_service(repo):
    return {"repo": repo}

service = build_service({"add": str})
print(service["repo"]["add"]("x"))
# 输出: x

# 示例类型:关联/组合
组合 2:泛型容器
python 复制代码
# 示例目标:泛型容器
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
    return items[0]
print(first([1]))
# 输出: 1

# 示例类型:关联/组合
组合 3:协议作为边界
python 复制代码
# 示例目标:协议作为边界
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

from typing import Protocol
class Notifier(Protocol):
    def send(self, text: str) -> None: ...
class Console:
    def send(self, text: str) -> None:
        print(text)
Console().send("hello")
# 输出: hello

# 示例类型:关联/组合

易错/易混

易错 1:把具体数据库放进领域对象

错误代码:

python 复制代码
# 示例目标:把具体数据库放进领域对象(错误示范)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

class Task:
    def save(self):
        return database.connect("tasks.db")

# 示例类型:错误示范

实际结果:领域对象与数据库驱动绑定,测试和替换困难。

修正代码:

python 复制代码
# 示例目标:把具体数据库放进领域对象(修正)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

class Task:
    pass

def save(repository, task):
    return repository.save(task)

# 示例类型:易错修正

原因:领域对象只表达业务状态,持久化通过仓储边界完成。

易错 2:为每个类都创建协议

错误代码:

python 复制代码
# 示例目标:为每个类都创建协议(错误示范)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

class Everything(Protocol):
    def do(self): ...

# 示例类型:错误示范

实际结果:接口膨胀,阅读和实现成本上升。

修正代码:

python 复制代码
# 示例目标:为每个类都创建协议(修正)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行

class Repo(Protocol):
    def save(self, title: str) -> str: ...

# 示例类型:易错修正

原因:只在真实变化点和测试边界抽象。

练习

  1. 定义 Store[T] 协议,并提供内存实现。
  2. 写一个依赖 Clock 协议的服务。

本卷综合实验室:类型化任务仓储

实验目标

用 dataclass、Enum、Protocol、类型注解和不可变值对象,把任务数据模型与存储实现分开。这个实验强调接口和边界,不追求真实数据库。

完整实现

python 复制代码
# 示例目标:类型化任务仓储
# 前置知识:第 4 卷 dataclass、Enum、Protocol、typing、property
# 代码状态:完整可运行

from dataclasses import dataclass, field, replace
from enum import StrEnum
from typing import Protocol

class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"

@dataclass(frozen=True)
class Task:
    id: str
    title: str
    status: Status = Status.PENDING
    tags: tuple[str, ...] = ()

    def __post_init__(self):
        if not self.title.strip():
            raise ValueError("title required")

    def complete(self):
        return replace(self, status=Status.DONE)

class Repository(Protocol):
    def add(self, task: Task) -> Task: ...
    def update(self, task: Task) -> Task: ...
    def get(self, task_id: str) -> Task | None: ...
    def list(self) -> list[Task]: ...

class MemoryRepository:
    def __init__(self):
        self.items = {}

    def add(self, task: Task) -> Task:
        if task.id in self.items:
            raise ValueError("duplicate id")
        self.items[task.id] = task
        return task

    def get(self, task_id: str) -> Task | None:
        return self.items.get(task_id)

    def update(self, task: Task) -> Task:
        self.items[task.id] = task
        return task

    def list(self) -> list[Task]:
        return sorted(self.items.values(), key=lambda task: task.id)

class TaskService:
    def __init__(self, repository: Repository):
        self.repository = repository

    def create(self, task_id: str, title: str, tags=()) -> Task:
        task = Task(task_id, title.strip(), tags=tuple(tags))
        return self.repository.add(task)

    def complete(self, task_id: str) -> Task:
        task = self.repository.get(task_id)
        if task is None:
            raise LookupError("task not found")
        completed = task.complete()
        self.repository.update(completed)
        return completed

repository = MemoryRepository()
service = TaskService(repository)
task = service.create("t1", " Read ", tags=("work",))
print(task)
print(service.complete("t1"))

# 输出:
# Task(id='t1', title='Read', status=<Status.PENDING: 'pending'>, tags=('work',))
# Task(id='t1', title='Read', status=<Status.DONE: 'done'>, tags=('work',))

关键解释

  1. @dataclass(frozen=True) 让任务成为值对象;complete() 不修改旧对象,而是 replace 出新对象。
  2. __post_init__ 只做轻量不变量校验,外部 JSON 仍需专门解析/验证。
  3. Repository 是结构化接口,内存实现和未来 SQLite 实现都可以满足。
  4. TaskService 只依赖 Protocol,不依赖具体存储;这是依赖倒置的最小形态。
  5. list() 排序使用命名函数,保持输出稳定,便于测试和 diff。

本卷关联

  • 类与初始化来自 4.1,注解来自 4.2。
  • 继承/组合来自 4.3,魔术方法来自 4.4。
  • dataclass 进阶来自 4.12,Protocol/泛型来自 4.7/4.15。

验收标准

  • 空标题在创建时失败。
  • 完成任务不修改原对象。
  • 重复 ID 被拒绝。
  • 服务只依赖仓储接口,可替换内存实现。

复习问答

问:为什么 Task 不用可变 dataclass?

答:任务状态变化可以用 replace 表达新快照,避免共享对象在缓存、日志和测试间被悄悄修改。

问:为什么 TypeVar 没有出现在这个实验里?

答:泛型是通用组件工具,当前任务仓储只需要具体 Task;不要为了类型而增加无实际收益的泛型参数。


本卷练习答案

先独立完成练习,再展开答案;答案只使用本卷及之前介绍过的知识。
4.1 类、实例与方法 练习答案

练习 1 :写一个 Task 类,包含 title 和 done,并实现 complete 方法。

python 复制代码
class Task:
    def __init__(self, title):
        self.title = title
        self.done = False
    def complete(self):
        self.done = True
t = Task("Read")
t.complete()
print(t.done)

练习 2 :写一个类方法 empty() 创建标题为 untitled 的实例。

python 复制代码
class Task:
    def __init__(self, title):
        self.title = title
    @classmethod
    def empty(cls):
        return cls("untitled")
print(Task.empty().title)

4.2 类型注解与函数接口 练习答案

练习 1 :给 double(value) 加参数和返回值注解。

python 复制代码
def double(value: int) -> int:
    return value * 2
print(double(3))

练习 2 :写一个返回 str | None 的查找函数。

python 复制代码
def find(items: list[str], target: str) -> str | None:
    return target if target in items else None
print(find([], "a"))

4.3 继承、MRO 与 super 练习答案

练习 1:写一个父类和子类,子类在 describe 中调用 super。

python 复制代码
class Base:
    def describe(self) -> str:
        return "base"
class Child(Base):
    def describe(self) -> str:
        return "child " + super().describe()
print(Child().describe())

练习 2:打印一个类的 MRO 名称。

python 复制代码
class A:
    pass
class B(A):
    pass
print([c.__name__ for c in B.__mro__])

4.4 魔术方法与对象协议 练习答案

练习 1 :让 Task 的 == 按 title 比较。

python 复制代码
class Task:
    def __init__(self, title: str) -> None:
        self.title = title
    def __eq__(self, other: object) -> bool:
        return isinstance(other, Task) and self.title == other.title
print(Task("a") == Task("a"))

练习 2 :实现 __len__,让 len(Tags(["a"])) 返回 1。

python 复制代码
class Tags:
    def __init__(self, items: list[str]) -> None:
        self.items = items
    def __len__(self) -> int:
        return len(self.items)
print(len(Tags(["a"])))

4.5 dataclass、不可变对象与默认工厂 练习答案

练习 1:定义包含 id、title、tags 的 dataclass,tags 使用默认工厂。

python 复制代码
from dataclasses import dataclass, field
@dataclass
class Task:
    id: str
    title: str
    tags: list[str] = field(default_factory=list)
print(Task("a", "Read"))

练习 2 :用 replace 从 frozen Task 生成 done=True 的新对象。

python 复制代码
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Task:
    title: str
    done: bool = False
print(replace(Task("Read"), done=True))

4.6 property、描述符与属性校验 练习答案

练习 1 :给 Task 增加只读属性 slug。

python 复制代码
class Task:
    def __init__(self, title: str) -> None:
        self.title = title
    @property
    def slug(self) -> str:
        return self.title.lower().replace(" ", "-")
print(Task("Read Docs").slug)

练习 2:写一个 priority setter,拒绝 0 以下的值。

python 复制代码
class Task:
    @property
    def priority(self) -> int:
        return self._priority
    @priority.setter
    def priority(self, value: int) -> None:
        if value < 0:
            raise ValueError("negative")
        self._priority = value
t = Task()
t.priority = 2
print(t.priority)

4.7 ABC、Protocol 与组合 练习答案

练习 1 :定义 Repository Protocol 和 MemoryRepository 实现。

python 复制代码
from typing import Protocol
class Repository(Protocol):
    def save(self, title: str) -> str: ...
class MemoryRepository:
    def save(self, title: str) -> str:
        return title
print(MemoryRepository().save("x"))

练习 2:写一个抽象 Notifier,并实现 ConsoleNotifier。

python 复制代码
from abc import ABC, abstractmethod
class Notifier(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...
class ConsoleNotifier(Notifier):
    def send(self, message: str) -> None:
        print(message)
ConsoleNotifier().send("hi")

4.8 枚举、常量与状态建模 练习答案

练习 1 :定义 Status 枚举,包含 pending 和 done。

python 复制代码
from enum import StrEnum
class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"
print(Status("pending"))

练习 2:用 match 对 Status.DONE 返回 finished。

python 复制代码
from enum import StrEnum
class Status(StrEnum):
    PENDING = "pending"
    DONE = "done"
status = Status.DONE
print("finished" if status is Status.DONE else "open")

4.9 泛型、TypedDict、Literal 与静态检查 练习答案

练习 1 :写一个泛型 first(items: list[T]) -> T。

python 复制代码
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
    return items[0]
print(first(["a"]))

练习 2 :用 TypedDict 描述 {title: str, done: bool}。

python 复制代码
from typing import TypedDict
class Payload(TypedDict):
    title: str
    done: bool
p: Payload = {"title": "Read", "done": False}
print(p)

4.10 复制、相等、哈希与 slots 练习答案

练习 1:用 deepcopy 隔离嵌套列表。

python 复制代码
from copy import deepcopy
original = [[1]]
clone = deepcopy(original)
clone[0].append(2)
print(original)

练习 2:用 slots 定义只有 x、y 的 Point。

python 复制代码
class Point:
    __slots__ = ("x", "y")
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y
print(Point(1, 2).x)

4.11 OOP 与类型项目线:TaskFlow v04 练习答案

练习 1:用 frozen dataclass 定义 Task,并在 complete 中返回新对象。

python 复制代码
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Task:
    title: str
    done: bool = False
    def complete(self):
        return replace(self, done=True)
print(Task("Read").complete())

练习 2:定义 Repository Protocol 和 MemoryRepository。

python 复制代码
from typing import Protocol
class Repository(Protocol):
    def add(self, title: str) -> str: ...
class MemoryRepository:
    def add(self, title: str) -> str:
        return title
print(MemoryRepository().add("Read"))

4.12 dataclass 进阶:校验、字段策略与继承 练习答案

练习 1 :用 dataclass 和 post_init 计算 tag_count。

python 复制代码
from dataclasses import dataclass, field
@dataclass
class Task:
    tags: list[str] = field(default_factory=list)
    count: int = field(init=False)
    def __post_init__(self):
        self.count = len(self.tags)
print(Task(["a", "b"]).count)

练习 2:定义一个 kw_only dataclass,包含 title 和 urgent。

python 复制代码
from dataclasses import dataclass
@dataclass(kw_only=True)
class Task:
    title: str
    urgent: bool = False
print(Task(title="Read", urgent=True))

4.13 对象协议进阶:可调用、索引与上下文 练习答案

练习 1 :实现 call,让对象把数字翻倍。

python 复制代码
class Double:
    def __call__(self, value):
        return value * 2
print(Double()(3))

练习 2 :实现 contains,让对象支持成员测试。

python 复制代码
class Tags:
    def __init__(self, values):
        self.values = values
    def __contains__(self, item):
        return item in self.values
print("a" in Tags(["a"]))

4.14 typing 进阶:Callable、TypeVar 与 overload 练习答案

练习 1 :定义一个接收 Callable[[int], int] 的高阶函数。

python 复制代码
from typing import Callable
def apply(fn: Callable[[int], int], value: int) -> int:
    return fn(value)
print(apply(abs, -4))

练习 2:用 TypeVar 编写返回列表首元素的泛型函数。

python 复制代码
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
    return items[0]
print(first([1, 2]))

4.15 泛型接口与依赖注入组合 练习答案

练习 1 :定义 Store[T] 协议,并提供内存实现。

python 复制代码
from typing import Protocol, TypeVar
T = TypeVar("T")
class Store(Protocol[T]):
    def save(self, value: T) -> None: ...
class Memory:
    def __init__(self): self.values=[]
    def save(self, value: int) -> None: self.values.append(value)
s=Memory(); s.save(1); print(s.values)

练习 2:写一个依赖 Clock 协议的服务。

python 复制代码
class Clock:
    def now(self): return 1
class Service:
    def __init__(self, clock): self.clock=clock
    def stamp(self): return self.clock.now()
print(Service(Clock()).stamp())

本卷验收

  • 能解释本卷每个"本单元新语法"表中的形式。
  • 能独立完成全部练习,并说明错误示例失败的原因。
  • 能运行项目里程碑:taskflow/milestones/v04_modeled_tasks.py --demo。
  • 能说出下一卷开始前必须掌握的三个概念。
相关推荐
benchmark_cc2 小时前
量化项目启动时,应该先设计数据接口还是先编写策略?从系统架构角度做出正确选择
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
2601_962885722 小时前
如何用 Python 判断一只股票“追涨”还是“抄底”更有效?
android·开发语言·python
用户7783366132112 小时前
选 SERP API 别靠感觉:用 SerpBase 实测的评估脚本与判定标准
python·api
用户019027581612 小时前
如何用 Python 计算 BRAR 情绪指标(AR/BR)?
python
荣码2 小时前
LangSmith可观测性:AI应用出bug根本不知道哪里出了问题,我踩了4个坑
java·python
在世修行2 小时前
智学文枢:AI 产出的持久化之道
python
CaptainT3 小时前
第 1 卷:执行模型与基础语法
python
总裁余(余登武)3 小时前
K-means算法改进(Python版)
python·算法·kmeans
CaptainT3 小时前
第 2 卷:函数、模块与推导式
python