科普:Python中的生成器——带`yield`的函数

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) #无输出

工程取舍:

  1. 大数据(千万量数级样本):不要list(),直接for迭代生成器 ,内存只保留当前折数组。
    因为:如果把kf.split(df)全部list保存,大数据场景会额外消耗大量内存。

    for tr_idx, val_idx in kf.split(df):
    #做OOF特征工程
    pass

  2. 小数据集,需要多次复用划分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完全一致。

四、重要易错点举例

  1. ❌生成器不能下标索引

    gen = kf.split(df)
    print(gen[0]) #报错 TypeError,生成器不支持[]取元素

  2. ❌生成器遍历一次就空

    g = (i for i in range(3))
    print(list(g)) #[0,1,2]
    print(list(g)) #[]

  3. 区分:迭代器≠生成器>

生成器属于迭代器的一种;但迭代器不一定是生成器。

复制代码
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

相关推荐
whcyhhh17 分钟前
头歌实践教学平台:数据科学与大数据技术导论(十四)
大数据·开发语言·python
清水白石00817 分钟前
Python dataclass 高级避坑指南:从 default_factory、frozen、slots 到哈希灾难
windows·python·哈希算法
在学了加油21 分钟前
阿尔茨海默病诊断(优化特征选择版)
人工智能·python·dnn
淼澄研学30 分钟前
GPT-4o及mini模型参数解析与Python API调用实操
开发语言·python
独立开发之道1 小时前
【three.js教程】Three.js 材质详解:从“不反光“到“物理级真实“怎么选
开发语言·javascript·材质
kyrie_sakura1 小时前
python学习笔记 7--- 文件(IO)操作
笔记·python·学习
Patrick在香港1 小时前
Claude Agent 进阶编排:循环控制 + 写权限审批闸门 + 幂等重试
python·agent·claude·编排·anthropic api
Highcharts.js1 小时前
Highcharts for Python 一套 Python 可视化图表生成库
开发语言·python·highcharts·可视化图表