python基础(生成器)

生成器是一种使用普通函数语法定义的迭代器。生成器创建起来与函数一样简单。下面创建一个将嵌套列表展开的函数。这个函数将一个类似于下面的列表作为参数:
nested = [[1, 2], [3, 4], [5]] 
换而言之,这是一个列表的列表。函数应按顺序提供这些数字,下面是一种解决方案:

def flatten(nested): 
   for sublist in nested: 
       for element in sublist: 
           yield element 
这个函数的大部分代码都很简单。它首先迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表的元素。倘若最后一行为print(element),这个函数将容易理解得多,不是吗?
在这里,你没有见过的是yield语句。包含yield语句的函数都被称为生成器。这可不仅仅是名称上的差别,生成器的行为与普通函数截然不同。差别在于,生成器不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。
为使用所有的值,可对生成器进行迭代。
>>> nested = [[1, 2], [3, 4], [5]] 
>>> for num in flatten(nested): 
...        print(num) 
... 






>>> list(flatten(nested)) 
[1, 2, 3, 4, 5]

 

简单生成器
在Python 2.4中,引入了一个类似于列表推导的概念:生成器推导(也叫生成器表达式)。其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而是返回一个生成器,让你能够逐步执行计算。
>>> g = ((i + 2) ** 2 for i in range(2, 27)) 
>>> next(g) 
16 
如你所见,不同于列表推导,这里使用的是圆括号。在像这样的简单情形下,还不如使用列表推导;但如果要包装可迭代对象(可能生成大量的值),使用列表推导将立即实例化一个列表,从而丧失迭代的优势。
另一个好处是,直接在一对既有的圆括号内(如在函数调用中)使用生成器推导时,无需再添加一对圆括号。换而言之,可编写下面这样非常漂亮的代码:
sum(i ** 2 for i in range(10)) 

 

递归式生成器
前一节设计的生成器只能处理两层的嵌套列表,这是使用两个for循环来实现的。如果要处理任意层嵌套的列表,该如何办呢?例如,你可能使用这样的列表来表示树结构(也可以使用特定的树类,但策略是相同的)。对于每层嵌套,都需要一个for循环,但由于不知道有多少层嵌套,你必须修改解决方案,使其更灵活。该求助于递归了。
def flatten(nested): 
    try: 
        for sublist in nested: 
            for element in flatten(sublist): 
                yield element 
    except TypeError: 
        yield nested 
调用flatten时,有两种可能性(处理递归时都如此):基线条件和递归条件。在基线条件下,要求这个函数展开单个元素(如一个数)。在这种情况下,for循环将引发TypeError异常(因为你试图迭代一个数),而这个生成器只生成一个元素。
然而,如果要展开的是一个列表(或其他任何可迭代对象),你就需要做些工作:遍历所有的子列表(其中有些可能并不是列表)并对它们调用flatten,然后使用另一个for循环生成展开后的子列表中的所有元素。这可能看起来有点不可思议,但确实可行。
>>> list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8])) 
[1, 2, 3, 4, 5, 6, 7, 8] 
然而,这个解决方案存在一个问题。如果nested是字符串或类似于字符串的对象,它就属于序列,因此不会引发TypeError异常,可你并不想对其进行迭代。

在函数flatten中,不应该对类似于字符串的对象进行迭代,主要原因有两个。首先,你想将类似于字符串的对象视为原子值,而不是应该展开的序列。其次,对这样的对象进行迭代会导致无穷递归,因为字符串的第一个元素是一个长度为1的字符串,而长度为1的字符串的第一个元素是字符串本身!

要处理这种问题,必须在生成器开头进行检查。要检查对象是否类似于字符串,最简单、最快捷的方式是,尝试将对象与一个字符串拼接起来,并检查这是否会引发TypeError异常。添加这种检查后的生成器如下:
def flatten(nested): 
    try: 
        # 不迭代类似于字符串的对象:
        try: nested + '' 
        except TypeError: pass 
        else: raise TypeError 
        for sublist in nested:

            for element in flatten(sublist): 
                yield element 
     except TypeError: 
         yield nested
