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)。
可变对象在函数内外是同一份内存。
python
def add_item(lst):
lst.append(100) #直接修改外部传入的列表,没有return
my_list = [1,2,3]
add_item(my_list)
print(my_list) # [1, 2, 3, 100]
pandas、numpy大量代码用这种模式;很多方法
inplace=True就是这个原理。
不可变对象(int、str、tuple)这种方式无效:
python
def f(x):
x = x +1
a = 10
f(a)
print(a) # 10,没有变化,int不可变
2. yield from (委托子生成器)
不是替换yield,是生成器内部委托另一个生成器,把子生成器所有值逐个产出。
python
def sub_gen():
yield 1
yield 2
def main_gen():
yield from sub_gen() #把sub_gen全部产出交给外层
yield 3
g = main_gen()
print(list(g)) # [1,2,3]
等价手写:
python
def main_gen():
for v in sub_gen():
yield v
yield 3
3. raise 抛出异常(不是返回值,向外抛出错误信号)
函数不返回结果,向上抛出异常,交由调用方捕获处理。
python
def div(a,b):
if b == 0:
raise ZeroDivisionError("分母不能为0")
return a / b
# div(10,0) #会抛出异常,程序中断,除非try捕获
区别:return是"给结果正常结束";raise是"报告出错,异常退出"。
4. 全局变量 / nonlocal 修改外层变量(不推荐工程使用)
函数内部修改全局作用域变量,实现向外输出,可读性差,工程尽量避免。
python
res = 0
def calc(x):
global res
res = x * 2
calc(5)
print(res) #10
5. print / stdout 标准输出(仅仅打印,不是函数返回值)
很多新手混淆:print只是控制台打印文字,函数返回值是None。
python
def hello():
print("hello world")
ret = hello()
print(ret) # None,print输出到屏幕,并没有作为返回值交给变量ret
6. 回调函数 callback(把结果传给外部传入的函数)
函数不把结果返回给自己调用方,把结果丢给传入的回调函数。
python
def compute(a,b, callback):
s = a + b
callback(s) #把结果传给回调函数
def my_cb(val):
print(f"得到结果:{val}")
compute(3,5, my_cb) #输出:得到结果:8
异步IO、很多库底层大量使用回调模式。
7. 闭包 nonlocal:内层函数修改外层函数变量
python
def outer():
count = 0
def inner():
nonlocal count
count +=1
return count
return inner
f = outer()
print(f()) #1
print(f()) #2
8. 类实例属性(面向对象方式)
函数(方法)把结果存到实例对象的成员变量,外部读取实例属性拿结果。
python
class ResultHolder:
def __init__(self):
self.val = None
def calc(self,x):
self.val = x**2 #计算结果存入实例属性
obj = ResultHolder()
obj.calc(6)
print(obj.val) #36