生成器 Generator 是迭代器的一种.
上篇呢, 对迭代器 有过谈到, 从 迭代过程, 迭代对象, 迭代器都进行了说明, 首先要理解概念, 其实理解词性就可以. 迭代器 对 可迭代对象 进行 迭代. 从主谓宾上就理清了这几个名词. 更通俗一般地理解:
可迭代对象: 能够被 遍历 的对象, 如 list, tuple, str, dict, range, enumerate, zip ....
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
貌似讲了很多迭代器的概念理解,然而, 具体在业务中如何应用, 似乎没有怎么涉及, 除了 for 循环外, 似乎也没怎么涉及, so, 本篇的 生成器, 就是来做应用的.
我的痛点是这样的.
有时候, 我会 读取大文件, GB级这种, 但只是看看列字段, 或者预览几行这样子, 如果全部给读进内存, 这非常耗时, 而且非常浪费时间, 又感觉不值得. 再考虑极端情况, 我的电脑是 8GB 的内存, 但我要读取一个 16GB 的文件, 这直接读肯定内存就爆了呀....
之前在写爬虫程序的时候, 函数需要传递一大批的 url. 可能有几十万个. 通常呢, 会用一个容器如 list 来装起来, 但这量特大的时候, 内存受不了或者浪费, 因为 url 也是一个个 处理的呀, 传100万长度的 list 也是挨个处理 (假设没有 多任务) .
体现在代码层面. 有时候呢, 我想让一个函数实现一个 触发 的效果, 每次被触发都返回一个值, 但程序呢, 没有结束, 而是出于一个 阻塞,监听 的状态. 或者说, 让函数 有记忆, 本次调用的时候,, 能够记得上一次的结果等. (就像排队时的取票机一样, 每点击一次取票, 则拿到的号码是上一次 + 1)
而这类问题的解决办法, 就是生成器. (是迭代器的一种). 这种一边迭代, 一边计算的机制, 就是生成器. 有点像一个车递推的过程, 而非先算出所有的结果.
在代码执行效率上, 内存上等需要进行优化. (尤其是 大文件, 大列表, 大字典等的处理)
应用场景: 读取大文件, 懒加载, 批量插入数据到数据库, 爬虫ur处理等.
当理解迭代器之后, 这不就是要实现一个, 不断调用 __ next __ 方法 和 __ iter __ 的对象呀. 事先先构造好一个容器对象, 或者元素推导的规则等. 然后进行遍历. 不同在于, 不是先算好所有的结果存起来遍历, 而是 一边迭代, 一边遍历, 这样就节约内存了呀.
语法层面上, 就两个方式, 通过元组推导式, 或者 在函数中 使用 yeild 关键字.
推导式
这算是 Python 简洁的体现吧, 常见的有, 列表推导式, 元组推导式, 字典推导式 等
[ i 2 for i in range(100) ]; 复杂的还可以判断和嵌套, 如 [ i ** 2 for i in range(100) if i % 2 == 0]
字典推导倒是用的挺少的, 不来栗子了. 元组推导式, 就是一个生成器, 挺有趣的还.
lst = [i for i in range(5)]
print(lst
g = (i for i in range(5))
print(g)
# output
[0, 1, 2, 3, 4]
<generator object <genexpr> at 0x0000023FF171BF10>
# 方式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 "<stdin>", line 1, in <module>
StopIteration
list 保存的是实际的值, 而 generator 保存的是一个算法的地址. 其每次调用 __ next __ () 就会计算下一个值, 直到最后, 抛出 StopIteration 异常. 同时从代码编写上, 显然在程序中, 咱是不可能去 一个个 __ next __ 的, for 遍历显然更优雅.
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])
<generator object fib at 0x000001C1EE0FBF10>
[1, 1, 2, 3, 5]
真实中不会这么写, 没啥意义, 只是为了连接函数在有 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])
<generator object fib at 0x0000016417E6BF10>
[1]
可以看出, 在函数中有 yield 后, 代码会 反复被执行, 每遇到 yield 就返回值, 直到遇见 return 或 异常则终止. 而return 则是彻底结束函数的运行.
其他的应用场景, 如 爬虫方面, 有用过 Scrapy 框架的就知道, 继承于 CrawlSpider 类的 数据处理函数, 要求的就是要 yeild item. 一边爬取, 一边解析, 将结果 yeild 给 pipelines 来存储.
就不贴代码了, 太长了, 理解就行.
还有之前在 读取大文件的时候, open() 其实就是一个迭代器, readline() 就相等于 next() 一行. 而读取大文件的方式就是, 分块读, 处理, 在读这样子, 每次读一定量的数据, 处理好了, yield 结果. 然后再继续读....
还有就是在一些传参, 传递一个车 迭代器对象, 让其一边调用, 一边处理. 我之前有写个 批量数据插入 mysql的帖子.
# args 就是一个巨大excel表的数据, 以可迭代对象的方式传参
_ = cursor.executemany(insert_sql, args)
只要真正理解了迭代器, 就自然懂了 yield , 以及其 这种懒加载的思想了, 应该是一边执行, 一边计算.
核心: 懂这种, 一遍加载, 一遍执行, 能提高效率和节省内存, 就可以了.
原文:https://www.cnblogs.com/chenjieyouge/p/12285545.html