课程进度 课程大纲 已发布 24/24 课
Python 基础
数据与集合
构建可靠的程序
使用对象建模
专业 Python
高级 Python
类型提示是便利贴,而不是守卫
类型提示告诉读者和静态工具,一个值应该是什么类型。它通常不会守在门口,阻止不同类型的运行时值进入。
-
注解
name: str - 类型检查器 在运行前读取
- Python 使用实际对象
def repeat(label: str, times: int) -> str:
return label * times
print(repeat("go", 3))
print(repeat(2, 3))
gogogo
6
类型检查器应该标记第二次调用,但 Python 仍会执行整数乘法。来自用户、文件和网络的数据,需要由你自己验证。注解也不会自动转换数据。
描述容器和可空值
集合类型提示既描述容器,也描述其中的内容:
def total_by_topic(sessions: list[tuple[str, int]]) -> dict[str, int]:
totals: dict[str, int] = {}
for topic, minutes in sessions:
totals[topic] = totals.get(topic, 0) + minutes
return totals
print(total_by_topic([("Python", 20), ("Python", 25)]))
{'Python': 45}
类型提示不会在运行时检查每一个元素。只要求你真正需要的能力:一个只进行循环的只读函数可以接受 Iterable[str];需要按索引访问的函数可以接受 Sequence[str];明确需要修改集合的函数可能需要 list[str]。
str | None 表示字符串或者 None。这是 Optional[str] 的现代写法。int | str 表达的联合类型概念与 Union[int, str] 相同。
def author_label(author: str | None) -> str:
if author is None:
return "Unknown"
return author.strip().title()
当处理 None 的分支返回后,类型检查器便可以将 author 缩窄为 str。可选类型并不意味着参数可以省略:调用 author_label() 时仍然必须提供参数。如果允许省略,请使用 author: str | None = None。
-
联合类型
int | str -
检查
isinstance(value, int) - 整数分支 可以安全地进行整数运算
- 其他分支 可以安全地进行字符串操作
类型别名可以为反复出现的结构赋予一个领域名称:
from typing import TypeAlias
Session: TypeAlias = tuple[str, int]
Python 3.12 还支持 type Session = tuple[str, int]。别名只是词汇,不是新的运行时类或验证器。当字段需要名称或规则时,请使用数据类。
描述行为并保留类型关系
Callable[[str], str] 描述了一个可调用对象:它接受一个字符串并返回一个字符串:
from collections.abc import Callable
def clean_all(words: list[str], clean: Callable[[str], str]) -> list[str]:
return [clean(word) for word in words]
它可以接受符合这一约定的函数、绑定方法或可调用对象。
TypeVar 保留类型之间的关系,而不是指定某一种具体类型:
from collections.abc import Sequence
from typing import TypeVar
T = TypeVar("T")
def first_or_none(items: Sequence[T]) -> T | None:
return items[0] if items else None
对于 list[int],结果是 int | None;对于 list[str],结果是 str | None。如果返回 object | None,这种有用的联系就会丢失。只有当类型变量表达了真实的输入输出关系时,才应该添加它。
Protocol 是一张能力卡
Protocol 为使用方所需的一小组行为命名。类只要拥有兼容的成员,就能以结构化方式满足它;不需要继承它。
-
Protocol
要求提供
summary() - 文章 拥有这种能力
- 视频 也拥有这种能力
from typing import Protocol
class Summarizable(Protocol):
def summary(self) -> str:
...
def show(item: Summarizable) -> None:
print(item.summary())
协议应该保持精简。@runtime_checkable 允许使用 isinstance() 进行有限的成员存在性检查,但它不会验证完整的方法签名或带注解的值类型:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Fetcher(Protocol):
def fetch(self) -> list[str]:
...
class WrongShape:
def fetch(self, required_argument):
return 42
print(isinstance(WrongShape(), Fetcher))
这段代码可能打印 True,因为 fetch 确实存在。静态检查和运行时数据验证仍然很重要。
小项目:带类型的通知摘要
创建 notification_digest.py。from __future__ import annotations 让这种现代注解语法也能在较旧的受支持解释器上运行。这个项目结合了类型别名、可调用对象、泛型辅助函数、协议和真正的运行时验证。
from __future__ import annotations
from collections.abc import Callable, Iterable, Sequence
from dataclasses import dataclass
from typing import Protocol, TypeVar
RawNotice = tuple[str, int]
T = TypeVar("T")
@dataclass(frozen=True)
class Notice:
source: str
message: str
score: int
def __post_init__(self) -> None:
if not self.source.strip() or not self.message.strip():
raise ValueError("text must not be blank")
if isinstance(self.score, bool) or not isinstance(self.score, int):
raise TypeError("score must be an integer")
if not 0 <= self.score <= 100:
raise ValueError("score must be from 0 to 100")
class NoticeSource(Protocol):
def fetch(self) -> Iterable[Notice]:
...
class MemorySource:
def __init__(self, name: str, records: list[RawNotice]) -> None:
self.name = name
self.records = records
def fetch(self) -> Iterable[Notice]:
for message, score in self.records:
yield Notice(self.name, message, score)
def first_or_none(items: Sequence[T]) -> T | None:
return items[0] if items else None
def collect(
sources: Iterable[NoticeSource], keep: Callable[[Notice], bool]
) -> list[Notice]:
notices = [
notice
for source in sources
for notice in source.fetch()
if keep(notice)
]
return sorted(notices, key=lambda notice: (-notice.score, notice.source))
def format_digest(notices: Sequence[Notice]) -> str:
if not notices:
return "No notices."
lines = ["Notification digest"]
lines.extend(
f"- {notice.score:03d} | {notice.source}: {notice.message}"
for notice in notices
)
top = first_or_none(notices)
if top is not None:
lines.append(f"Top source: {top.source}")
return "\n".join(lines)
sources: list[NoticeSource] = [
MemorySource("Python", [("Typing notes", 88), ("Style guide", 62)]),
MemorySource("Testing", [("Regression suite", 91)]),
]
notices = collect(sources, lambda notice: notice.score >= 70)
print(format_digest(notices))
print(f"Empty helper: {first_or_none([]) is None}")
运行 python3 notification_digest.py 并检查:
Notification digest
- 091 | Testing: Regression suite
- 088 | Python: Typing notes
Top source: Testing
Empty helper: True
MemorySource 从未继承 NoticeSource;它拥有兼容的 fetch(),这对于静态结构化类型来说已经足够。再次提醒边界:类型提示不会强制分数处于规定范围内——数据类会在运行时进行检查。传入不合适的回调也仍然可能在运行时失败。
三个小任务
- 缩窄联合类型。 接受
int | str;在进行isinstance()检查后,为整数补前导零,并将非空字符串转换为大写。 - 保留类型。 编写
last_or_none(items: Sequence[T]) -> T | None,并使用整数和Notice对象进行尝试。 - 添加一个数据源。 创建一个以元组为基础、拥有
fetch()的类,但不要继承NoticeSource。
当你做到以下几点时,就可以学习第 17 章了……
- 你能够解释类型提示通常不会强制约束运行时值;
- 你能够根据集合所需的能力为其添加注解;
- 你能够使用
Optional/Union的概念,并通过实际检查缩窄类型; - 你知道类型别名只是为一种结构命名,并不会验证它;
- 你能够使用
Callable为回调添加注解; - 你能够使用
TypeVar保留输入输出之间的关系; - 你能够定义一个小型
Protocol,并以结构化方式满足它; - 你知道
runtime_checkable检查的是成员是否存在,而不是完整的方法签名; - 你能够运行通知摘要,并看到
Empty helper: True。
接下来,你将了解迭代器和生成器如何让数据源逐个生成值。