「Python 系列」 Python 生成器函数详解


「Python 系列」 Python 生成器函数详解


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]: g
Out[10]: <generator>

In [11]: next(g)
Out[11]: 10

In [12]: g = gen(10)

In [13]: g
Out[13]: <generator>

In [14]: next(g)
Out[14]: 10

In [15]: next(g)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-15-e734f8aca5ac> in <module>()
----> 1 next(g)

StopIteration:

In [16]: /<module>/<ipython-input-15-e734f8aca5ac>/<generator>/<generator>

该函数除了“屈服”作为参数传递的值外,什么也不做。但是,仅像“正常”函数那样调用函数不会产生返回值。如果可能,将使用一个参数实例化生成器函数,并将其保存在变量中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)
...:
5
4
3
2
1

但是我发现这可能导致控制流和控制权的混乱。必须理解的是,在循环内迭代时,生成器不会产生任何值,直到从客户端请求它们为止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]: 5

In [35]: next(g_iter)
Out[35]: 4

In [36]: next(g_iter)
Out[36]: 3

In [37]: next(g_iter)
Out[37]: 2

In [38]: next(g_iter)
Out[38]: 1

In [39]: next(g_iter)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-39-fe4ec6cc82e2> in <module>()
----> 1 next(g_iter)

StopIteration:

In [40]: /<module>/<ipython-input-39-fe4ec6cc82e2>

当然,next()可以在生成器函数的迭代器上显式调用它,这有助于在Python解释器控制台上手动强制生成器函数进行迭代。

下图也可以帮助解释这些步骤。


「Python 系列」 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 boto3


class 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 True


class 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生成器函数在标准库中得到了广泛使用,并为程序员提供了用于推迟计算,节省时间和空间的强大工具。


分享到:


相關文章: