Python生成器函数概述:运用实例分解说明机制

780 阅读9分钟

全文共5930字,预计学习时长12分钟

Python的生成器函数为数据和计算资源管理提供了强大的机制,但对于Python初学者而言,理解它们并非易事。这篇文章会分解生成器的机制,同时介绍一个管理和分类S3文件资源的小类的例子,希望能对你有所启发。

鉴于Python入门并不难,非常容易就可写出真正能够运作的代码(比如迭代一列数值,计算以及/或打印一些数值),一些Python初学者和粗心的程序员可能没有意识到该语言建立在procrastination,也即延迟计算的概念之上。对使用过编译语言(如C++)的人而言,这种根植于该语言本身的松散性,或者说惰性,可能有点陌生。

很多程序员都学过“惰性计算”以及如何写代码来实现这一操作。但Python语言本身就支持这种计算(只需一个关键词就可轻易实现),这种有效性和表达性在其他程序语言中非常罕见。所以,惰性计算这个概念被引入“拉达姆演算”,而Python尽管并非专门的功能语言(例如Lisp),也体现出功能编程的特性,也就不足为奇了,Python使用闭包函数也是拉达姆演算特性的一部分。

2001年,“PEP 255 — Simple Generators”(https://www.python.org/dev/peps/pep-0255/)介绍了生成器,提出动机是对惰性计算更加直接的表达:

当一个生产函数遇到需要保持在产出值之间的状态,面对这一难题,很多程序语言无法提供令人满意的有效解决方案……

机制

Python生成器函数是一个很强大的概念,但不同于函数装饰器(function decorators)复杂的构架,它们运行和表达机制相当简单,只需“yield”语句(yield这一新的关键词在PEP 255中被加入Python)。

作为及物动词,yield表示产出。作为不及物动词,它表示让步或撤回。这个单词的两种含义都会在Python生成器函数中出现。

人们通常认为,函数在返回单个值、以列表或词库形式返回多个值、或用户定义的对象时,会通过返回语句产生结果。返回语句是函数结束控制并将控制和结果让渡给调用者的方式。返回语句后,运行环境(解释器)将给定函数的堆栈帧从调用堆栈中移除,给定函数的“环境”就会消失(直到下一次调用该函数)

Python的yield语句则完全改变了上述操作。下面来看看一个非常简单的人为设计的生成器例子,附有额外代码以证明它的效用(代码来自iPython解释器交互对话):

In [8]: def gen(x):
   ...:     yield x

In [9]: g = gen(10)
In [10]: g
Out[10]: <generator object gen at 0x10d26ed00>

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

In [12]: g = gen(10)

In [13]: g
Out[13]: <generator object gen at 0x10d41d1a8>

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]:

这个函数只会“产出”作为参数传递的值。但是,仅像“普通”函数那样调用该函数不会产生返回值。生成器函数会通过参数实例化并存于变量g之中。那么,当 next()明确调用生产器对象时,生产器就必须进行迭代以产出值。而且,一旦产出(单个)值,生产器就会停止运作,此时继续调用next()会导致 “StopIteration”异常。但如果在for循环中迭代生成器函数,for中包含的底层迭代机制就会巧妙地处理StopIteration异常。

很多Python文本都会通过循环语句引入generators,如以下代码:

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: 

当然,在生成器的迭代器上也可以显式地调用next(),并且在Python解释器控制台中,手动强制迭代生成器是有帮助的。

下面的图表或许有助于解释这些步骤。

通过这种方式,与使用闭包函数一样,Python生成器函数在连续调用期间保持相同状态。或者,正如PEP 255所说:

如果遇到yield语句,函数的状态将被冻结,expression_list的值将返回给.next()的调用者。“冻结”意味着保留所有本地状态,包括当前的局部变量绑定、指令指针和内部评价栈。保存足够的信息,这样下次调用. next()时,该函数就可以直接开始运作,这时yield语句就好比是另一个外部调用。

上述的状态保留和惰性产出值很难用这样一个小而琐碎的例子解释清楚,所以本文尝试通过编写一个可能有用的生成器函数做出更加具体的解释。

例子讲解——S3

Amazon的S3存储服务提供了一种相当简单且可延展的方法,可在非层级结构中远程存储数据。本文不会全面讨论S3, 但会进行简单介绍,然后再探讨是否可以将一些有用的S3资源访问功能封装到生成器函数中。

boto3 Python库提供了访问S3会话、资源和文件对象的API调用。此前笔者使用了download_file() API调用,但正如预计的那样,它会将整个远程文件下载到当前工作目录中。如果在Docker运容器和EC2实例上运行Python脚本,这种方式是没问题的。但要在MacBook Air上运行脚本,就得找到一种方法避免使用本地存储,同时仍然能够访问远程文件。

幸运的是,boto3库允许通过Object 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生成器函数广泛应用于标准库中,为程序员提供了一个强大的延迟计算工具,节省了时间和空间。

留言 点赞 关注

我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”


(添加小编微信:dxsxbb,加入读者圈,一起讨论最新鲜的人工智能科技哦~)