课程进度 课程大纲 已发布 24/24 课
Python 基础
数据与集合
构建可靠的程序
使用对象建模
专业 Python
高级 Python
让每个辅助函数只做一件事
第 8 章制作了一份学习报告。很长的一套步骤也能完成任务,但小型辅助函数更容易理解和测试。一个函数应该只有一项明确的工作,并拥有一个简洁的契约:传入什么、返回什么,以及它会改变什么。
def session_label(topic, minutes=30, *, excited=False):
label = f"{topic}: {minutes} minutes"
if excited:
return label.upper() + "!"
return label
print(session_label("Python"))
print(session_label("Git", minutes=45, excited=True))
输出:
Python: 30 minutes
GIT: 45 MINUTES!
topic 是必需参数,minutes 有默认值,而 * 后面的所有参数都必须按名称传入。excited=True 比一个含义不明的第二个 True 更清楚。
这个契约很简单:接收一个主题和分钟数,返回文本,并且不改变函数外部的任何内容。返回数据可以让计算过程与 print() 产生的可见副作用彼此分离。
函数本身也是值。第 8 章把 minutes_in 作为键函数传给了 sorted()。传递函数时不要加 ();否则,括号会让函数立即被调用。
可变默认值是一个共享的背包
Python 会在执行 def 语句时计算默认值,而不是在每次调用函数时计算。因此,一个列表可能会把旧东西带进下一次调用。
- 一个背包 创建默认列表
- 放入 Python 背包会保留它
- 放入 Git Python 仍然在里面
下面的示例是故意写错的:
def collect(topic, topics=[]):
topics.append(topic)
return topics
print(collect("Python"))
print(collect("Git"))
输出:
['Python']
['Python', 'Git']
使用 None 作为“请创建一个新列表”的标志:
def collect(topic, topics=None):
if topics is None:
topics = []
topics.append(topic)
return topics
现在,两次省略列表参数的调用会各自独立开始。需要注意一个边界情况:调用者提供的列表仍然会被改变。如果契约承诺不修改它,就先使用 topics = list(topics) 创建副本。
从内向外查找名称
当 Python 遇到一个名称时,它会按照 LEGB 顺序查找:局部、封闭、全局、内置。
- 局部 当前这次函数调用
- 封闭 外层函数
- 全局 当前文件的顶层
- 内置 例如 len 这样的名称
course = "Python" # Global
def make_reader():
prefix = "Study" # Enclosing
def label(minutes):
suffix = "minutes" # Local
return f"{prefix} {course}: {minutes} {suffix}"
return label
reader = make_reader()
print(reader(40)) # print is Built-in
输出:
Study Python: 40 minutes
在函数内部进行赋值通常会创建一个局部名称。不要仅仅为了更新总数而使用 global;应该接收旧总数,再返回新总数。隐藏的共享修改会让测试结果取决于哪个调用先运行。
闭包可以记住一件小事
上面返回的 reader 仍然记得 prefix。一个函数与它记住的封闭作用域名称组合在一起,就称为闭包。只有当内层函数必须替换封闭作用域中的值时,才使用 nonlocal:
def make_counter(start=0):
total = start
def add(minutes):
nonlocal total
total += minutes
return total
return add
counter = make_counter(10)
print(counter(20))
print(counter(15))
输出:
30
45
如果没有 nonlocal,total += minutes 就会尝试在新的局部变量 total 还没有值之前使用它。闭包适合保存少量私有状态,不适合用来隐藏整个程序。
纯转换只使用输入并返回结果,不改变外部状态。打印内容、写入文件和修改调用者的列表都属于副作用。只要可以,就让转换处于程序中间,把副作用放在边缘。
构建一个小型分析流水线
- 规范化 创建干净的新记录
- 分析 计算总数并排名
- 格式化 返回报告文本
- 打印 一个可见的副作用
将下面这个完整项目保存为 study_pipeline.py:
def normalize_session(session):
return {
"topic": session["topic"].strip(),
"minutes": session["minutes"],
}
def minutes_by_topic(sessions):
totals = {}
for session in sessions:
topic = session["topic"]
totals[topic] = totals.get(topic, 0) + session["minutes"]
return totals
def minutes_in(item):
return item[1]
def analyze(sessions, target=60):
clean = [normalize_session(session) for session in sessions]
totals = minutes_by_topic(clean)
ranking = sorted(totals.items(), key=minutes_in, reverse=True)
total = 0
for session in clean:
total += session["minutes"]
return {"total": total, "met": total >= target, "ranking": ranking}
def format_report(analysis, *, heading="Study report"):
status = "met" if analysis["met"] else "not met"
lines = [heading, f"Total: {analysis['total']} minutes", f"Target: {status}"]
for number, (topic, minutes) in enumerate(analysis["ranking"], start=1):
lines.append(f"{number}. {topic}: {minutes} minutes")
return "\n".join(lines)
def run_pipeline(sessions, formatter=format_report, *, target=60):
return formatter(analyze(sessions, target=target))
sessions = [
{"topic": " Python ", "minutes": 45},
{"topic": "Git", "minutes": 40},
{"topic": "Python", "minutes": 30},
]
before = [session.copy() for session in sessions]
report = run_pipeline(sessions, target=100)
assert sessions == before
assert "Python: 75 minutes" in report
print("Checks passed.")
print(report)
运行 python3 study_pipeline.py:
Checks passed.
Study report
Total: 115 minutes
Target: met
1. Python: 75 minutes
2. Git: 40 minutes
formatter 接收一个函数值。所有计算都会返回数据;只有最后两次 print() 调用会产生可见的副作用。
三个小任务
- 编写
format_duration(minutes, *, compact=False),返回45 minutes或45m。 - 不传入列表,调用安全版本的
collect()两次,并证明两次结果彼此独立。 - 创建一个从 100 开始的闭包,依次加上 20 和 5,并在运行前预测两次的答案。
准备好学习第 10 章了吗?
- 我能说明一个函数的输入、返回值和副作用。
- 我知道为什么列表通常不应该用作默认值。
- 我能按照 LEGB 顺序查找名称。
- 我能解释闭包会记住什么,以及
nonlocal会改变什么。 - 我能把计算与打印分开。
- 我运行了流水线,并且它的检查全部通过。
接下来,你将通过精确的异常和验证来拒绝错误输入,从而保护这些函数契约。