Python函数输出,除return 外,还有 yield ,这就是本文谈的“生成器”。
一、什么是生成器
生成器(generator):按需动态产出数据,而不会一次性把全部结果放入内存。在大数据处理时,常用它来降低内存需求。
核心标志是:函数内部写yield。
- 带
yield的函数 →生成器函数 - 调用生成器函数,得到返回对象 →生成器对象(不会立刻执行函数体)
yield:生成产出一个值,然后暂停函数;下次迭代,从暂停位置继续执行。即:你要,我才生成;不要,我就不生成;遍历一遍就结束。
示例:
1、产生生成器对象,并没有获得返回值
def gen_func(): yield 10 yield 20 yield 30 g = gen_func() # 仅仅拿到生成器对象,函数体没有运行 print(g) # <generator object gen_func at ...>2、读取生成器两种方式:
#方式1 next()手动取 print(next(g)) #10 print(next(g)) #20 #方式2 for循环自动迭代(最常用) for item in gen_func(): print(item)3、特性:生成器只能遍历一次,遍历完毕直接耗尽,不能重复使用
g = gen_func() lst1 = [x for x in g] lst2 = [x for x in g] print(lst1) # [10,20,30] print(lst2) # [] 已经耗尽,拿不到任何数据对比普通return函数(列表)
普通函数一次性计算全部结果,全部存入内存。
def normal_func(): return [10,20,30] res = normal_func() # [10,20,30]全部已经存在内存,可以反复读取| 普通列表/return函数 | 生成器 yield | |
|---|---|---|
| 数据产生时机 | 调用函数,一次性全部算完 | 迭代的时候,才现场生成一个 |
| 内存占用 | 全部数据驻留内存 | 只保存程序运行状态,不存全部结果 |
| 重复读取 | 可以多次循环 | 遍历1次就耗尽 |
通俗比喻:
- 列表:工厂一次性把所有商品全部生产堆进仓库。
- 生成器:按需生产,要一件,现场做一件,做完暂停。
二、生成器实例
机器学习中的n-折方法中,每次只需对一种划分进行处理,显然符合生成器的特征要求,故常用生成器来处理。
1.kf.split(df)生成器实例(sklearn)
from sklearn.model_selection import KFold import pandas as pd df = pd.DataFrame({"v":range(10)}) kf = KFold(n_splits=5, shuffle=False) gen = kf.split(df) # gen是生成器对象,此时没有计算任何fold下标 # for循环每一轮,触发内部yield,生成一组(tr_idx,val_idx) for tr_idx, val_idx in gen: print("train下标", tr_idx) print("val下标", val_idx)简化的内部伪代码,看懂yield逻辑:
def mock_split(n_samples,n_splits): indices = list(range(n_samples)) for fold in range(n_splits): #计算本折训练、验证下标 val_mask = [...] tr_idx = indices[~val_mask] val_idx = indices[val_mask] yield tr_idx, val_idx # 产出,暂停 #每一次for循环,执行到yield,返回一组下标不会预先把5套下标数组全部生成保存在内存;迭代到哪一折,才生成那一折的数据。
2.生成器转列表list(生成器)
list()会完整消费整个生成器,把所有yield产出的值存进普通列表。
gen = kf.split(df) all_folds = list(gen) print(len(all_folds)) #5,5折,列表存储5组(tr_idx,val_idx)元组 #转list之后,支持多次循环、下标访问 tr0,val0 = all_folds[0] #但是原来的gen已经耗尽,无法再迭代 for t,v in gen: print(t) #无输出工程取舍:
- 大数据(千万量数级样本):不要list(),直接for迭代生成器,内存只保留当前折数组。
因为:如果把kf.split(df)全部list保存,大数据场景会额外消耗大量内存。
for tr_idx, val_idx in kf.split(df): #做OOF特征工程 pass- 小数据集,需要多次复用划分:
all_folds = list(kf.split(df))。
如果不想list占用内存,又要复用划分:固定
random_state,重新调用kf.split(df)。
三、其他常见生成器写法、场景
①生成器表达式,圆括号()
lst = [x**2 for x in range(5)] #列表推导,全部存入内存 gen = (x**2 for x in range(5)) #生成器表达式,惰性 for i in gen: print(i)②读取超大文件(文件对象是迭代器,生成器思想)
不需要一次性把几十GB文件全部读入内存,逐行读取:
with open("big_file.txt","r") as f: for line in f: #一次只拿一行 pass③分块读取超大csv
def read_chunk(path): for chunk in pd.read_csv(path,chunksize=10000): yield chunk for part_df in read_chunk("huge.csv"): #处理每一块数据 pass④sklearn其他返回生成器API
TimeSeriesSplit.split()、GroupKFold.split(),行为和KFold完全一致。
四、重要易错点举例
- ❌生成器不能下标索引
gen = kf.split(df) print(gen[0]) #报错 TypeError,生成器不支持[]取元素- ❌生成器遍历一次就空
g = (i for i in range(3)) print(list(g)) #[0,1,2] print(list(g)) #[]- 区分:迭代器≠生成器>
生成器属于迭代器的一种;但迭代器不一定是生成器。
lst = [1,2,3] it = iter(lst) #迭代器,但没有yield,不是生成器 next(it)附:Python函数输出(return / yield 之外)的其它方式
return:函数正常返回一个(或多个)返回值,函数直接结束。yield:生成器函数,产出值、暂停函数,多次产出,得到生成器对象。
除这两个,函数向外传递数据还有下面几类方式。
1. 修改外部可变对象(无return,直接改传入的容器)
函数不返回东西,直接修改传入的可变对象(list、dict、numpy数组、pandas df)。
可变对象在函数内外是同一份内存。
defadd_item(lst):lst.append(100)#直接修改外部传入的列表,没有returnmy_list=[1,2,3]add_item(my_list)print(my_list)# [1, 2, 3, 100]pandas、numpy大量代码用这种模式;很多方法
inplace=True就是这个原理。
不可变对象(int、str、tuple)这种方式无效:
deff(x):x=x+1a=10f(a)print(a)# 10,没有变化,int不可变2. yield from (委托子生成器)
不是替换yield,是生成器内部委托另一个生成器,把子生成器所有值逐个产出。
defsub_gen():yield1yield2defmain_gen():yieldfromsub_gen()#把sub_gen全部产出交给外层yield3g=main_gen()print(list(g))# [1,2,3]等价手写:
defmain_gen():forvinsub_gen():yieldvyield33. raise 抛出异常(不是返回值,向外抛出错误信号)
函数不返回结果,向上抛出异常,交由调用方捕获处理。
defdiv(a,b):ifb==0:raiseZeroDivisionError("分母不能为0")returna/b# div(10,0) #会抛出异常,程序中断,除非try捕获区别:return是“给结果正常结束”;raise是“报告出错,异常退出”。
4. 全局变量 / nonlocal 修改外层变量(不推荐工程使用)
函数内部修改全局作用域变量,实现向外输出,可读性差,工程尽量避免。
res=0defcalc(x):globalres res=x*2calc(5)print(res)#105. print / stdout 标准输出(仅仅打印,不是函数返回值)
很多新手混淆:print只是控制台打印文字,函数返回值是None。
defhello():print("hello world")ret=hello()print(ret)# None,print输出到屏幕,并没有作为返回值交给变量ret6. 回调函数 callback(把结果传给外部传入的函数)
函数不把结果返回给自己调用方,把结果丢给传入的回调函数。
defcompute(a,b,callback):s=a+b callback(s)#把结果传给回调函数defmy_cb(val):print(f"得到结果:{val}")compute(3,5,my_cb)#输出:得到结果:8异步IO、很多库底层大量使用回调模式。
7. 闭包 nonlocal:内层函数修改外层函数变量
defouter():count=0definner():nonlocalcount count+=1returncountreturninner f=outer()print(f())#1print(f())#28. 类实例属性(面向对象方式)
函数(方法)把结果存到实例对象的成员变量,外部读取实例属性拿结果。
classResultHolder:def__init__(self):self.val=Nonedefcalc(self,x):self.val=x**2#计算结果存入实例属性obj=ResultHolder()obj.calc(6)print(obj.val)#36