Python学习之旅—生成器与迭代器案例剖析

 前言

   前面一篇博客笔者带大家详细探讨了生成器与迭代器的本质,本次我们将实际分析一个具体案例来加深对生成器与迭代器相关知识点的理解。


   本次的案例是一个文件过滤操作,所做的主要操作就是过滤出一个目录下的文件中含有python的行。我们先直接上代码:

 

import os

def init(func):  #预激生成器 装饰器
    def wrapper(*args,**kwargs):
        g=func(*args,**kwargs) # 这里是一个生成器函数
        next(g)
        return g
    return wrapper

@init
def list_files(target):  #target为opener_g
    while 1:
        dir_to_search = yield
        for top_dir, dir, files in os.walk(dir_to_search):
            for file in files:
                target.send(os.path.join(top_dir,file))
@init
def opener(target): # target为cat_g while 1: file = yield fn=open(file, encoding='utf-8') target.send((file, fn))
@init
def cat(target): #target为grep_g while 1: file,fn=yield for line in fn: target.send((file,line))
@init
def grep(pattern,target): #target为printer_g while 1: file,line=yield if pattern in line: target.send(file)
@init
def printer(): while 1: file=yield if file: print(file) g = list_files(opener(cat(grep('python',printer())))) g.send('D:\pythoncodes\hellobipython')

 

       【001】init(func)函数是一个装饰器函数,在该函数体里面,被它装饰过的函数在执行之前,就已经执行过一次next,也就是被该装饰器装饰的函数会停在第一个yield那里等你,函数需要等待我们给它一个信号(send方法)或者调用next方法,才能继续往下走,该装饰器函数的主要作用就是预激活生成器。

       【002】继续看下面的函数:list_files(),该函数是一个生成器函数,只要出现dir_to_search = yield赋值表达式,我们就知道这里肯定调用了send方法,这里我们推测是通过send方法传递了一个路径进来。这里有一个os.walk模块,我们通过实际代码来观察该模块的作用:

