• 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 关键字.
    • 二者配合, 应用场景有, 读取大文件, 爬虫, 批量传参.

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

  • 相关阅读:
    修改ubuntu DNS的步骤/wget url报错: unable to resolve host address的解决方法
    MySQL5.7 Replication主从复制配置教程
    总结一下安装linux系统经验-版本选择-安装ubuntu
    分布式与集群的联系与区别
    spring 后置处理器BeanFactoryPostProcessor和BeanPostProcessor的用法和区别
    mysql几种性能测试的工具使用
    mysql max_allowed_packet查询和修改
    mysql主从复制(超简单)
    10 个免费的网络监控工具(转)
    DOS批处理中%cd%和%~dp0的区别
  • 原文地址:https://www.cnblogs.com/chenjieyouge/p/12285545.html
Copyright © 2020-2023  润新知