第 10 章
收尾工具箱:集合、类型提示、argparse 与生成器
最后一章补齐散落的工具:用 set 去重、用 global 处理模块级状态、用常量与类型提示防错、用 docstring 写文档、用 argparse 解析命令行,再用 * 与 ** 解包、推导式、enumerate 收紧代码;最后用 yield 解决「一百万只羊」的内存问题。
Et Cetera: Sets, Type Hints, argparse and Generators
The final chapter gathers the loose tools: sets for deduplication, global for module-level state, constants and type hints for defence, docstrings for documentation, argparse for command lines, unpacking with * and **, comprehensions and enumerate for tighter code, and yield to solve the million-sheep memory problem.
篇内目录
- Timeline
- set: duplicate removal
- Globals and global
- Why it is discouraged
- Constants: a convention only
- Type hints and mypy
- Docstrings
- Command-line arguments
- sys.argv
- argparse
- Unpacking: * and **
- Variadic parameters
- map, filter and comprehensions
- map: apply a function to every element
- List comprehensions
- filter: keep by predicate
- Choosing
- Dictionary comprehensions
- enumerate
- Generators and yield
- Takeaways
- Comprehension check
- How it connects
这是长视频(约 16 小时)的最后一章,对应 CS50P 第 9 次讲座「Et Cetera」,时间区间 13:29:08–15:57:18。前面九章覆盖了函数、条件、循环、异常、库、测试、文件、正则与面向对象;这一章收拢那些没归进任何大主题、但迟早会用到的工具。
时间轴
| 时间 | 内容 |
|---|---|
| 13:30:20 | set:自动去重的集合 |
| 13:36:33 | 全局变量、UnboundLocalError 与 global |
| 13:46:32 | 用类重写银行账户 |
| 13:53:15 | 常量:大写命名只是约定 |
| 13:58:02 | 类型提示:n: int、-> None 与 mypy |
| 14:16:14 | 文档字符串:三引号与 reST 风格 |
| 14:23:28 | 命令行参数:sys.argv 与 argparse |
| 14:39:06 | 解包:*list 与 **dict |
| 14:57:04 | 变长参数:*args 与 **kwargs |
| 15:06:59 | map、列表推导式、filter、字典推导式 |
| 15:34:15 | enumerate:同时拿索引与值 |
| 15:36:59 | 生成器与 yield:一次只给一个 |
set:自动去重
给一串学生记录,想知道霍格沃茨到底有几个学院。手写去重:
houses = []
for student in students:
if student["house"] not in houses:
houses.append(student["house"])
set 直接解决:
houses = set()
for student in students:
houses.add(student["house"])
- 创建空集合必须写
set(),{}是空字典; - 添加元素用
.add(),不是.append(); - 重复元素自动合并;判断成员用
in,与列表语法相同。
集合是无序的,打印顺序不保证。要固定顺序就再 sorted(houses)。
注意大小写与拼写:Slytherin 与 Slytherinn 是两个不同的字符串,集合会当作两个元素。数据来自用户时,先统一大小写或做清洗。
全局变量与 global
balance = 0
def main():
print("Balance:", balance)
deposit(100)
withdraw(50)
print("Balance:", balance)
def deposit(n):
balance += n
def withdraw(n):
balance -= n
运行会得到
UnboundLocalError: cannot access local variable 'balance' where it is not associated with a value
原因:函数里只要出现对 balance 的赋值,Python 就把这个名字当作局部变量;于是 balance += n 里的读取发生在赋值之前,读的是一个尚未存在的局部变量。
读取全局变量没问题,写入才需要声明:
def deposit(n):
global balance
balance += n
global 告诉 Python:这个名字指向模块级的那个变量,不要新建局部变量。
为什么不推荐
- 变量藏在文件顶部,函数却在别处修改它,阅读时必须来回跳;
- 函数不再自包含,测试与复用都变难;
- 名称遮蔽容易出错:函数里再定义同名变量,会悄悄盖住全局的。
课程给出的更好写法是类:
class Account:
def __init__(self):
self._balance = 0
@property
def balance(self):
return self._balance
def deposit(self, n):
self._balance += n
def withdraw(self, n):
self._balance -= n
状态与操作它的方法放在一起,_balance 用下划线提示「外部别碰」。global 适用于很小的脚本,规模一大就该换成类。
常量:只是约定
MEOWS = 3
for _ in range(MEOWS):
print("meow")
大写命名是在告诉读者:这是常量,不要改。但 Python 不阻止你写 MEOWS = 4。Java 的 final、C++ 的 const 会在编译期拦截,Python 不会。
类里也一样:
class Cat:
MEOWS = 3
def meow(self):
for _ in range(Cat.MEOWS):
print("meow")
写成 cat.MEOWS 会在实例上查找,找不到再回到类;写成 Cat.MEOWS 更明确。修改实例属性不会影响类属性,这也是容易踩的坑。
类型提示与 mypy
def meow(n: int) -> None:
for _ in range(n):
print("meow")
number: int = int(input("Number: "))
meow(number)
n: int 与 -> None 是注解,不是强制约束。Python 运行时完全不检查,传字符串照样报运行时错误。
想要提前发现,用 mypy:
pip install mypy
mypy meow.py
它会静态分析代码,指出「这里期望 int,却传了 str」。这相当于把错误从「用户运行时遇到」提前到「提交代码前」。
代价是要维护注解,且在动态类型代码里经常需要额外声明。课程的态度是:小脚本可以不用,团队项目值得用。
文档字符串
def meow(n: int) -> str:
"""
Meow n times.
:param n: Number of times to meow
:type n: int
:raise TypeError: If n is not an int
:return: A string of n meows, one per line
:rtype: str
"""
return "meow\n" * n
三引号写在函数体第一行,就是 docstring。help(meow) 与 meow.__doc__ 都能读到它。:param:、:type: 这一套是 reST 风格,Sphinx 等工具可以据此生成网页或 PDF 文档。
注意:这是约定,不是 Python 语法。工具只能按约定的格式解析,所以团队要统一风格。
命令行参数
sys.argv
import sys
if len(sys.argv) == 1:
print("meow")
elif len(sys.argv) == 3 and sys.argv[1] == "-n":
n = int(sys.argv[2])
for _ in range(n):
print("meow")
else:
print("usage: meow.py")
sys.argv 是字符串列表,第一个元素是脚本名。参数一多,判断就会爆炸:-n、-a、--number,还要考虑顺序。
argparse
import argparse
parser = argparse.ArgumentParser(description="Meow like a cat")
parser.add_argument("-n", default=1, help="number of times to meow", type=int)
args = parser.parse_args()
for _ in range(args.n):
print("meow")
ArgumentParser是类,parser是对象;add_argument声明参数:名字、默认值、帮助文本、类型;parse_args()自动读取sys.argv并解析;args.n直接是int,不用手动转换;-h/--help自动生成帮助信息;- 用户传了非数字,argparse 自己报错并打印 usage。
这正是「标准库替你处理通用问题」的又一例。
解包:* 与 **
def total(galleons, sickles, knuts):
return (galleons * 17 + sickles) * 29 + knuts
coins = [100, 50, 25]
print(total(*coins), "Knuts")
*coins 把列表展开成三个位置参数,等价于 total(100, 50, 25)。
coins = {"galleons": 100, "sickles": 50, "knuts": 25}
print(total(**coins), "Knuts")
**coins 把字典展开成三个关键字参数,等价于 total(galleons=100, sickles=50, knuts=25)。字典的键必须与参数名一致,否则报 unexpected keyword argument。
变长参数
反过来,函数也可以接收任意多个参数:
def f(*args, **kwargs):
print("Positional:", args)
print("Named:", kwargs)
f(100, 50, 25, galleons=100, sickles=50)
*args收集位置参数,得到一个元组;**kwargs收集关键字参数,得到一个字典。
print 本身就是这样的函数:print(*objects, sep=" ", end="\n")。所以 print() 可以没有参数,print("a") 可以一个,print("a", "b") 可以多个。
map、filter 与推导式
map:对每个元素应用函数
words = ["This", "is", "CS50"]
uppercase = map(str.upper, words)
print(*uppercase) # THIS IS CS50
map 接收一个函数和一个序列,把函数应用到每个元素。注意 str.upper 后面没有括号——传的是函数本身,由 map 负责调用。
列表推导式
uppercase = [word.upper() for word in words]
读法是「给我 word.upper(),对每个 word 在 words 里」。它把循环、append 和表达式压缩成一行。
加条件:
gryffindors = [
student["name"]
for student in students
if student["house"] == "Gryffindor"
]
filter:按条件筛选
def is_gryffindor(s):
return s["house"] == "Gryffindor"
gryffindors = filter(is_gryffindor, students)
filter 要求第一个函数返回布尔值,只保留返回 True 的元素。也可以直接传 lambda:
filter(lambda s: s["house"] == "Gryffindor", students)
怎么选
| 目的 | 常用写法 |
|---|---|
| 变换每个元素 | 列表推导式 / map |
| 按条件筛选 | 列表推导式带 if / filter |
| 一次变换 + 筛选 | 推导式更清楚 |
map/filter 返回的是迭代器,print 时不会显示列表内容,需要 list(...) 或 * 解包。推导式直接返回列表。
字典推导式
gryffindors = {student["name"]: "Gryffindor" for student in students}
外层用花括号、内部写 键: 值,就从列表构造出字典。
enumerate
for i, student in enumerate(students):
print(i + 1, student)
enumerate 同时给出索引与值,省掉 range(len(...)) 加下标访问。默认从 0 开始,也可以 enumerate(students, start=1)。
生成器与 yield
def sheep(n):
flock = []
for i in range(n):
flock.append("🐑" * i)
return flock
n = 10000 还能跑,n = 1_000_000 直接把内存打满,程序被系统杀掉。原因是函数要先造出全部结果,再一次性返回。
def sheep(n):
for i in range(n):
yield "🐑" * i
yield 每次只交出一个值,函数暂停在这里,下一次迭代再从暂停处继续。内存里同时只存在一行羊,所以一百万次也能立刻开始输出。
这就是生成器:它返回一个迭代器,可以用 for 逐个取值。函数的状态(循环进行到第几次)由 Python 保存,不需要你手动维护。
两点区别:
return结束整个函数,yield只是暂停;- 生成器只能遍历一次,取完就空了;需要重复使用就重新创建。
本章结论
set自动去重,用set()创建、.add()添加,无序。- 函数内给全局变量赋值需要
global;规模一大应改用类封装状态。 - 常量只是大写约定,Python 不强制;类常量写成
ClassName.NAME。 - 类型提示不参与运行,mypy 等工具可以静态检查。
- docstring 用三引号写在函数体首行,reST 风格可被工具生成文档。
sys.argv手工解析会迅速失控,argparse负责参数、类型、默认值与帮助。*list解包为位置参数,**dict解包为关键字参数;反向的*args/**kwargs接收任意数量参数。- 推导式与
map/filter是同一类问题的不同写法;推导式返回列表,map/filter返回迭代器。 enumerate同时给出索引与值。- 生成器用
yield逐个产出值,把「一次生成全部」变成「按需生成」,解决内存问题。
理解检验
1. 用列表去重与用 set 去重,代码与行为上有什么差别?为什么 {} 不能创建空集合?
参考答案:列表去重要手动写 if x not in list: list.append(x),每次插入还要线性扫描,整体复杂度是 ;set 内部用哈希表,.add() 平均 ,重复元素自动合并,整体接近 。行为上列表保留首次出现的顺序,集合无序,打印顺序不保证,需要顺序时再 sorted()。{} 在 Python 中已经被字典占用,空字典与空集合必须有不同写法,因此集合用 set()。
2. 为什么在函数里写 balance += n 会报 UnboundLocalError?global 做了什么?
参考答案:Python 在编译函数时,只要函数体中出现对某个名字的赋值,就把这个名字判定为局部变量。balance += n 等价于 balance = balance + n,右侧的读取发生在赋值之前,而局部变量此时尚未绑定,于是抛出 UnboundLocalError。global balance 声明这个名字指向模块级变量,赋值和读取都作用于全局那份,不再创建局部变量。注意只读全局变量不需要 global,只有写入才需要。
3. 类型提示会在运行时阻止传错类型吗?那它有什么用?
参考答案:不会。def meow(n: int) 里的 int 只是注解,Python 解释器运行时完全不检查,传字符串照样执行到 range(n) 才报 TypeError。它的价值在于让静态检查工具(如 mypy)在运行前分析代码,指出「这里期望 int、实际是 str」,把错误提前到开发阶段;同时它也是给读者和 IDE 的文档。注解需要维护,所以小脚本可以省略,团队项目更值得使用。
4. *coins 与 **coins 有什么区别?*args 与 **kwargs 又对应什么?
参考答案:*coins 把列表(或元组等可迭代对象)展开成一个个位置参数,total(*coins) 等价于 total(100, 50, 25);**coins 把字典展开成关键字参数,total(**coins) 等价于 total(galleons=100, sickles=50, knuts=25),因此字典的键必须与参数名一致。反过来定义函数时,*args 把任意多个位置参数收集成元组,**kwargs 把任意多个关键字参数收集成字典。print(*objects, sep=" ", end="\n") 就是这个机制的实际应用。
5. 列表推导式与 map/filter 各适合什么场景?为什么 print(map(...)) 看不到结果?
参考答案:推导式适合「变换 + 可选筛选」且希望直接得到列表的场景,可读性通常更好;map 适合把已有函数直接套用到每个元素,filter 适合用现成的布尔函数筛选,二者更贴近函数式风格。map/filter 返回的是迭代器而不是列表,print 只会打印迭代器对象本身,看不到内容;要用 list(...) 转成列表,或用 * 解包传给 print 才会逐个输出。
6. 为什么生成 100 万只羊的版本会崩溃,而 yield 版本不会?生成器与普通函数在控制流上有什么不同?
参考答案:普通函数先把 100 万行字符串全部放进 flock 列表再返回,内存占用与 成正比,超过可用内存后进程被系统终止。yield 版本每产出一行就暂停,内存里只保留当前一行,占用与 无关。控制流上,return 结束整个函数并交出唯一结果;yield 暂停在当前位置,保存局部状态,下一次迭代从暂停处继续。生成器返回的是迭代器,只能遍历一次,取完即空。
7. 为什么 argparse 比手写 sys.argv 判断更好?它替你做了哪些事?
参考答案:手写判断要逐一检查参数个数、位置与名称,支持 -n、--number、可选参数、默认值时会出现大量嵌套 if/elif,顺序变化或新增参数就要重写逻辑。argparse 声明式地登记参数(名字、类型、默认值、帮助文本),自动解析 sys.argv、按名称取值、把 type=int 的转换做好、生成 -h/--help 帮助信息,并在用户输入非法时给出标准错误与 usage。它把「通用且重复」的解析工作外包给标准库,让代码只保留业务逻辑。
与前后章的关系
这一章是整门课的工具箱收尾:set 补充了第 7 章的数据结构,global 与类常量把第 9 章的封装讲完整,类型提示与 docstring 让第 6 章的工程习惯延伸到接口层,argparse 把第 5 章的库使用推进到「写自己的命令行工具」,推导式与生成器则把前面反复出现的循环写法收束成更紧凑、更省内存的形式。第 8 章的正则、第 7 章的文件读写与本章的 argparse 组合起来,就是一个完整的命令行数据处理脚本。
This is chapter 10, the final chapter of the long video (about 16 hours), corresponding to CS50P lecture 9, "Et Cetera", covering 13:29:08–15:57:18. The first nine chapters covered functions, conditionals, loops, exceptions, libraries, testing, files, regex and objects; this one collects the tools that did not belong to any of those themes but will eventually be needed.
Timeline
| Time | Topic |
|---|---|
| 13:30:20 | set: a collection that removes duplicates |
| 13:36:33 | Globals, UnboundLocalError and global |
| 13:46:32 | Rewriting the bank with a class |
| 13:53:15 | Constants: uppercase is only a convention |
| 13:58:02 | Type hints: n: int, -> None, and mypy |
| 14:16:14 | Docstrings: triple quotes and reST style |
| 14:23:28 | Command-line arguments: sys.argv and argparse |
| 14:39:06 | Unpacking: *list and **dict |
| 14:57:04 | Variadic parameters: *args and **kwargs |
| 15:06:59 | map, list comprehensions, filter, dict comprehensions |
| 15:34:15 | enumerate: index and value together |
| 15:36:59 | Generators and yield: one value at a time |
set: duplicate removal
Given a list of student records, find how many houses Hogwarts really has. Doing it by hand:
houses = []
for student in students:
if student["house"] not in houses:
houses.append(student["house"])
A set handles it directly:
houses = set()
for student in students:
houses.add(student["house"])
- an empty set must be written
set();{}is an empty dictionary; - add with
.add(), not.append(); - duplicates collapse automatically; membership uses
in, as with lists.
Sets are unordered, so printing order is not guaranteed. For a stable order, use sorted(houses).
Watch spelling and case: Slytherin and Slytherinn are different strings, and the set keeps both. When data comes from users, normalise case or clean it first.
Globals and global
balance = 0
def main():
print("Balance:", balance)
deposit(100)
withdraw(50)
print("Balance:", balance)
def deposit(n):
balance += n
def withdraw(n):
balance -= n
Running it gives
UnboundLocalError: cannot access local variable 'balance' where it is not associated with a value
The rule: if a name is assigned anywhere in a function body, Python treats that name as local throughout the function. In balance += n, the read happens before the assignment, so it reads a local variable that does not exist yet.
Reading a global is fine; writing requires a declaration:
def deposit(n):
global balance
balance += n
global tells Python the name refers to the module-level variable rather than a new local.
Why it is discouraged
- the variable lives at the top of the file while functions elsewhere mutate it, so reading the code means jumping around;
- functions are no longer self-contained, which makes testing and reuse harder;
- name shadowing is easy: a same-named local silently hides the global.
The lecture's better answer is a class:
class Account:
def __init__(self):
self._balance = 0
@property
def balance(self):
return self._balance
def deposit(self, n):
self._balance += n
def withdraw(self, n):
self._balance -= n
State and the methods that change it live together, and _balance signals "do not touch from outside". global suits tiny scripts; once the program grows, use a class.
Constants: a convention only
MEOWS = 3
for _ in range(MEOWS):
print("meow")
The uppercase name tells the reader this is a constant. Python does not stop you writing MEOWS = 4. Java's final and C++'s const are enforced at compile time; Python's are not.
Inside a class:
class Cat:
MEOWS = 3
def meow(self):
for _ in range(Cat.MEOWS):
print("meow")
cat.MEOWS looks on the instance first and falls back to the class; Cat.MEOWS is explicit. Assigning to an instance attribute does not change the class attribute — a common trap.
Type hints and mypy
def meow(n: int) -> None:
for _ in range(n):
print("meow")
number: int = int(input("Number: "))
meow(number)
n: int and -> None are annotations, not constraints. The interpreter ignores them; passing a string still fails at runtime.
To catch it earlier, use mypy:
pip install mypy
mypy meow.py
It analyses the code statically and reports "expected int, got str", moving the error from the user's runtime to before the code ships. The cost is maintaining annotations, which can require extra declarations in dynamic code. The lecture's stance: optional for small scripts, worthwhile for team projects.
Docstrings
def meow(n: int) -> str:
"""
Meow n times.
:param n: Number of times to meow
:type n: int
:raise TypeError: If n is not an int
:return: A string of n meows, one per line
:rtype: str
"""
return "meow\n" * n
Triple quotes on the first line of the body make a docstring. help(meow) and meow.__doc__ both read it. The :param: / :type: fields are reST style, which Sphinx and similar tools can turn into HTML or PDF documentation.
Note this is a convention, not Python syntax; tools can only parse the format the team agrees on.
Command-line arguments
sys.argv
import sys
if len(sys.argv) == 1:
print("meow")
elif len(sys.argv) == 3 and sys.argv[1] == "-n":
n = int(sys.argv[2])
for _ in range(n):
print("meow")
else:
print("usage: meow.py")
sys.argv is a list of strings whose first element is the script name. With more flags — -n, -a, --number, in any order — the branching explodes.
argparse
import argparse
parser = argparse.ArgumentParser(description="Meow like a cat")
parser.add_argument("-n", default=1, help="number of times to meow", type=int)
args = parser.parse_args()
for _ in range(args.n):
print("meow")
ArgumentParseris a class andparseran object;add_argumentdeclares a flag: name, default, help text, type;parse_args()reads and parsessys.argv;args.nis already anint, no manual conversion;-h/--helpis generated automatically;- a non-numeric value produces a standard error and usage message.
Another case of the standard library absorbing a universal problem.
Unpacking: * and **
def total(galleons, sickles, knuts):
return (galleons * 17 + sickles) * 29 + knuts
coins = [100, 50, 25]
print(total(*coins), "Knuts")
*coins expands the list into three positional arguments, equivalent to total(100, 50, 25).
coins = {"galleons": 100, "sickles": 50, "knuts": 25}
print(total(**coins), "Knuts")
**coins expands the dictionary into keyword arguments, equivalent to total(galleons=100, sickles=50, knuts=25). The keys must match the parameter names, or Python raises unexpected keyword argument.
Variadic parameters
Functions can also accept any number of arguments:
def f(*args, **kwargs):
print("Positional:", args)
print("Named:", kwargs)
f(100, 50, 25, galleons=100, sickles=50)
*argscollects positional arguments into a tuple;**kwargscollects keyword arguments into a dictionary.
print works this way: print(*objects, sep=" ", end="\n"). That is why it accepts zero, one or many arguments.
map, filter and comprehensions
map: apply a function to every element
words = ["This", "is", "CS50"]
uppercase = map(str.upper, words)
print(*uppercase) # THIS IS CS50
map takes a function and a sequence and applies the function to each element. Note str.upper has no parentheses — the function itself is passed, and map calls it.
List comprehensions
uppercase = [word.upper() for word in words]
Read it as "give me word.upper() for each word in words". It compresses the loop, the append and the expression into one line.
With a condition:
gryffindors = [
student["name"]
for student in students
if student["house"] == "Gryffindor"
]
filter: keep by predicate
def is_gryffindor(s):
return s["house"] == "Gryffindor"
gryffindors = filter(is_gryffindor, students)
filter needs a function returning a boolean and keeps the elements for which it returns True. A lambda works too:
filter(lambda s: s["house"] == "Gryffindor", students)
Choosing
| Goal | Usual choice |
|---|---|
| transform every element | list comprehension / map |
| keep by condition | comprehension with if / filter |
| transform and filter at once | comprehension |
map and filter return iterators, so print shows the object rather than the contents; wrap in list(...) or unpack with *. Comprehensions return a list directly.
Dictionary comprehensions
gryffindors = {student["name"]: "Gryffindor" for student in students}
Curly braces outside, key: value inside, and a dictionary is built from the list.
enumerate
for i, student in enumerate(students):
print(i + 1, student)
enumerate yields index and value together, replacing range(len(...)) plus indexing. It starts at 0, or use enumerate(students, start=1).
Generators and yield
def sheep(n):
flock = []
for i in range(n):
flock.append("🐑" * i)
return flock
n = 10_000 still runs; n = 1_000_000 exhausts memory and the process is killed, because the function must build every result before returning it.
def sheep(n):
for i in range(n):
yield "🐑" * i
yield hands out one value, pauses the function, and resumes from the same point on the next iteration. Only one row of sheep exists at a time, so a million rows begin printing immediately.
That is a generator: it returns an iterator that a for loop can step through, with Python preserving the function's state.
Two differences from return:
returnends the function;yieldmerely pauses it;- a generator can be iterated only once; when exhausted it is empty, so create a new one to traverse again.
Takeaways
setremoves duplicates; create withset(), add with.add(), unordered.- Assigning to a module-level variable inside a function needs
global; for larger programs, encapsulate state in a class. - Constants are an uppercase convention; Python does not enforce them. Class constants use
ClassName.NAME. - Type hints do not affect runtime; mypy and similar tools check them statically.
- A docstring is triple-quoted on the first body line; reST conventions let tools generate documentation.
- Hand-parsing
sys.argvgets out of hand quickly;argparsehandles flags, types, defaults and help. *listunpacks to positional arguments,**dictto keyword arguments;*args/**kwargsdo the reverse in definitions.- Comprehensions and
map/filtersolve the same problems differently; comprehensions return lists,map/filterreturn iterators. enumerateprovides index and value together.- Generators with
yieldproduce values on demand, turning "build everything" into "build as needed" and solving the memory problem.
Comprehension check
1. How does deduplicating with a list differ from using a set, in code and in behaviour? Why can {} not create an empty set?
Reference answer: List deduplication requires if x not in list: list.append(x), scanning on every insert, so the cost is overall. A set is hash-based: .add() is on average, duplicates collapse, so the whole loop is about . Behaviourally, lists preserve first-seen order while sets are unordered and print in no guaranteed order; call sorted() when order matters. {} is already the empty-dictionary literal, so sets need a distinct spelling: set().
2. Why does balance += n inside a function raise UnboundLocalError, and what does global do?
Reference answer: Python decides a name is local if it is assigned anywhere in the function body. balance += n means balance = balance + n, so the read happens before the assignment and refers to a local that has not been bound yet, raising UnboundLocalError. Writing global balance declares the name refers to the module-level variable, so both read and write touch that one instead of creating a local. Reading a global needs no declaration; only assignment does.
3. Do type hints stop a wrong type at runtime? What are they for?
Reference answer: No. The int in def meow(n: int) is an annotation the interpreter ignores at runtime; passing a string still runs until range(n) raises TypeError. Their value is that static tools such as mypy analyse the code before it runs and report "expected int, got str", moving the failure into development, and they document intent for readers and IDEs. Annotations cost maintenance, so small scripts may skip them and team projects benefit more.
4. What is the difference between *coins and **coins? What do *args and **kwargs correspond to?
Reference answer: *coins expands a list (or tuple, or other iterable) into separate positional arguments, so total(*coins) equals total(100, 50, 25). **coins expands a dictionary into keyword arguments, so total(**coins) equals total(galleons=100, sickles=50, knuts=25), which requires the keys to match the parameter names. In a definition, *args collects any number of positional arguments into a tuple and **kwargs collects keyword arguments into a dictionary. print(*objects, sep=" ", end="\n") is the everyday example.
5. When do you use a list comprehension versus map/filter? Why does print(map(...)) show nothing useful?
Reference answer: Comprehensions suit "transform with optional filtering" when a list is wanted and usually read better. map suits applying an existing function to every element, and filter suits keeping elements by an existing boolean predicate — both closer to functional style. map and filter return iterators, so print shows only the iterator object; convert with list(...) or unpack with * to see the elements.
6. Why does the million-sheep version crash while the yield version does not? How do generators differ in control flow?
Reference answer: The ordinary function builds all one million strings in flock before returning, so memory grows with and the process is killed once it exceeds available memory. The yield version produces one row, pauses, and resumes later, so only the current row is held and memory is independent of . In control flow, return ends the function with a single result; yield suspends at the current point, preserving local state, and the next iteration continues from there. A generator returns an iterator that can be traversed once and is empty afterwards.
7. Why is argparse better than hand-written sys.argv checks, and what does it take over?
Reference answer: Hand-written checks must test argument counts, positions and names one by one; supporting -n, --number, optional flags and defaults produces deeply nested if/elif, and any change of order or new flag forces a rewrite. argparse declares parameters (name, type, default, help) and then parses sys.argv, exposing values by name, converting type=int automatically, generating -h/--help, and producing a standard usage error for invalid input. It outsources the repeated, generic parsing so the code keeps only the business logic.
How it connects
This chapter closes the toolbox: set extends the data structures of chapter 7; global and class constants complete the encapsulation story of chapter 9; type hints and docstrings extend chapter 6's engineering habits to interfaces; argparse takes chapter 5's library use to the point of writing your own command-line tools; comprehensions and generators compress the loop patterns that recurred throughout. Combined with chapter 8's regex and chapter 7's file I/O, argparse completes a full command-line data-processing script.