Python的生成器函数提供了一种强大的机制来管理数据和计算资源,但是对于Python的新手来说,它们不一定直观。在本文中,我将分解生成器的机制,同时还介绍我希望是一个有启发性的示例:用于管理和流传输S3文件资源的小类。
简介与历史
鉴于使用Python入门和编写实际完成某件事的代码非常容易(例如,遍历值列表,计算和/或打印这些值),对于新手或不经意的Python程序员来说,使用该语言可能并不陌生建立在拖延或延迟计算的概念中。语言本身固有的这种松散(或懒惰)对于那些使用编译语言(例如C ++)的人来说似乎是陌生的。
大多数程序员学习“ 惰性评估 ”,并被教导编写代码以实现这种做法。但是Python在语言中对此的原生支持(通过单个关键字简单而优雅地实现)引入了功能和表达能力,这在其他编程语言中似乎很少见。毫不奇怪的是,作为“ lambda演算 ”的一部分引入了惰性评估作为概念,并且Python(尽管不是唯一的功能语言(例如Lisp))体现了这种功能编程DNA。(我之前写过关于Python函数装饰器的文章,Python对闭包的使用也是lambda演算的遗产的一部分)。
“ PEP 255-Simple Generators ”在2001年引入了generators,将懒惰评估的一种稍微倾斜的表达作为动机:
当生产者职能部门的工作非常艰苦,需要维持所产生的值之间的状态时,大多数编程语言都无法提供令人满意且有效的解决方案……。
机械学
Python生成器函数是一个功能强大的概念,但是与复杂的函数装饰器框架不同,它们是通过非常简单的机制来实现或表示的,即“ yield”语句(yield是PEP 255下添加到Python的新关键字)。
作为及物动词,yield表示生产;作为不及物动词,它表示让步或放弃。单词的两种含义都在Python的生成器函数中起作用。
传统上,我们认为函数在返回单个值,列表或字典形式的多个值或用户定义的对象时通过它们的return语句产生结果。我们认为return语句是函数结束控制并将控制和结果割让回其调用方的一种方式。在return语句之后,运行时环境(解释器)将给定函数的堆栈框架弹出调用堆栈,并且给定函数的“环境”(如其存在)将不复存在(直到下一次调用该函数)。
Python的yield语句完全改变了这种行为。让我们看一下生成器的一个非常简单且人为的示例-带有一些其他代码来演示其用法(代码来自iPython解释器交互式会话):
In [8]: def gen(x): ...: yield x In [9]: g = gen(10)In [10]: gOut[10]: In [11]: next(g)Out[11]: 10In [12]: g = gen(10)In [13]: gOut[13]: In [14]: next(g)Out[14]: 10In [15]: next(g)---------------------------------------------------------------------------StopIteration Traceback (most recent call last) in ()----> 1 next(g)StopIteration: In [16]:
该函数除了“屈服”作为参数传递的值外,什么也不做。但是,仅像“正常”函数那样调用函数不会产生返回值。如果可能,将使用一个参数实例化生成器函数,并将其保存在变量中g。现在,随着next()对生成器对象的调用清楚了,必须迭代生成器以生成值。并且,一旦产生其(单个)值,生成器便会耗尽-随后的调用next()会引发“ StopIteration”异常。如果我们在一个for循环中迭代了此生成器函数,则包含在中的底层迭代机制for将StopIteration优雅地处理该异常。
大多数Python文本使用循环语句介绍生成器,类似于以下代码:
In [19]: def countdown_gen(x): ...: count = x ...: while count > 0: ...: yield count ...: count -= 1 ...: In [20]: g = countdown_gen(5)In [21]: for item in g: ...: print(item) ...: 54321
但是我发现这可能导致控制流和控制权的混乱。必须理解的是,在循环内迭代时,生成器不会产生任何值,直到从客户端请求它们为止for。在for循环中,Python 隐式地调用next()了它从生成器对象获得的迭代器。也就是说,在for循环中,Python隐式地这样做:
In [32]: g = countdown_gen(5)In [33]: g_iter = iter(g)In [34]: next(g_iter)Out[34]: 5In [35]: next(g_iter)Out[35]: 4In [36]: next(g_iter)Out[36]: 3In [37]: next(g_iter)Out[37]: 2In [38]: next(g_iter)Out[38]: 1In [39]: next(g_iter)---------------------------------------------------------------------------StopIteration Traceback (most recent call last) in ()----> 1 next(g_iter)StopIteration: In [40]:
当然,next()可以在生成器函数的迭代器上显式调用它,这有助于在Python解释器控制台上手动强制生成器函数进行迭代。
下图也可以帮助解释这些步骤。
这样,与闭包一样,Python的生成器函数在连续调用之间保持状态。或者,如PEP 255所述:
如果遇到yield语句,则函数的状态将被冻结,并且expression_list的值返回给.next()的调用方。“冻结”是指保留所有局部状态,包括局部变量的当前绑定,指令指针和内部评估堆栈:保存了足够的信息,以便下次调用.next()时,该函数可以就像yield语句只是另一个外部调用一样继续进行。
这种状态保留和值的懒散生成很难用这么小的琐碎示例来概念化,因此我尝试通过编码我认为可能有用的生成器函数使其更加具体。
用例— S3
Amazon的S3存储服务提供了一种相当简单且可扩展的方式,可以以非分层结构远程存储数据。关于S3的完整讨论不在本文的讨论范围之内,但是在吸引我探索是否可以在生成器函数中封装一些有用的S3资源访问功能之前,已经对S3进行了一些工作。
该boto3 Python库提供的API调用访问S3会话,资源和文件对象。以前我曾使用过download_file()API调用,但是正如预期的那样,这会将整个远程文件下载到一个人的当前工作目录中。如果您要在EC2实例上的Docker容器中运行Python脚本,那就很好,但是对于我目前的工作,我在MacBook Air上运行脚本,因此我很想找到一种避免使用本地存储的方法,仍然能够访问远程文件。
幸运的是,boto3库允许通过对象API访问文件资源的“流主体”。这似乎是生成器函数的理想候选者,因为文件对象仅应按需流传输(即,延迟)。
当然,可以直接使用这些API调用并直接在文件流上进行迭代。但是我认为包装访问文件流所需的所有S3客房整理可能会更优雅。尽管生成器会在调用之间保留状态,但我的建议是在类内部组合生成器函数,以管理S3会话状态。通过重载__iter__类中的方法,我可以使类可迭代。这样,我可以使我的S3类的行为类似于Python标准库中的文件对象。
我的此类代码如下:
import boto3class S3FileReader: """ class S3FileReader: Class to encapsulate boto3 calls to access an S3 resource and allow clients to stream the contents of the file iteratively, via a generator function: __iter__() """ def __init__(self, cfg, resource_key, bucket=None): """ __init__(self, cfg, bucket, resource_name): S3FileReader constructor initializes the S3 Session, gets the resource for a given bucket and key, obtains the resource's object, and obtains a handle to the object. Params: cfg: config.py file containing S3 crexentials bucket: name of the S3 bucket to access resource_key: key of the S3 resource (file name) """ try: if not bucket: bucket = cfg.bucket self._session = boto3.Session( aws_access_key_id=cfg.aws_access_key_id, aws_secret_access_key=cfg.aws_secret_access_key) self._resource = self._session.resource('s3') self._object = self._resource.Object(bucket, resource_key) self._handle = self._object.get() except Exception: raise S3FileReaderException('Failed to initialize S3 resources!') def __iter__(self): """ __iter__(self): Provide iteration interface to clients. Get the stream of our S3 object handle and produce results lazily for our clients from a generator function. yield statement yields a single line from the file. Returns: nothing. A StopIteration exception is implicitly raised following the completion of the for loop. """ if not self._handle: raise S3FileReaderException('No S3 object handle!') stream = self._handle['Body'] for line in stream: yield line def __enter__(self): """ __enter__(self): Implement Python's context management protocol so this class can be used in a "with" statement. """ return self def __exit__(self, exc_type, exc_value, exc_tb): """ __exit__(self, exc_type, exc)_value, exc_tb): Implement Python's context management protocol so this class can be used in a "with" statement. If exc_type is not None, then we are handling an exception and for safety should delete our resources """ if exc_type is not None: del self._session del self._resource del self._object del self._handle return False else: # normal exit flow return Trueclass S3FileReaderException(Exception): """ class S3FileReaderException(Exception): Simple exception class to use if we can't get an S3 File handle, or otherwise have an exception when dealing with S3. """ def __init(self, msg): self.msg = msg
毫无疑问,此类的代码比其提供的有限功能所必需的更为精细。但是它提供了少量的异常处理,此外,它实现了Python的上下文管理接口,因此可以像标准库的文件对象一样使用该类。这消除了对更多详细的try / except块的需要。该__exit__函数利用了免费对象删除功能-这背叛了我以前C++遵循类析构函数最佳实践的习惯;但它也明确表明了在对象清除时释放所有S3资源的意图-会话,资源和对象。boto3库似乎不支持close()方法。
在构造函数中完成必要的S3内部整理之后,该类提供了一个不错的接口,用于通过该__iter__方法迭代文件的流。客户代码可能希望在流上进行迭代时实现其他处理或逻辑。对他的小类的一个很好的增强将是添加一个过滤谓词,这样,__iter__如果用户知道他们只对数据的一个子集感兴趣,则该方法不必发出大文件的每一行。标准库itertools.dropwhile功能的使用在这里可以很好地工作。
主要好处是S3FileReader类的客户端不必担心S3的内部维护和维护,只需要表示感兴趣的资源即可。即使该类在文件流上进行迭代以在__iter__方法中产生行,但是由类的客户端控制迭代和数据的产生。
结论
Python生成器函数在标准库中得到了广泛使用,并为程序员提供了用于推迟计算,节省时间和空间的强大工具。