"""
查看某一个路径下文件和文件夹的信息
打印如下:
返回的是一个元组,第一项为路径或者是子路径,第二项为路径或者是子路径下的文件夹,第三项为路径或者是子路径下的文件名
('D:\\pythoncodes\\hellobipython', ['.idea'], ['config.py', 'RedisDemo.py', 'RequestDemo.py', 'UrllibDemo.py', 'weixinspider.py'])
('D:\\pythoncodes\\hellobipython\\.idea', [], ['hellobipython.iml', 'misc.xml', 'modules.xml', 'workspace.xml'])
""""""
import os
ret = os.walk(r'D:\pythoncodes\hellobipython')  # 保证字符串原样输出,使用r来保证不用转义
print(ret)  # <generator object walk at 0x000002B8F53FBE60>

for i in ret:
    print(i)

        【003】我们接着来看代码:for top_dir, dir, files in os.walk(dir_to_search),在这个for循环中,它只取file。接下来看代码:target.send(os.path.join(top_dir,file)),根据前面生成器和迭代器的知识,这句话使用send方法给生成器target传递了一个参数os.path.join(top_dir,file),这个参数的意思是将绝对路径名和文件名拼接起来,组合成为一个绝对路径,然后传递给生成器target。

  【004】接着我们来看生成器函数opener(target),同理和上面的分析类似,最后一句target.send((file, fn))相当于给生成器target传递一个文件和该文件的文件句柄。

  【005】接下来我们来看生成器函数cat(target),可以看到它通过表达式file,fn=yield接收了文件和该文件的文件句柄,然后获取该文件的每一行,再通过代码target.send((file,line))将该文件和文件的每一行都发送给生成器cat.

   【006】接下来我们来看生成器函数grep(target),file,fn=yield接收文件和该文件的文件句柄,接下来的一行代码:if pattern in line,可以猜测该句的意思是某个目标在不在某一行里面,如果pattern在该行,则将该行发送给生成器target.

  【007】 最后再来观察最后一个生成器函数printer(),函数体执行的主要内容就是接收一个文件file,只要该文件不为空,那么就将该文件打印出来。

以上就是各个函数体的内容,接下来我们就来看看具体的函数加载和调用顺序。


 

【01】Python解释器从上到下执行当读取到第一个@init时相当于执行了如下的语句:list_files = init(list_files),当执行init(list_files)时,就执行了

init函数,此时返回了内部函数名:wrapper。此时这里wrapper里面的func就是list_files。

【02】同理,类似于上面的分析,下面依次会执行opener= init(opener),cat= init(cat).....依次类推,这里是装饰器里面的知识。

【03】在执行完上面的步骤后,接着往下面走:g = list_files(opener(cat(grep('python',printer()))));前面我们说过对于这种嵌套的函数,我们的执行标准就是从里往外面执行,因此这里首先执行printer()。

【04】接着上面的继续分析,执行printer,本质上执行的是wrapper,这是装饰器里面的知识。因为执行printer()语句时,上面的@init已经执行了表达式printer= init(printer),此时printer返回一个wrapper,我们在执行printer()时,就相当于执行wrapper(),此时wrapper里面的func()相当于生成器函数printer(),执行wrapper()里面的内容,相当于就拿到了生成器g,而且对该生成器进行了激活,这就是所谓的预激活生成器。

【05】在执行完毕next(g)后,相当于执行到printer()函数里面的yield表达式:file=yield;注意yield后面没有任何值,因此返回的也是空,而在我们的wrapper函数里面也没有接收返回值,只是执行了一个next(g),接着返回了一个生成器对象g。该生成器对象相当于是执行完printer()语句后的返回值,我们暂且称之为:printer_g。这里只是为了标识,我们只需要知道printer_g是一个生成器函数执行后返回的一个生成器对象,而且该对象已经被激活,根据前面的知识,已经被激活表示此时生成器函数printer()已经执行到第一个yield表达式这里:file=yield,此时就在这里等着,等着谁来给它send一个参数,然后将该参数的值赋值给file。

【06】紧接着我们来执行grep('python',printer()),相当于执行grep('python',printer_g),此时pattern就相当于python,参数target的值就相当于生成器对象printer_g。同样和上面一样,执行grep()相当于执行wrapper(),只是此时wrapper中的func()相当于生成器函数grep(),那么此时我们又拿到了生成器对象grep_g,因为只要执行grep('python',printer()),我们其实就拿到了生成器对象grep_g,只不过该生成器对象在装饰器中被预激活了。那么grep()函数的函数体就执行到表达式file,line=yield这里等待着grep_g通过send方法传递两个参数进来。此时执行完grep函数后,又返回了一个被激活的生成器,和上面一样,我们称之为grep_g。

【07】接着我们来执行cat函数,和上面的分析一样,执行完毕后,我们拿到了一个被激活的生成器:cat_g。

【08】接着我们来执行opener()函数,返回的同样是一个被激活的生成器:open_g,而且opener()函数也执行到函数体里面的yield表达式这里。

【09】最后执行list_files()函数,和上面一样,也返回一个被激活的生成器list_files_g。因此最终表达式变为:g = list_files_g

【10】执行g.send('D:\pythoncodes\hellobipython'),相当于将参数D:\pythoncodes\hellobipython传递给生成器list_files_g,也相当于

传递给表达式dir_to_search = yield左边的dir_to_search,此时dir_to_search = 'D:\pythoncodes\hellobipython'。

【11】接着我们继续执行dir_to_search = yield下面的内容,当执行完毕target.send(os.path.join(top_dir, file))后,此时target为生成器

opener_g。

 【12】我们前面说过opener函数在file=yield这里停着,你现在给我发送了一个完整的文件路径,然后赋值给file。在opener()函数中,我们拿到了

文件句柄,并将文件路径和文件句柄又一起发送给了cat_g。注意,这里文件路径和文件句柄组成了一个元组。

 【13】此时cat函数接收到文件路径和文件句柄,然后在cat函数中,又继续send给生成器grep_g,这里发送的是文件和文件的每一行,以元组的形式发送。

【14】接下来继续从grep_g函数中的表达式file,line=yield开始执行,然后判断pattern是否在line中,根据我们前面传递过来的参数,pattern为python,然后执行

target.send(file),将文件路径发送给生成器函数printer()函数,然后从printer()函数体中的file=yield开始执行,只要文件存在,那么就将该文件打印出来。

总结:

   综合上面的分析,该程序完成的功能就是:一个路径下的所有文件,只要你这个文件中有python关键字,就将该文件打印出来。

思考:为什么要写这样的程序,我们其实可以写一个程序就可以解决。

  1.在程序中,我们肯定要反复地打开和关闭文件,这样就要反复地创建变量;如果使用生成器解决,那么就不会出现这样的问题。生成器会直接保存相应的状态,

每个函数都是单独执行,你执行你的,我执行我的。

  2.如果我们自己开发程序,我们首先要拿到文件,这样,我们每拿到一个文件,就要去调用另一个函数来判断这个文件是否含有关键词,然后再遍历下一个文件。

现在我使用生成器,我直接将文件发送给你,然后我来控制你做,这样就完成了一定的协同工作。即函数与函数之间的调用是依赖于某些条件的。

  3.我们需要明白的是,协程函数的精髓在于它从原来一根弦地执行程序变成了在函数内部通过条件控制函数是否继续执行。也就是说,正常情况下,我们要保证函数执行完毕后然后返回给你,现在我来控制函数在哪停,在哪执行。直到我又找到了符合条件的file;比如我们这里的打印函数printer()和判断函数grep(),你打印你的,我判断我的,大家互不干扰,这就体现了协程的概念。


结语:

         本次总结的内容比较难于理解,主要考察生成器和装饰器的结合使用,希望大家能够好好理解这个程序,真正掌握生成器的使用。

 

posted @ 2017-09-07 19:03  哀乐之巅写年华  阅读(274)  评论(0编辑  收藏  举报