CS1602计算导论
Lab 13Part 6 Pythonic 与现代实践AI Level 1

迭代器与生成器

七道题。前四题写生成器,第五题用 itertools 和 collections 简化代码,最后两题实测内存差别并处理一个真实的大数据场景。

截止:小作业 5 · 12 月 29 日 周二 23:59
本页目录

本次目标

  • 用 yield 写出比类简洁得多的迭代逻辑
  • 分清可迭代对象和迭代器,避开”只能用一次”的坑
  • 用 itertools 和 collections 替掉手写的样板代码
  • 亲手测出生成器省下的内存

题目

13-1 基础生成器

实现三个生成器:

def evens(n: int):
    """产出前 n 个正偶数:2, 4, 6, ..."""

def repeat_each(items: list[str], k: int):
    """每个元素重复产出 k 次。
    repeat_each(['a','b'], 2) → a, a, b, b"""

def running_max(nums: list[int]):
    """产出到目前为止的最大值。
    running_max([3,1,4,1,5]) → 3, 3, 4, 4, 5"""

三个都必须用 yield,不许先造列表再返回。

13-2 生成器串联

实现:

def read_records(lines: list[str]):
    """产出非空、去掉首尾空白的行。"""

def parse_records(lines):
    """接收一个生成器,产出 (姓名, 分数) 元组。跳过格式不对的。"""

def passing_only(records):
    """接收一个生成器,只产出分数 >= 60 的。"""

然后串起来用:

data = ["张三 87", "", "李四 45", "格式不对", "  王五 92  "]
result = list(passing_only(parse_records(read_records(data))))
# 期望:[("张三", 87), ("王五", 92)]

在提交说明里回答:如果 data 是一个 10GB 的文件,这个串联会占多少内存?为什么?

13-3 排错:为什么结果不对

下面四段代码都有问题,且都不会报错——它们静静地给出错的答案。找出每一段的原因并改对。

# A
def stats(nums):
    gen = (x * 2 for x in nums)
    return sum(gen), max(gen), len(list(gen))
# B
import itertools
data = ["apple", "carrot", "banana", "celery"]
key = lambda w: "fruit" if w in ("apple", "banana") else "veg"
groups = {k: list(g) for k, g in itertools.groupby(data, key=key)}
# C
def first_big(nums, threshold):
    big = [x for x in nums if x > threshold]
    return big[0]
# D
counts = {}
for word in ["a", "b", "a"]:
    counts[word] += 1

13-4 无限生成器

实现 primes(),一个无限产出质数的生成器。

import itertools
print(list(itertools.islice(primes(), 20)))
# 期望:[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]

要求:不能预先设定上限。你的 primes() 必须能一直产出下去。

在提交说明里回答:如果不用生成器,你要怎么写这个函数?会遇到什么问题?

13-5 用标准库简化

下面每段代码都能用 itertools 或 collections 里的一个工具替掉。改写它们,并说明用了什么。

# A:统计词频
counts = {}
for w in words:
    if w in counts:
        counts[w] += 1
    else:
        counts[w] = 1

# B:按首字母分组
groups = {}
for w in words:
    if w[0] not in groups:
        groups[w[0]] = []
    groups[w[0]].append(w)

# C:求累计和
totals = []
s = 0
for x in nums:
    s += x
    totals.append(s)

# D:两个列表的所有配对
pairs = []
for a in list1:
    for b in list2:
        pairs.append((a, b))

# E:只保留最近 100 条记录
recent.append(item)
if len(recent) > 100:
    recent.pop(0)

13-6 内存实测

写一段代码,对比下面两种写法在 n = 10^5, 10^6, 10^7 时的内存占用(用 sys.getsizeof)和创建耗时:

a = [x ** 2 for x in range(n)]
b = (x ** 2 for x in range(n))

整理成一张表。在提交说明里回答:

  1. 列表的内存随 n 怎么变?生成器呢?
  2. 创建一个生成器要多久?为什么?
  3. 如果只需要 sum(),哪个更好?如果需要 sorted() 呢?

13-7 自己实现迭代协议

不用 yield,用类实现一个 Fibonacci 可迭代对象:

class Fibonacci:
    def __init__(self, limit: int) -> None:
        """产出前 limit 个斐波那契数。"""
    def __iter__(self): ...

要求:能被重复遍历。

fib = Fibonacci(10)
print(list(fib))
print(list(fib))     # 必须和第一次一样

然后用 yield 重写一遍,对比两个版本的行数。在提交说明里回答:用 yield 的版本能被重复遍历吗?如果不能,怎么改才能?

提交前自查

  • 13-1 三个生成器都用了 yield,没有先造列表
  • 13-2 回答了 10GB 文件的内存问题
  • 13-3 四段都找出了原因,C 段用了生成器改写
  • 13-4 的 primes() 没有预设上限
  • 13-5 五段都改写了并说明用了什么工具
  • 13-6 有完整的表和三个问题的回答
  • 13-7 两个版本都实现了,回答了重复遍历的问题
  • AI 使用声明写了