python 数据生成器_Python 生成器

生成器 Generator 是迭代器的一种.

Review Iterator

上篇呢, 对迭代器 有过谈到, 从 迭代过程, 迭代对象, 迭代器都进行了说明, 首先要理解概念, 其实理解词性就可以. 迭代器 对 可迭代对象 进行 迭代. 从主谓宾上就理清了这几个名词. 更通俗一般地理解:

迭代: 在代码中表现为对 某个对象进行 for 遍历 的过程 (包含了 next())

可迭代对象: 能够被 遍历 的对象, 如 list, tuple, str, dict, range, enumerate, zip ....

迭代器: 能够被 next() 函数调用, 并不断返回下一个值的对象. 即实现了 __ iter __ 和 __ next __ 方法.

for 循环原理: 会先调用 __ iter __ 方法, 然后不断调用 __ next __ 方法, 直到捕捉到异常类 StopIteration

# for的原理

class A:

def __iter__(self):

print("__iter__ is called")

return self

def __next__(self):

print("__next__ is called")

for i in range(3): print(i)

raise StopIteration

if __name__ == '__main__':

for _ in A(): pass

__iter__ is called

__next__ is called

0

1

2

__ iter __ 必须返回 self 对象本身, 试过其他的好像不行, 没想研究太过于底层,暂时

__ next __ 如果没有定义异常, 则会 自动反复调用 __ next __ , 异常是为了停止该函数执行的.

貌似讲了很多迭代器的概念理解,然而, 具体在业务中如何应用, 似乎没有怎么涉及, 除了 for 循环外, 似乎也没怎么涉及, so, 本篇的 生成器, 就是来做应用的.

Generator 痛点

我的痛点是这样的.

痛点1: 读 大文件

有时候, 我会 读取大文件, GB级这种, 但只是看看列字段, 或者预览几行这样子, 如果全部给读进内存, 这非常耗时, 而且非常浪费时间, 又感觉不值得. 再考虑极端情况, 我的电脑是 8GB 的内存, 但我要读取一个 16GB 的文件, 这直接读肯定内存就爆了呀....

痛点2: 超大容器 (list, dict)

之前在写爬虫程序的时候, 函数需要传递一大批的 url. 可能有几十万个. 通常呢, 会用一个容器如 list 来装起来, 但这量特大的时候, 内存受不了或者浪费, 因为 url 也是一个个 处理的呀, 传100万长度的 list 也是挨个处理 (假设没有 多任务) .

痛点3: 懒加载

体现在代码层面. 有时候呢, 我想让一个函数实现一个 触发 的效果, 每次被触发都返回一个值, 但程序呢, 没有结束, 而是出于一个 阻塞,监听 的状态. 或者说, 让函数 有记忆, 本次调用的时候,, 能够记得上一次的结果等. (就像排队时的取票机一样, 每点击一次取票, 则拿到的号码是上一次 + 1)

而这类问题的解决办法, 就是生成器. (是迭代器的一种). **这种一边迭代, 一边计算的机制, 就是生成器. ** 有点像一个车递推的过程, 而非先算出所有的结果.

需求梳理

在代码执行效率上, 内存上等需要进行优化. (尤其是 大文件, 大列表, 大字典等的处理)

需构造一个对象或一种机制, 能够对对象, 一边迭代, 一边计算.

让一个函数能不断返回值而不终止函数运行. 有 "记忆", 能够记住上次的动作.

应用场景: 读取大文件, 懒加载, 批量插入数据到数据库, 爬虫ur处理等.

当理解迭代器之后, 这不就是要实现一个, **不断调用 __ next __ 方法 和 __ iter __ ** 的对象呀. 事先先构造好一个容器对象, 或者元素推导的规则等. 然后进行遍历. 不同在于, 不是先算好所有的结果存起来遍历, 而是 一边迭代, 一边遍历, 这样就节约内存了呀.

生成器-实现

语法层面上, 就两个方式, 通过元组推导式, 或者 在函数中 使用 yeild 关键字.

推导式

这算是 Python 简洁的体现吧, 常见的有, 列表推导式, 元组推导式, 字典推导式 等

[ i 2 for i in range(100) ]; 复杂的还可以判断和嵌套, 如 [ i ** 2 for i in range(100) if i % 2 == 0]

字典推导倒是用的挺少的, 不来栗子了. 元组推导式, 就是一个生成器, 挺有趣的还.

方式1: 元组推导式

lst = [i for i in range(5)]

