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]

<generator object <genexpr> 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 "<stdin>", line 1, in <module>
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])
<generator object fib at 0x000001C1EE0FBF10>
[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])

<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 , 以及其 这种懒加载的思想了, 应该是一边执行, 一边计算.

小结

  • 迭代器的核心方式是 __ iter __ 和 __ next __ 理解 for 原理就大致明白了.
  • 生成器也迭代器的一种, 就是反复调用 __ next __ 夹杂着业务逻辑呀
  • 生成器 实现有两种方式: 元组推导式, 函数中 有 yield 关键字.
  • 二者配合, 应用场景有, 读取大文件, 爬虫, 批量传参.

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

posted @ 2020-02-08 22:50  致于数据科学家的小陈  阅读(281)  评论(0编辑  收藏  举报