Python 生成器

Posted onetoinf

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 生成器相关的知识,希望对你有一定的参考价值。

知道迭代器之后,就可以正式进入生成器的话题了。普通函数用 return 返回一个值,和 Java 等其他语言是一样的,然而在 Python 中还有一种函数,用关键字 yield 来返回值,这种函数叫生成器函数,函数被调用时会返回一个生成器对象,生成器本质上还是一个迭代器,也是用在迭代操作中,因此它有和迭代器一样的特性,唯一的区别在于实现方式上不一样,后者更加简洁

>>> def gen(n):
...     yield n**2
... 
>>> from collections import Iterator
>>> g = gen(10)
>>> type(g)
<class 'generator'>
>>> isinstance(g, Iterator)
True
  1. 语法上和函数类似:生成器函数和常规函数几乎是一样的。它们都是使用def语句进行定义,差别在于,生成器使用yield语句返回一个值,而常规函数使用return语句返回一个值
  2. 生成器本质上还是一个迭代器
  3. 状态挂起:生成器使用yield语句返回一个值。yield语句挂起该生成器函数的状态,保留足够的信息,以便之后从它离开的地方继续执行

Python有两种不同的方式提供生成器:

  • 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
  • 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

最简单的生成器函数:

生成器函数

xrange不是生成器

函数返回的是一个xrange obj。xrange类型是一种不可变的序列,通常用于循环。xrange类型的优点是,xrange对象总是使用相同数量的内存,不管它所代表的范围大小。所以xrange比range更好。(Python3中变为range, xrange也变为range对象了。)

In [28]: I = xrange(10)         
                                
In [29]: isinstance(I, Iterable)
Out[29]: True                   
                                
In [30]: isinstance(I, Iterator)
Out[30]: False                  

自定义生成器

以生成斐波那契数列序列为例

  • 不使用生成器
def fib(n):
    pre, cur = 0, 1
    result = []
    if n == 0:
        return result
    elif n == 1:
        return [1]
    elif n > 1:
        result.append(cur)
        for i in range(n - 1):
            pre, cur = cur, pre + cur
            result.append(cur)
        return result


if __name__ == '__main__':

    for i in fib(10):
        print(i, end=' ')
    print()
  • 使用生成器
def fib(n):
    prev, curr = 0, 1
    while n > 0:
        n -= 1
        yield curr
        prev, curr = curr, curr + prev


if __name__ == '__main__':

    for i in fib(10):
        print(i, end=' ')
    print()

运行结果都一样:

1 1 2 3 5 8 13 21 34 55 
[Finished in 0.1s]

很显然使用生成器比不使用更加简洁。

生成器表达式

生成器表达式与列表推导式长的非常像,但是它俩返回的对象不一样,前者返回生成器对象,后者返回列表对象。

>>> g = (x*2 for x in range(10))
>>> type(g)
<type 'generator'>
>>> l = [x*2 for x in range(10)]
>>> type(l)
<type 'list'>

这样并不能看出什么,在Ipython中测试一下性能

In [33]: %timeit (x*2 for x in range(1000000))
100 loops, best of 3: 16.5 ms per loop

In [34]: %timeit [x*2 for x in range(1000000)]
10 loops, best of 3: 98.5 ms per loop

可以看到生成器表达式更为高效!

生成器支持的方法

>>> help(fib(10))

fib = class generator(object)
 |  Methods defined here:
 |  
 |  __del__(...)
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __next__(self, /)
 |      Implement next(self).
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  close(...)
 |      close() -> raise GeneratorExit inside generator.
 |  
 |  send(...)
 |      send(arg) -> send 'arg' into generator,
 |      return next yielded value or raise StopIteration.
 |  
 |  throw(...)
 |      throw(typ[,val[,tb]]) -> raise exception in generator,
 |      return next yielded value or raise StopIteration.
 |  
 |  ----------------------------------------------------------------------

close

手动关闭生成器函数,后面的调用会直接返回StopIteration异常。

>>> def g4():
...     yield 1
...     yield 2
...     yield 3
...
>>> g=g4()
>>> next(g)
1
>>> g.close()
>>> next(g)    #关闭后,yield 2和yield 3语句将不再起作用
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

send

生成器函数最大的特点是可以接受外部传入的一个变量,并根据变量内容计算结果后返回。
这是生成器函数最难理解的地方,也是最重要的地方,实现后面我会讲到的协程就全靠它了。

def gen():
    value=0
    while True:
        receive=yield value
        if receive=='e':
            break
        value = 'got: %s' % receive
 
g=gen()
print(g.send(None))     
print(g.send('aaa'))
print(g.send(3))
print(g.send('e'))