print(lst

g = (i for i in range(5))

print(g)

# output

[0, 1, 2, 3, 4]

at 0x0000023FF171BF10>

list 推导式返回的一个有值列表

tuple 推导式返回的是个对象地址, 是个 generator

区别在于, 后者在函数执行时不耗时, __ next __ 才会去执行.

对于list 可直接通过 下标 来打印任意一个元素, 而 生成器不行, 只能通过 通过 next() 来不断获取.

# 方式1: for遍历

for i in g:

print(i, end=' ')

# ouput

0 1 2 3 4

for 遍历 其实也是 调用 __ next __() 或者 next() 这两个 next 是一样的

内置函数 与其 魔法方法 的映射

next() 的魔法方法就是 __ next __ () , 相当于, " + " 这个运算符对应的 魔法方法是 __ __ add __(), 有些可以直接用下划线这种, 有些不可以, 只能尝试.

# 方式2 调用 next() 或 obj.__next__() 一样的.

>>> g = (i for i in range(5))

>>> g.__next__()

0

>>> next(g)

1

>>> next(g)

2

>>> g.__next__()

3

>>> g.__next__()

4

>>> g.__next__()

Traceback (most recent call last):

File "", line 1, in

StopIteration

list 保存的是实际的值, 而 generator 保存的是一个算法的地址. 其每次调用 __ next __ () 就会计算下一个值, 直到最后, 抛出 StopIteration 异常. 同时从代码编写上, 显然在程序中, 咱是不可能去 一个个 __ next __ 的, for 遍历显然更优雅.

方式2: yeild 关键字

yield 读作 (美 [jiːld]) 作动词表示 生产, 产出, 屈服等; 做名词表示 产量, 利润. 在函数中将 return 改 yeild , 该函数就变成了一个生成器.

def fib(num):

count, a, b = 0, 0, 1

while count < num:

print(b, end=" ")

a, b = b, a + b

count += 1

if __name__ == '__main__':

fib(5)

# output

1 1 2 3 5

这个函数过程, 其实是没有存储中间过程的值的. 而生成器, 就这个词非常直观, 保留了算法.

def fib(num):

count, a, b = 0, 0, 1

while count < num:

# print(b, end=" ")

yield b

a, b = b, a + b

count += 1

if __name__ == '__main__':

ret = fib(5)

print(ret)

# 遍历生成器 里面的元素

print([i for i in ret])

[1, 1, 2, 3, 5]

case1: yeild 和 return

真实中不会这么写, 没啥意义, 只是为了连接函数在有 yeild 之后, 执行的顺序怎样的.

def fib(num):

count, a, b = 0, 0, 1

while count < num:

a, b = b, a + b

count += 1

yield b

return "一次就结束"

if __name__ == '__main__':

ret = fib(5)

print(ret)

# 遍历生成器

print([i for i in ret])

[1]

可以看出, 在函数中有 yield 后, 代码会 反复被执行, 每遇到 yield 就返回值, 直到遇见 return 或 异常则终止. 而return 则是彻底结束函数的运行.

其他的应用场景, 如 爬虫方面, 有用过 Scrapy 框架的就知道, 继承于 CrawlSpider 类的 数据处理函数, 要求的就是要 yeild item. 一边爬取, 一边解析, 将结果 yeild 给 pipelines 来存储.

就不贴代码了, 太长了, 理解就行.

还有之前在 读取大文件的时候, open() 其实就是一个迭代器, readline() 就相等于 next() 一行. 而读取大文件的方式就是, 分块读, 处理, 在读这样子, 每次读一定量的数据, 处理好了, yield 结果. 然后再继续读....

还有就是在一些传参, 传递一个车 迭代器对象, 让其一边调用, 一边处理. 我之前有写个 批量数据插入 mysql的帖子.

# args 就是一个巨大excel表的数据, 以可迭代对象的方式传参

_ = cursor.executemany(insert_sql, args)

只要真正理解了迭代器, 就自然懂了 yield , 以及其 这种懒加载的思想了, 应该是一边执行, 一边计算.

小结

迭代器的核心方式是 __ iter __ 和 __ next __ 理解 for 原理就大致明白了.

生成器也迭代器的一种, 就是反复调用 __ next __ 夹杂着业务逻辑呀

生成器 实现有两种方式: 元组推导式, 函数中 有 yield 关键字.

二者配合, 应用场景有, 读取大文件, 爬虫, 批量传参.

核心: 懂这种, 一遍加载, 一遍执行, 能提高效率和节省内存, 就可以了.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值