跳转到内容

模块、迭代器、类与调试

计算机基础 · Python基础

前置知识:文件、异常及常用标准库

本章目录

本章学习目标

  • 组织可导入且无意外副作用的脚本
  • 理解迭代器的一次性消费与生成器
  • 用小类表达数据并定位异常来源

一、模块如何组织复用代码

一个 Python 文件通常就是一个模块,import 会找到文件、创建模块对象并执行顶层代码。若文件顶层直接读取数据、训练模型或写文件,其他脚本只想导入函数也会触发这些操作。常用约定是把演示入口放进 main 函数,并在 if __name__=='__main__': main() 内调用;直接执行时名字是 __main__,导入时是模块名。

同一进程通常缓存已导入模块,第二次 import 不意味着重新运行整个文件,但交互环境重新加载与新进程启动另当别论。不要命名文件为 numpy.py、csv.py、torch.py 等遮蔽库名;排查导入错误时查看模块 __file__ 和 sys.path。包是一组有组织的模块,初学课程不要求复杂发布流程,但要保持依赖方向简单,避免两个模块互相导入尚未定义的对象。

二、可迭代对象与迭代器

列表可多次开始遍历,每次 iter(list) 得到新的迭代器;迭代器通过 next 提供下一个元素,耗尽时抛 StopIteration。生成器是方便地构造迭代器的方式,函数含 yield 时调用得到生成器对象,而不是立刻执行到最后。每次取值从上次暂停位置继续,因此可以按需处理大文件,不必一次把所有记录读入内存。

一次性消费是重要陷阱:先 sum(generator),再 list(generator) 会得到空列表,因为前一次已经取完。zip、map 和文件迭代也可能具有这种特性。若确实需要多次遍历,重新创建生成器,或在数据规模允许时转成列表。惰性并不意味着没有错误;生成器内部的解析异常可能直到实际迭代才出现,try 应围绕消费过程而不仅是创建对象。

三、类用于表达关联状态与规则

类把相关数据和行为放在一起,例如 Order 保存订单编号、金额和退款,net 方法计算净额。self 是当前实例,各订单实例有自己的状态。对于简单记录,dataclass 可以自动生成初始化和展示方法,减少样板;frozen=True 约束属性重新赋值,但如果字段里包含可变对象,不意味着内部对象也彻底不可变。

类不是比函数更高级的必选方案。只计算均值用函数更清晰;需要多次使用已拟合均值和标准差的标准化器时,类可以表达拟合状态与转换行为。必须区分实例属性和类属性,若把共享列表定义在类体里,不同实例可能意外共用它。建立类前先问:有哪些必须一致维护的状态?生命周期是什么?是否只是一张简单记录?不要为了面向对象而把每一行计算包装成一个类。

四、调试顺序与最小复现

定位问题先缩小输入,保留能触发错误的最小数据,再观察异常和中间状态。traceback 给出调用链,从最后一行异常向上找到属于自己文件的位置;局部打印应包含类型、长度或形状,不只是数据表的全部内容。breakpoint() 可以在交互终端暂停检查变量,但自动验证脚本不能留有等待用户输入的断点。

如果结果错误而无异常,检查不变量:总金额是否守恒、唯一键是否仍唯一、排序是否符合时间顺序、输入是否被原地修改。不要一开始就大范围改代码,那会让错误原因消失又产生新问题。生成器被耗尽、模块顶层有副作用、类实例共享列表都可以通过很小例子揭示。记录触发条件和修复后的断言,后续同类错误出现时才有可靠依据。

五、从课程脚本过渡到项目

配套脚本把数据记录定义成不可重绑的 dataclass,用生成器产生净额,在 main 中汇总并检查;导入该文件不会主动打印。先手算三笔订单净额一百、六十、零,总额一百六十,再观察生成器耗尽后的空列表。这个例子将模块边界、记录结构和按需计算连在一起,不需要外部文件或网络。

做综合项目时,建议把读取、校验、转换和报告分成函数,由一个入口组合;只有训练器这类需要维护状态的对象再使用类。依赖应从上游传入,避免到处读取全局数据。对于陌生代码,先找到入口和返回数据结构,再追踪数据流,不要按文件从第一行机械读到最后。Python 基础学完的标准是能够运行、解释并修改一条清晰的数据处理流程,而不是背下全部语言特性。

完整示例与实际输出

from dataclasses import dataclass

@dataclass(frozen=True)
class Order:
    order_id: int
    amount: int
    refund: int = 0

    def net(self):
        if not 0 <= self.refund <= self.amount:
            raise ValueError('invalid refund')
        return self.amount-self.refund

def net_values(orders):
    for order in orders:
        yield order.net()

def main():
    orders=[Order(1,100),Order(2,80,20),Order(3,50,50)]
    stream=net_values(orders)
    total=sum(stream)
    assert total==160 and list(stream)==[]
    values=list(net_values(orders))
    assert values==[100,60,0]
    print('values',values,'total',total,'exhausted',list(stream))

if __name__=='__main__':
    main()

实际运行通过CPU Python execution.

values [100, 60, 0] total 160 exhausted []

五道自测:先回答,再核对

01 · import 文件时顶层代码会执行吗?

查看提示

导入不仅查找函数。

查看参考答案

首次导入通常执行模块顶层;演示入口应放在 __main__ 守卫中。

02 · sum(g) 后 list(g) 为什么常为空?

查看提示

g 是生成器还是列表?

查看参考答案

生成器已被消费完;需要重新创建或提前保存。

03 · yield 函数调用后是否立即执行全部函数体?

查看提示

惰性迭代。

查看参考答案

不会,返回生成器,实际消费时执行到各次 yield。

04 · 类体定义一个可变列表可能怎样?

查看提示

实例与类级状态。

查看参考答案

实例可能共享同一类属性列表;应在实例初始化时创建独立状态。

05 · 数据处理错误但无 traceback,应检查什么?

查看提示

业务不变量。

查看参考答案

行数、唯一键、合计、排序、输入修改和数据类型等。

两项动手任务

任务 1 · 写生成器 yielding 三个平方数,证明它是一次性消费。

提示

先 list 一次,再 list 第二次。

完整参考解答与检查方式

首次得到 [1,4,9],再次为空;重新调用生成器函数可恢复。

def squares(values):
    for v in values:
        yield v*v
g=squares([1,2,3]); first=list(g); second=list(g)
assert first==[1,4,9] and second==[]
print(first,second,list(squares([1,2,3])))

任务 2 · 用 dataclass 定义 Product(name,price),实现 quantity 非负整数时计算收入。

提示

记录数据、方法验证约束。

完整参考解答与检查方式

不让负数或浮点数量进入计算,bool 也不作为整数数量接受。

from dataclasses import dataclass
@dataclass(frozen=True)
class Product:
    name: str
    price: int
    def revenue(self,quantity):
        if type(quantity) is not int or quantity<0:
            raise ValueError('quantity must be nonnegative int')
        return self.price*quantity
p=Product('book',20)
assert p.revenue(3)==60 and p.revenue(0)==0
try:
    p.revenue(-1)
except ValueError:
    print('negative quantity rejected')
else:
    raise AssertionError('must reject')
print(p,p.revenue(3))

官方资料与掌握状态

完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。

动手任务记录

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。