执行流程:

  1. 通过g.send(None)或者next(g)可以启动生成器函数,并执行到第一个yield语句结束的位置。此时,执行完了yield语句,但是没有给receive赋值。yield value会输出初始值0注意:在启动生成器函数时只能send(None),如果试图输入其它的值都会得到错误提示信息。
  2. 通过g.send(‘aaa’),会传入aaa,并赋值给receive,然后计算出value的值,并回到while头部,执行yield value语句有停止。此时yield value会输出”got: aaa”,然后挂起。
  3. 通过g.send(3),会重复第2步,最后输出结果为”got: 3″
  4. 当我们g.send(‘e’)时,程序会执行break然后推出循环,最后整个函数执行完毕,所以会得到StopIteration异常。

最后的执行结果如下:

0
got: aaa
got: 3
Traceback (most recent call last):
File "h.py", line 14, in <module>
  print(g.send('e'))
StopIteration

throw

用来向生成器函数送入一个异常,可以结束系统定义的异常,或者自定义的异常。
throw()后直接跑出异常并结束程序,或者消耗掉一个yield,或者在没有下一个yield的时候直接进行到程序的结尾。

def gen():
    while True: 
        try:
            yield 'normal value'
            yield 'normal value 2'
            print('here')
        except ValueError:
            print('we got ValueError here')
        except TypeError:
            break
 
g=gen()
print(next(g))
print(g.throw(ValueError))
print(next(g))
print(g.throw(TypeError))

输出结果为:

normal value
we got ValueError here
normal value
normal value 2
Traceback (most recent call last):
  File "h.py", line 15, in <module>
    print(g.throw(TypeError))
StopIteration

生成器挂起

yield在生成一个值之后会扶起当前线程

利用多个CPU只有两种方法

  • 利用多个进程分享CPU
  • 一个进程,创建多个进程

Python支持吗?

  • Python支持
  • 但是并不是真正的支持

GIL锁:Python由于众所周知的GIL的原因,导致其线程无法发挥多核的并行计算能力(当然,后来有了multiprocessing,可以实现多进程并行),显得比较鸡肋。既然在GIL之下,同一时刻只能有一个线程在运行,那么对于CPU密集的程序来说,线程之间的切换开销就成了拖累,而以I/O为瓶颈的程序正是协程所擅长的:

协程(脚本语言的开发者发明的)

  • 模拟并行
  • 消息机制

协程比多线程快的原因是:线程切换耗费时间;

生成器:

# encoding:utf-8


def generator():
    m = []
    while True:
        # 返回m值,并挂起函数,暂停执行,此时赋值语句尚未完成
        x = yield m
        m.append(x)


if __name__ == '__main__':
    g = generator()
    print(g)
    print(g.send(None))
    print(g.send(1))
    print(g.send(2))

运行结果:

<generator object generator at 0x00000000025CBE10>
[]
[1]
[1, 2]
[Finished in 0.1s]

消费者生产者模型:

# encoding:utf-8


'''消费者生产者模型

生产者 -- 生产东西 -- 主程 -- 不能写死循环
消费者 -- 消费东西 -- 协程 -- 往往写死循环
'''


def producer(c):
    c.send(None)
    n = 0
    while n < 5:
        n += 1
        print('生产者,生产了... %s' % n)
        r = c.send(n)
        print('生产者, 收到了回馈... %s' % r)
    c.close()


def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        else:
            print('消费者,消费了... %s' % n)


if __name__ == '__main__':
    g = generator()
    print(g)
    print(g.send(None))
    print(g.send(1))
    print(g.send(2))
    # c = consumer()
    # producer(c)

运行结果:

生产者,生产了... 1
消费者,消费了... 1
生产者, 收到了回馈... 
生产者,生产了... 2
消费者,消费了... 2
生产者, 收到了回馈... 
生产者,生产了... 3
消费者,消费了... 3
生产者, 收到了回馈... 
生产者,生产了... 4
消费者,消费了... 4
生产者, 收到了回馈... 
生产者,生产了... 5
消费者,消费了... 5
生产者, 收到了回馈... 
[Finished in 0.1s]

总结

  1. 按照鸭子模型理论,生成器就是一种迭代器,可以使用for进行迭代。
  2. 第一次执行next(generator)时,会执行完yield语句后程序进行挂起,所有的参数和状态会进行保存。再一次执行next(generator)时,会从挂起的状态开始往后执行。在遇到程序的结尾或者遇到StopIteration时,循环结束。
  3. 可以通过generator.send(arg)来传入参数,这是协程模型。
  4. 可以通过generator.throw(exception)来传入一个异常。throw语句会消耗掉一个yield。可以通过generator.close()来手动关闭生成器。
  5. next()等价于send(None)

以上是关于Python 生成器的主要内容,如果未能解决你的问题,请参考以下文章

postman 自动生成 curl 代码片段

Apollo Codegen 没有找到生成代码的操作或片段

前端开发工具vscode如何快速生成代码片段

前端开发工具vscode如何快速生成代码片段

vscode代码片段生成vue模板

常用python日期日志获取内容循环的代码片段