>>> lines = [line.rstrip() for line in open('a.txt')] >>> lines ['first line', 'second line', 'third line'] 这个为什么可以
那每次取数据调用__next__,难道就不要取迭代器里拿吗,迭代器里面是iter(open('a.txt')),每次拿不要open?
是因为你把 iter(open('a.txt')) 看作是一段“随时会被执行的代码”,但在 Python 底层,它其实是一个“已经执行完的产物”。
open() 只发生了一次,并且已经“死”了
iter(open('a.txt')) 时:open('a.txt')。这个动作向操作系统申请了文件句柄,并在内存中创建了一个文件对象(File Object)。iter(),把它变成了一个迭代器对象(Iterator)。.iter 里面装的到底是什么?
iter 里面根本没有 open 这个函数,里面装的是 open 留下的“遗产”!a.txt 的底层句柄。每次 __next__ 到底在干嘛?
__next__() 时,Python 做的事情极其简单:open 函数的参与! iter(open('a.txt')) 当成了一个动作(函数调用),但在 Python 底层,它其实是一个已经存在的“活体对象”(内存地址)。1. iter(open('a.txt')) 不是代码,是一个“盒子”
for line in open('a.txt'): 时,iter(open('a.txt')) 这行代码只执行了一次。a.txt 文件的指针(句柄)。open() 这个动作就已经彻底结束了。 内存里只剩下这个“盒子”(迭代器对象)。2. 每次调用 __next__ 到底在干嘛?
__next__() 时,Python 根本不会去执行 open(),也不会重新去 iter()。line 变量。3. 用生活场景来类比
open('a.txt') = 你走到前台,出示身份证,办理借阅手续。(只发生一次,耗时较长)iter() = 图书管理员把书递给你,并在借阅系统里生成了一条借阅记录。(只发生一次)__next__() = 你翻开书,看一页。(发生很多次)__next__)就行了。4. 终极验证:看内存地址
id() 函数来看看内存地址。id() # 我们用一个生成器来模拟这个行为 def my_iter(): print(">>> 正在执行 open 和 iter!") yield 1 yield 2 yield 3 # 获取迭代器 it = my_iter() print(f"迭代器的内存地址: {id(it)}") for item in it: print(f"拿到数据: {item}, 迭代器的内存地址依然是: {id(it)}") >>> 正在执行 open 和 iter! 迭代器的内存地址: 140234567890 拿到数据: 1, 迭代器的内存地址依然是: 140234567890 拿到数据: 2, 迭代器的内存地址依然是: 140234567890 拿到数据: 3, 迭代器的内存地址依然是: 140234567890
在 lines = [line.rstrip() for line in open('a.txt')] 这种写法中,确实没有显式调用 f.close(),文件句柄在代码执行完的那一刻确实还没有被释放。
但这在 Python 中是被允许的,并且有底层的机制来“兜底”
__del__()(析构函数)。当 Python 的垃圾回收器发现这个文件对象不再被任何变量引用时(比如你的列表推导式执行完毕,那个临时的文件对象失去了引用),就会自动触发 __del__(),这个方法内部会调用 close() 来释放文件句柄。f.close()2. 既然 GC 会兜底,为什么大家还强烈建议用 with?
__del__() 是不确定的,可能会延迟很久。OSError: [Errno 24] Too many open files。3. 终极解决方案:with 语句
with with open('a.txt') as f: lines = [line.rstrip() for line in f]
with 语句的底层逻辑是:无论 with 代码块里是正常执行完毕,还是发生了严重的异常崩溃,只要离开这个代码块,它都会强制且立即调用 close() 释放文件句柄。