如你所见,如果表达式nested + ''引发了TypeError异常,就忽略这种异常;如果没有引发TypeError异常,内部try语句中的else子句将引发TypeError异常,这样将在外部的excpet子句中原封不动地生成类似于字符串的对象。明白了吗?
下面的示例表明,这个版本也可用于字符串:
>>> list(flatten(['foo', ['bar', ['baz']]])) 
['foo', 'bar', 'baz']
请注意,这里没有执行类型检查:我没有检查nested是否是字符串,而只是检查其行为是否类似于字符串,即能否与字符串拼接。对于这种检查,一种更自然的替代方案是,使用isinstance以及字符串和类似于字符串的对象的一些抽象超类,但遗憾的是没有这样的标准类。另外,即便是对UserString来说,也无法检查其类型是否为str。

 

通用生成器
如果你按前面的例子做了,就差不多知道了如何使用生成器。你知道,生成器是包含关键字yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求值时,都将执行生成器的代码,直到遇到yield或return。yield意味着应生成一个值,而return意味着生成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。
换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话说,这两个实体通常被视为一个,通称为生成器。
>>> def simple_generator(): 
            yield 1 
... 
>>> simple_generator 
<function simple_generator at 153b44> 
>>> simple_generator() 
<generator object at 1510b0> 
对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。

 

生成器的方法
在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。这个通信渠道包含如下两个端点。

  • 外部世界:外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要发送的“消息”,可以是任何对象)。
  • 生成器:在挂起的生成器内部,yield可能用作表达式而不是语句。换而言之,当生成器重新运行时,yield返回一个值——通过send从外部世界发送的值。如果使用的是next,yield将返回None。

请注意,仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。
要在此之前向生成器提供信息,可使用生成器的函数的参数。

如果一定要在生成器刚启动时对其调用方法send,可向它传递参数None。

下面的示例很傻,但说明了这种机制:
def repeater(value): 
    while True: 
        new = (yield value) 
        if new is not None: value = new 
下面使用了这个生成器:
>>> r = repeater(42) 
>>> next(r) 
42 
>>> r.send("Hello, world!") 
"Hello, world!" 
注意到使用圆括号将yield表达式括起来了。在有些情况下,并非必须这样做,但小心驶得万年船。如果要以某种方式使用返回值,就不管三七二十一,将其用圆括号括起吧。


生成器还包含另外两个方法。
方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个traceback对象。
方法close:用于停止生成器,调用时无需提供任何参数。
方 法close( 由Python垃圾收集器在需要时调用)也是基于异常的:在yield处引发GeneratorExit异常。因此如果要在生成器中提供一些清理代码,可将yield放在一条try/finally语句中。如果愿意,也可捕获GeneratorExit异常,但随后必须重新引发它(可能在清理后)、引发其他异常或直接返回。对生成器调用close后,再试图从它那里获取值将导致RuntimeError异常。

补充

生成器有一个send方法,是用来给yield传递数据

send和next都可以触发生成器,send传递数据,必须执行到yield这个方法的地方才能接收数据

所以第一次send只能传递个None,目的是触发生成器,下一次send发送数据,yield就会接收到数据

def go():

    print(1)

    yield 'a'

    print(2)

    yield 'b'

    print(3)

    yield 'c'

g = go()

print(next(g)) 输出1 'a'

print(next(g)) 输出2 'b'

print(next(g)) 输出3 'c'

 

模拟生成器
如果你使用的是较老的Python版本,就无法使用生成器。下面是一个简单的解决方案,让你能够使用普通函数模拟生成器。
首先,在函数体开头插入如下一行代码:
result = [] 

如果代码已使用名称result,应改用其他名称。(在任何情况下,使用更具描述性的名称都是不错的主意。)接下来,将类似于yield some_expression的代码行替换为如下代码行:
yield some_expression with this: 
result.append(some_expression) 
最后,在函数末尾添加如下代码行:
return result 
尽管使用这种方法并不能模拟所有的生成器,但可模拟大部分生成器。例如,这无法模拟无穷生成器,因为显然不能将这种生成器的值都存储到一个列表中。
下面使用普通函数重写了生成器flatten:
def flatten(nested): 
     result = [] 
     try: 
          # 不迭代类似于字符串的对象:
          try: nested + '' 
          except TypeError: pass 
          else: raise TypeError 
          for sublist in nested: 
              for element in flatten(sublist): 
                   result.append(element) 
     except TypeError: 
              result.append(nested) 
     return result

print(flatten((['foo',16 ,True,['bar',25, ['baz',18]]])))

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值