实验 5:可变性、迭代器与生成器
截止时间:7 月 9 日(星期四)晚上 11:59。
起始文件
下载 lab05.zip.
出勤要求
要获得实验课学分,除了到场参加实验课外,你还需要提交实验题目。
如果你因合理原因(例如生病或时间冲突)缺席实验课,或者由于某种原因未能完成签到,请在一周内发送邮件至 cs61a@berkeley.edu ,以补记出勤学分。
必做题
可变性
Python 中的列表和字典等对象是 可变的,也就是说可以改变其内容或状态。数字、元组和字符串等其他对象则是 不可变的,创建后就不能再改变。
列表最常见的两种修改操作是元素赋值和
append 方法。
>>> s = [1, 3, 4]
>>> t = s # A second name for the same list
>>> t[0] = 2 # this changes the first element of the list to 2, affecting both s and t
>>> s
[2, 3, 4]
>>> s.append(5) # this adds 5 to the end of the list, affecting both s and t
>>> t
[2, 3, 4, 5]
列表还有许多其他修改方法:
append(elem):把elem添加到列表末尾,返回None.extend(s):添加可迭代对象中的所有元素s添加到列表末尾,返回None.insert(i, elem):把elem插入索引i。如果i大于或等于列表长度,则把elem插入末尾。这个操作不会替换现有元素,只会添加新元素elem,返回None.remove(elem):删除列表中第一次出现的elem,返回None。如果elem不在列表中则报错。pop(i):删除并返回指定索引处的元素i.pop():删除并返回最后一个元素。
字典同样支持元素赋值(经常使用)以及 pop (很少使用)。
>>> d = {2: 3, 4: 16}
>>> d[2] = 4
>>> d[3] = 9
>>> d
{2: 4, 4: 16, 3: 9}
>>> d.pop(4)
16
>>> d
{2: 4, 3: 9}
迭代器
可迭代对象 是能够逐个元素遍历的值。我们已经用过 for 语句遍历可迭代对象:
for elem in iterable:
# do something
通常,在可迭代对象上调用内置 可迭代对象 iter
函数会返回一个 迭代器。在迭代器上调用 迭代器 next 函数会返回下一个值。
例如,列表就是一种可迭代值。
>>> s = [1, 2, 3, 4]
>>> next(s) # s is iterable, but not an iterator
TypeError: 'list' object is not an iterator
>>> t = iter(s) # Creates an iterator
>>> t
<list_iterator object ...>
>>> next(t) # Calling next on an iterator
1
>>> next(t) # Calling next on the same iterator
2
>>> next(iter(t)) # Calling iter on an iterator returns itself
3
>>> t2 = iter(s)
>>> next(t2) # Second iterator starts at the beginning of s
1
>>> next(t) # First iterator is unaffected by second iterator
4
>>> next(t) # No elements left!
StopIteration
>>> s # Original iterable is unaffected
[1, 2, 3, 4]
也可以在 for 语句中使用迭代器,因为所有迭代器都是可迭代对象。但迭代器会保存自身状态,因此通常只能完整遍历一次:
>>> t = iter([4, 3, 2, 1])
>>> for e in t:
... print(e)
4
3
2
1
>>> for e in t:
... print(e)
有些内置函数会返回迭代器。这些 Python 序列操作以惰性方式计算结果。
>>> m = map(lambda x: x * x, [3, 4, 5])
>>> next(m)
9
>>> next(m)
16
>>> f = filter(lambda x: x > 3, [3, 4, 5])
>>> next(f)
4
>>> next(f)
5
>>> z = zip([30, 40, 50], [3, 4, 5])
>>> next(z)
(30, 3)
>>> next(z)
(40, 4)
生成器
可以通过编写 生成器函数来创建自定义迭代器。它返回一种特殊的迭代器,称为 生成器。生成器函数的函数体使用 yield 语句,而不是 return
语句。调用生成器函数会返回生成器对象, 不会 立即执行函数体。
例如,观察下面的生成器函数:
def countdown(n):
print("Beginning countdown!")
while n >= 0:
yield n
n -= 1
print("Blastoff!")
调用 countdown(k) 会返回一个从 k
倒数到 0 的生成器对象。生成器也是迭代器,因此可以对结果调用 iter ,它只会返回同一个对象。此时函数体尚未执行,不会打印任何内容,也不会输出数字。
>>> c = countdown(5)
>>> c
<generator object countdown ...>
>>> c is iter(c)
True
那么倒数是怎样进行的?生成器是迭代器,所以我们调用
next 获取下一个元素。第一次调用 next 时,从函数体第一行开始执行,直到遇到
yield 语句。对其中表达式求值所得的结果会被返回。下面的交互会话接续上面的例子。
yield
>>> next(c)
Beginning countdown!
5
与此前见过的函数不同,生成器函数可以记住自身状态。后续每次调用 next时,都会从上一次执行的 yield 语句之后继续。与第一次调用 next一样,程序会一直执行到下一个 yield
语句。正因如此, Beginning countdown! 不会再次打印。
>>> next(c)
4
>>> next(c)
3
接下来三次调用 next 会继续依次产生递减整数,直到 0。再调用一次时会引发 StopIteration 错误,因为已经没有值可以产生(也就是在再次遇到 yield 语句前就到达了函数体末尾)。
>>> next(c)
2
>>> next(c)
1
>>> next(c)
0
>>> next(c)
Blastoff!
StopIteration
分别调用 countdown 会创建各自拥有独立状态的生成器对象。通常生成器不会自行重新开始;若要重置序列,请再次调用生成器函数创建新对象。
>>> c1, c2 = countdown(5), countdown(5)
>>> c1 is c2
False
>>> next(c1)
5
>>> next(c2)
5
总结如下:
- 一个 生成器函数 包含
yield语句,并返回 生成器对象. - 对生成器对象调用
iter函数会返回同一个对象,不会修改其当前状态。 - 只有对生成器对象调用
next时才会执行生成器函数体。调用next会计算并返回序列中的下一个对象。如果序列已经耗尽,则引发StopIteration。 生成器会为下一次
next调用“记住”当前状态。因此,第一次
next调用的过程如下:- 进入函数并运行到包含
yield. - 返回
yield语句中的值,同时保存函数状态供后续next调用使用。
- 进入函数并运行到包含
后续
next调用的过程如下:- 重新进入函数,从 上次执行的
yield语句下一行开始,运行到下一个yield语句。 - 返回
yield语句中的值,同时保存函数状态供后续next调用使用。
- 重新进入函数,从 上次执行的
- 调用生成器函数会返回全新的生成器对象(类似于对可迭代对象调用
iter)。 - 除非定义如此,否则生成器不会重新开始。要从第一个元素重新开始,只需再次调用生成器函数创建新生成器。
生成器还有一个实用工具: yield from 语句。 yield from
会依次产生某个迭代器或可迭代对象中的所有值。
>>> def gen_list(lst):
... yield from lst
...
>>> g = gen_list([1, 2, 3, 4])
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
4
>>> next(g)
StopIteration
可变性
问题 1:WWPD——列表修改
Important: 对于所有 WWPD 题,如果你认为答案是
Function,请输入<function...>,Error;如果会报错,输入Nothing;如果没有任何显示,则输入
使用 Ok 完成下面的“Python 会显示什么?”(WWPD)题,检查你对知识的掌握情况:
python3 ok -q list-mutation -u
>>> s = [6, 7, 8]
>>> print(s.append(6))
______None
>>> s
______[6, 7, 8, 6]
>>> s.insert(0, 9)
>>> s
______[9, 6, 7, 8, 6]
>>> x = s.pop(1)
>>> s
______[9, 7, 8, 6]
>>> s.remove(x)
>>> s
______[9, 7, 8]
>>> a, b = s, s[:]
>>> a is s
______True
>>> b == s
______True
>>> b is s
______False
>>> a.pop()
______8
>>> a + b
______[9, 7, 9, 7, 8]
>>> s = [3]
>>> s.extend([4, 5])
>>> s
______[3, 4, 5]
>>> a
______[9, 7]
>>> s.extend([s.append(9), s.append(10)])
>>> s
______[3, 4, 5, 9, 10, None, None]
问题 2:插入元素
请编写函数,它接收列表 s、值 before以及值
after。它会原地修改 s :在每个等于 after 的值后面插入 before 。 s它返回 s.
Important: 不要创建任何新列表。
注意: 如果传给
before和after的值相等,请确保遍历时不会不断插入而创建无限长的列表。如果代码运行超过几秒,函数可能陷入了不断插入新值的无限循环。
def insert_items(s: list[int], before: int, after: int) -> list[int]:
"""Insert after into s following each occurrence of before and then return s.
>>> test_s = [1, 5, 8, 5, 2, 3]
>>> new_s = insert_items(test_s, 5, 7)
>>> new_s
[1, 5, 7, 8, 5, 7, 2, 3]
>>> test_s
[1, 5, 7, 8, 5, 7, 2, 3]
>>> new_s is test_s
True
>>> double_s = [1, 2, 1, 2, 3, 3]
>>> double_s = insert_items(double_s, 3, 4)
>>> double_s
[1, 2, 1, 2, 3, 4, 3, 4]
>>> large_s = [1, 4, 8]
>>> large_s2 = insert_items(large_s, 4, 4)
>>> large_s2
[1, 4, 4, 8]
>>> large_s3 = insert_items(large_s2, 4, 6)
>>> large_s3
[1, 4, 6, 4, 6, 8]
>>> large_s3 is large_s
True
"""
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q insert_items
问题 3:分组
请编写函数,它接收列表 s 以及函数 fn,返回一个字典,根据应用 s 所得结果对 fn.
- 中的元素分组。每一种不同的
fn调用结果都应成为字典中的一个键。s. - 每个键对应的值是一个列表,包含
s中所有传给fn后会产生该键的元素。
换句话说,对每个元素 e 。 s,先求出 fn(e) ,再把 e 加入字典中 fn(e) 对应的列表。
def group_by(s: list[int], fn) -> dict[int, list[int]]:
"""Return a dictionary of lists that together contain the elements of s.
The key for each list is the value that fn returns when called on any of the
values of that list.
>>> group_by([12, 23, 14, 45], lambda p: p // 10)
{1: [12, 14], 2: [23], 4: [45]}
>>> group_by(range(-3, 4), lambda x: x * x)
{9: [-3, 3], 4: [-2, 2], 1: [-1, 1], 0: [0]}
"""
grouped = {}
for ____ in ____:
key = ____
if key in grouped:
____
else:
grouped[key] = ____
return grouped
使用 Ok 测试你的代码:
python3 ok -q group_by
迭代器
问题 4:WWPD——迭代器
Important: 请输入
StopIteration(如果发生StopIteration异常)、Error(如果发生其他错误),或者Iterator(如果输出为迭代器对象)。
Important: Python 的内置函数
map,filter和zip返回的是 迭代器,而不是列表。
使用 Ok 完成下面的“Python 会显示什么?”(WWPD)题,检查你对知识的掌握情况:
python3 ok -q iterators-wwpd -u
>>> s = [1, 2, 3, 4]
>>> t = iter(s)
>>> next(s)
______Error
>>> next(t)
______1
>>> next(t)
______2
>>> next(iter(s))
______1
>>> next(iter(s))
______1
>>> u = t
>>> next(u)
______3
>>> next(t)
______4
>>> r = range(6)
>>> r_iter = iter(r)
>>> next(r_iter)
______0
>>> [x + 1 for x in r]
______[1, 2, 3, 4, 5, 6]
>>> [x + 1 for x in r_iter]
______[2, 3, 4, 5, 6]
>>> next(r_iter)
______StopIteration
>>> map_iter = map(lambda x : x + 10, range(5))
>>> next(map_iter)
______10
>>> next(map_iter)
______11
>>> list(map_iter)
______[12, 13, 14]
>>> for e in filter(lambda x : x % 4 == 0, range(1000, 1008)):
... print(e)
______1000
1004
>>> [x + y for x, y in zip([1, 2, 3], [4, 5, 6])]
______[5, 7, 9]
问题 5:统计出现次数
请实现 count_occurrences,它接收迭代器 t、整数 n以及值 x。它返回 n 的前 t 个元素中等于 x.
可以假设 t 至少包含 n 个元素。
重要:应该对
next调用t恰好n次。如果发现需要遍历超过n个元素,请思考怎样优化答案。
def count_occurrences(t: Iterator[int], n: int, x: int) -> int:
"""Return the number of times that x is equal to one of the
first n elements of iterator t.
>>> s = iter([10, 9, 10, 9, 9, 10, 8, 8, 8, 7])
>>> count_occurrences(s, 10, 9)
3
>>> t = iter([10, 9, 10, 9, 9, 10, 8, 8, 8, 7])
>>> count_occurrences(t, 3, 10)
2
>>> u = iter([3, 2, 2, 2, 1, 2, 1, 4, 4, 5, 5, 5])
>>> count_occurrences(u, 1, 3) # Only iterate over 3
1
>>> count_occurrences(u, 3, 2) # Only iterate over 2, 2, 2
3
>>> list(u) # Ensure that the iterator has advanced the right amount
[1, 2, 1, 4, 4, 5, 5, 5]
>>> v = iter([4, 1, 6, 6, 7, 7, 6, 6, 2, 2, 2, 5])
>>> count_occurrences(v, 6, 6)
2
"""
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q count_occurrences
生成器
问题 6:生成排列
对于元素互不相同的序列,它的一个 排列 是包含序列全部元素、但顺序任意的列表。例如,
[1, 2, 3], [2, 1, 3], [1, 3, 2]和 [3, 2, 1] 是序列的一部分排列。 [1, 2, 3].
请实现 perms是一个生成器函数,接收序列 seq
并返回一个能够产生其所有排列的生成器 seq。本题中可以假设 seq 不为空。
排列可以按任意顺序产生。
提示: 请记住,生成器也是迭代器,因此可以在循环中遍历生成器对象。
def perms(seq):
"""Generates all permutations of the given sequence. Each permutation is a
list of the elements in SEQ in a different order. The permutations may be
yielded in any order.
>>> p = perms([100])
>>> type(p)
<class 'generator'>
>>> next(p)
[100]
>>> try: # Prints "No more permutations!" if calling next would cause an error
... next(p)
... except StopIteration:
... print('No more permutations!')
No more permutations!
>>> sorted(perms([1, 2, 3])) # Returns a sorted list containing elements of the generator
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
>>> sorted(perms((10, 20, 30)))
[[10, 20, 30], [10, 30, 20], [20, 10, 30], [20, 30, 10], [30, 10, 20], [30, 20, 10]]
>>> sorted(perms("ab"))
[['a', 'b'], ['b', 'a']]
"""
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q perms
在本地检查得分
你可以运行以下命令,在本地检查本次作业每道题的得分:
python3 ok --score
这不会提交作业! 确认得分符合预期后,请将作业提交到 Gradescope,以获得作业学分。
提交作业
请上传所有你修改过的文件,将本次作业提交到 对应的 Gradescope 作业入口。 Lab 00 中提供了详细说明。
正确完成所有题目可得 1 分。 Please ensure your TA has taken your attendance before leaving.
可选题
以下题目为选做题。即使不完成,你仍能获得本次作业的学分;不过它们很适合用来练习,建议还是做一做!
问题 7:连续重复
请实现 repeated,它接收迭代器 t 和整数 k (大于 1),返回 t 中第一个连续出现 k 次的值。可以假设 t 中一定存在至少连续重复 k 次的元素。
Important: 调用
next调用t的次数应尽可能少。如果遇到StopIteration异常,说明repeated函数调用next的次数过多。
def repeated(t: Iterator[int], k: int) -> int:
"""Return the first value in iterator t that appears k times in a row,
calling next on t as few times as possible.
>>> s = iter([10, 9, 10, 9, 9, 10, 8, 8, 8, 7])
>>> repeated(s, 2)
9
>>> t = iter([10, 9, 10, 9, 9, 10, 8, 8, 8, 7])
>>> repeated(t, 3)
8
>>> u = iter([3, 2, 2, 2, 1, 2, 1, 4, 4, 5, 5, 5])
>>> repeated(u, 3)
2
>>> repeated(u, 3)
5
>>> v = iter([4, 1, 6, 6, 7, 7, 8, 8, 2, 2, 2, 5])
>>> repeated(v, 3)
2
"""
assert k > 1
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q repeated