好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

20210330 迭代器与生成器

1-1

列表生成式

>>> a = [1,2,3]

>>> a

[1, 2, 3]

>>>

>>> [i*2 for i in  range (10)]

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# for i in range(10) 这是一个循环,循环 10 次

# i*2 中的 i 就是 for 循环中的 i

# 把 for 循环中的临时变量 i 赋值到前面的 i*2 的 i 中,得到的结果,就是列表中的元素

# 这个列表 和 列表 a 的区别是:列表 a 是写死的,下面的 i*2 是可以进行操作的

 

1-1-2

# [i*2 for i in range(10)] 相当于

>>> a = []

>>> for i in  range (10):

...     a.append(i*2)

...

>>> a

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# 相当于至少 3 行代码

# [i*2 for i in range(10)] 就叫 列表生成式

列表生成式的主要作用就是使代码更简洁

 

1-1-3

当然列表生成式也可以传函数,比如

>>> [func(i) for i in  range (10)]  

 

1.迭代器&生成器 

生成器 

通过列表生成式,我们可以直接创理一个列表。但是,受到内存限制,列表容量肯定是有限的。

而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们识仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。 

 

所以, 如果列表元素可以按照某种算法推算出来

那我们是否可以在循环的过程中不断推算出后续的元素呢?

这样就不必创建完整的list,从而节省大量的空间。

在Python中,这种一边循环一边计算的机制,称为生成器:generator。 

 

要创建一个generator, 有很多种方法。

第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator;

 

# 在 python2 上 range(10) 数据已经准备好了

# 在 python3 上 range(10) 是没有准备好的

 

# 现在希望在循环的时候,循环到 列表的 第 4 个元素时,这个元素才刚生成;

# 这样,就不用把所有数据都提前准备好了,就可以节省空间

 

2-1

>>> [i*2 for i in  range (10)]

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

>>> (i*2 for i in  range (10))

<generator object <genexpr> at 0x0000027B0E6C3BC8>

# 这就变成了一个生成器,对这个数据进行循环时,每循环一次就按此规律乘 2

 

2-1-2

>>> b = (i*2 for i in  range (10))

>>> for i in b:

...      print (i)

...

0

2

4

6

8

10

12

14

16

18

>>>                 

# 这里的数据量比较少,所以也许无法看出差异;但如果数据量达到100个以上,这种方法可以马上返回数据

# 这时,如果继续用以往的形式,就会感觉到慢了

 

2-1-3

>>> c = ( i*2 for i in  range (100000000))

>>> c

<generator object <genexpr> at 0x0000027B0E6F72C8>

>>>   

# 事实上,这里并没有生成数据,只是返回了一个地址,除非访问这个地址,才会生成数据

# 如果不访问,这个数据就不会存在;

# 而列表则是把所有数据都准备好的;而生成器则是准备了一个算法,算法写好了一个规则,调用时,才会产生数据

 

2-1-4

# 现在想访问 c 的第 1000 个数据,前面的 999 个数据要被循环到,才能够调用;否则无法直接一下调用第 1000 个

# 所以它不支持列表的那种切片

>>> c[1000]

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: 'generator' object is not subscriptable

 

2-1-5

# 既然生成器不支持切片,那生成器能够支持什么呢?

# 除了用 for 循环一个一个的取,还支持别的方式吗?

# 没有,只能用 for 循环这一种方式获取

 

2-1-6

# 如果只访问前两个数据,应该怎么做呢?

# 生成器,有一个方法,叫做 __next__() 方法

>>> c = ( i*2 for i in  range (100000000))

>>> for i in c:

...      print (i)

...

0

2

4

……

2210

2212

Traceback (most recent call last):

  File "<stdin>", line 2, in <module>

KeyboardInterrupt

>>> c. __next__ ()

2214          

>>> c. __next__ ()

2216

>>> c. __next__ ()

2218

>>> c. __next__ ()

2220

>>>               

# 现在要回去取 2210,是不可以的,回去取是不行的;而且也没有任何相应的方法让你回到上一个

# 生成器只记住当前位置,它不知道前面,也不知道后面;前面用完了,就没了

# 所以既不能往前走,也不能往后退;只能一点一点的往后走

 

1 生成器 只有在调用时才会生成相应的数据

2 只记录当前位置

3 只有一个  __next__ ()方法。(在 python2.7 中是  next ())

 

所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心 

StopIteration的错误: 

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。 

比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到: 

1,1,2,3,5,8,13,21,34,... 

 

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

       print (b)

      a, b = b, a + b

      n = n + 1

   return 'done'

注意,赋值语句:a, b = b, a + b

 

相当于:

t = (b, a + b)    # t 是一个 tuple

a = t[0]

b = t[1]

 

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第 

一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似 generator 

也就是说,上面的函数和generator仅一步之遥。                    

要把 fib 函数变成 generator,只需要把  print (b) 改为 yield b 就可以了: 

 

def  fib (max) : 

   n,a,b=0,0,1 

 

   while n< max : 

      #print(b) 

      yield b 

      a,b=b,a+b 

 

      n += 1 

 

   return'done' 

 

3-1

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

       print (b)

      a, b = b, a + b

      n = n + 1

   return 'done'

 

fib(10)     # 10 就表示从 1 开始,生成 10 个 斐波那契数列

--->

1

1

2

3

5

8

13

21

34

55

 

3-1-2

>>> a,b = 1,2

>>> a

1

>>> b

2

>>> t = (b,a+b)

>>> t

(2, 3)

>>>               

 

3-1-3

# 一个微妙之处

>>> foo = 'Monty'

>>> bar = foo

>>> foo = 'Python'

>>> bar

'Monty'

>>>           

这个结果与预期的完全一样。当在代码中写入bar=foo时,foo的值(字符串'Monty')被赋值给bar。也就是说,bar是foo的一个副本,所以当第2行用一个新 

的字符串'Python'覆盖foo时,bar的值不会受到影响。 

 

3-1-4-1

>>> a=0

>>> b=1

>>> a,b=b,a+b

>>> a

1

>>> b

1

 

3-1-4-2

>>> c=0

>>> d=1

>>> c=d

>>> d=c+d

>>> c

1

>>> d

2

 

3-2

# 现在把这个 fib 函数,变成生成器

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   return 'done'

 

print (fib(100))

--->

<generator object fib at 0x0000018C3144A7C8>

# 就变成生成器了

 

3-2-1

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   # return 'done'

 

f = fib(100)

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

--->

1

1

2

# 之前一个函数一旦调用,就需要等待执行结束,才可以继续执行后面的内容

# 现在用生成器,就可以在 函数没有完全执行完的过程中 “出出进进”

 

3-2-2

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   # return 'done'

 

f = fib(100)

print (f. __next__ ())

print ("===1===")

print (f. __next__ ())

print ("===2===")

print (f. __next__ ())

 

print ("====start loop")

for i in f:

    print (i)

--->

1

===1===       

1

===2===       

2

====start loop

3

5

8

…… ……

# 这样就不需要等函数执行完毕了,而是可以让函数停在一个地方;可以中断函数去做别的事情,然后在回来执行函数

 

3-3

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   return 'done'

 

f = fib(10)

print (f. __next__ ())

print ("=======")

print (f. __next__ ())

 

print ("====start loop")

for i in f:

    print (i)

--->

1

=======

1

====start loop

2

3

5

8

13

21

34

55

# 可以看出,done 并没有被打印;使用 for 循环,done 就不会被打印

 

3-3-1

# 那么,如果不用 for 循环呢?这里,有 10 组数,打印 11 个,让它报错,会发生什么呢?

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   return 'done'

 

f = fib(10)

print (f. __next__ ())

print ("=======")

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

print (f. __next__ ())

--->

1

=======

1

2

3

5

8

13

21

34

55

Traceback (most recent call last):

  File "c:/Users/Majesty/Desktop/2.py", line 22, in <module>

     print (f. __next__ ())

StopIteration: done

# 抛出了 一个包含 done 的异常

 

3-3-2

用for循环调用generator时, 发现拿不到generator的return语句的返回值。 

如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中: 

 

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   return 'done'

 

g=fib(6) 

 

while True:

   try:

      x= next (g) 

       print ('g:', x)

   except StopIteration as e:

       print ('Generator returnvalue:', e.value) 

      break 

--->

g: 1

g: 1

g: 2

g: 3

g: 5

g: 8

Generator returnvalue: done

 

# 这就是 return 在生成器中的作用;只要有 yield 它就是一个生成器,就不叫函数了

 

# 单线程就是串行执行

# 通过生成器可以实现并行效果

def  fib (max):

   n,a,b = 0,0,1

   while n <  max :

      # print(b)

      yield b

      a, b = b, a + b

      n = n + 1

   return 'done'

# 想把谁返回到外面,就把 yield 放到哪

# 只要执行到 yield b,程序就会中断,函数中断后,就会回到外面

# 回到外面后,通过 next(g) 就又回到了函数里

# 所以 yield 是保存了 函数的中断状态,返回当前状态的值

 

g=fib(6) 

 

while True:

   try:

      x= next (g)    # next()也是一个内置方法,和 __next__()是一样的

       print ('g:', x)

   except StopIteration as e:

       print ('Generator returnvalue:', e.value) 

      break 

 

1-1

生成器还可通过 yield 实现在单线程的情况下实现并发运算的效果

#_*_coding:utf-8_*_ 

__author__='Alex Li' 

 

import time 

def  consumer (name) : 

    print ("%s 准备吃包子啦!" %name)

   while True: 

      baozi = yield 

      # 这里 yield 没有返回值,没有返回值,结果就是空

 

       print ("包子[%s]来了,被[%s]吃了!" %(baozi,name))

 

def  producer (name) : 

   c=consumer('A') 

   c2=consumer('B')

        c. __next__ () 

        c2. __next__ ()

         print ("老子开始准备做包子啦!") 

        for i in  range (10) : 

                time.sleep(1) 

                 print ("做了2个包子!") 

                c.send(i) 

                c2.send(i) 

 

producer("alex") 

# 这是一个典型的生产者,消费者模型

 

1-1-2

import time 

def  consumer (name):

    print ("%s 准备吃包子啦!" %name)

   while True:

      baozi = yield 

 

       print ("包子[%s]来了,被[%s]吃了!" %(baozi,name))

 

c = consumer("ChenRonghua")   # 这样就变成一个生成器了

c. __next__ ()

--->

ChenRonghua 准备吃包子啦!

 

1-1-3

import time 

def  consumer (name):

    print ("%s 准备吃包子啦!" %name)

   while True:

      baozi = yield 

 

       print ("包子[%s]来了,被[%s]吃了!" %(baozi,name))

 

c = consumer("ChenRonghua")   # 这样就变成一个生成器了

c. __next__ ()

c. __next__ ()

--->

ChenRonghua 准备吃包子啦!

包子[None]来了,被[ChenRonghua]吃了!

# None 代表空,可是没有包子吃个毛线? 所以,可以这样

 

1-1-4

import time 

def  consumer (name):

    print ("%s 准备吃包子啦!" %name)

   while True:

      baozi = yield 

 

       print ("包子[%s]来了,被[%s]吃了!" %(baozi,name))

 

c = consumer("ChenRonghua")   # 这样就变成一个生成器了

c. __next__ ()

 

b1 = "韭菜馅"

c.send(b1)     

# 把 b1 发送过去,通过 send 函数,包子就被传进去了;传进去后,被 yield 接收到了

# 接收到后,将值付给了 baozi

# yield 的作用是保存当前状态,然后返回,返回后调用 next,返回 yield;但是 调用 next 并不会传值

# 但是 send 可以传值;

# next 和 send 的区别:1. next 只是再次调用 yield,但是不会传值; 2. send 调用 yield 的同时给 yield 传值

--->

ChenRonghua 准备吃包子啦!

包子[韭菜馅]来了,被[ChenRonghua]吃了!

 

1-2-1

# 现在要边做边吃,看一个并行的效果

import time 

def  consumer (name):

    print ("%s 准备吃包子啦!" %name)

   while True:

      baozi = yield 

 

       print ("包子[%s]来了,被[%s]吃了!" %(baozi,name))

 

c = consumer("ChenRonghua")   # 这样就变成一个生成器了

c. __next__ ()

 

b1 = "韭菜馅"

c.send(b1)    

 

def  producer (name):     # producer 就是一个生产包子的

   c = consumer('A')    # c 和 c2 相当于 两个消费者

   c2 = consumer('B')

   c. __next__ () 

   c2. __next__ ()

    print ("老子开始准备做包子啦!") 

   for i in  range (4): 

      time.sleep(1) 

       print ("做了2个包子!")  # 没一秒钟做两个包子

      c.send(i) 

      c2.send(i)             # 分别把 包子 送给 c 和 c2 两个消费者

 

producer("alex") 

--->

ChenRonghua 准备吃包子啦!

包子[韭菜馅]来了,被[ChenRonghua]吃了!

A 准备吃包子啦!

B 准备吃包子啦!

老子开始准备做包子啦!

做了2个包子!

包子[0]来了,被[A]吃了!

包子[0]来了,被[B]吃了!

做了2个包子!

包子[1]来了,被[A]吃了!

包子[1]来了,被[B]吃了!

做了2个包子!

包子[2]来了,被[A]吃了!

包子[2]来了,被[B]吃了!

做了2个包子!

包子[3]来了,被[A]吃了!

包子[3]来了,被[B]吃了!

# 这就是单线程下的并行效果;虽然还是依次执行的,但可以在不同角色之间来回切换;看起来就好像是并行的

# 这个就是 异步 IO 的雏形,nginx的效率高,就因为它是异步,底层的原理用的就是这个

# nginx在单线程下,承载的并发量,比多线程还要快;本质上就是通过这样的效果实现的

 

这种情况,不把它叫做 yield,把它叫做 协程;协程是比线程更小的单位;这个可以理解为是最简单的协程

 

# 前面生成器就彻底结束了

迭代器 

 

我们已经知道, 可以直接作用于for循环的数据类型有以下几种: 

一类是集合数据类型, 如list、tuple、dict、set、str等; 

一类是generator(数据结构),包括生成器和带yield的generator function. 

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。 (可迭代对象可以简单理解为可循环对象)

可以使用isinstance()判断一个对象是否是Iterable对象:

(Iter 迭代,Iterable 可迭代的)

 

>>> from collections import Iterable

>>>  isinstance ([],Iterable)

True

>>>  isinstance ('abc',Iterable)

True

>>>  isinstance ((),Iterable)

True

>>>  isinstance (10,Iterable)

False

>>>  isinstance ((x for x in  range (10)),Iterable)

True

 

而生成器不但可以作用于for循环, 还可以被next() 函数环断调用并返回下一个值, 

直到最后抛出StopIteration错误表示无法继续返回下一个值了。 

 

*可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator. 

可以使用isinstance()判断一个对象是否是Iterator对象:

 

# 可以for循环的对象称为可迭代对象;可以被 next()函数调用并不断返回下一个值的对象称为迭代器

# 可迭代 Iterable 对象和迭代器 Iterator 并不是一回事

 

1-1

>>> a = [1,2,3]

# 是可迭代对象,但并不是 迭代器;因为它没有 next() 方法

 

1-1-2

# 通过 dir 命令,可以查看它所有的可以调用的方法

>>> a = [1,2,3]

>>>  dir (a)

['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', 

'__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', 

'__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', 

'__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', 

'__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 

'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

>>>    

# 这个列表中,没有 next 方法,没有 next 方法,就不叫迭代器

# 有 next 方法,就叫 迭代器

# 可以使用 isinstance() 判断是否为 迭代器 对象

 

1-1-3

>>> from collections import Iterator

>>>  isinstance ((x for x in  range (5)),Iterator)

True

# 一个生成器就是一个迭代器,因为生成器有 next 方法

# 但迭代器不一定是生成器,有next方法不一定是生成器

 

# 所以,列表、字典、元组等 就一定不是迭代器

 

2-1

生成器都是Iterator对象, 但list、dict、str虽然是Iterable,却不是Iterator. 

把list、dict、str等Iterable变成Iterator可以使用iter()函数: 

 

>>>  isinstance ( iter ([]),Iterator)

True

>>>  isinstance ( iter ('abc'),Iterator)

True

>>>    

 

2-1-2

>>> a

[1, 2, 3]

>>>  iter (a)

<list_iterator object at 0x000001EAA35F4D88>

>>> b= iter (a)

>>> b. __next__ ()

1

>>> b. __next__ ()

2

>>> b. __next__ ()

3

# 字典和字符串也是如此

 

2-2

你可能会问, 为什么list、dict、str等数据类型不是Iterator?

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,

直到没有数据时抛出StopIteration错误。

# 列表一定有开始和结束

# 而数据流默认生成时,可以认为是一个流,没有固定的结束点

 

可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next() 函数实现按需计算下一个数据,

所以Iterator的计算是情性的,只有在需要返回下一个数据时它才会计算。 

# 有一种计算叫做 惰性计算,即走到这一步才计算,没有走到这一步就不会计算

# 而链表都是计算好的,所以不是惰性计算

 

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。 

# 因为 iterator 根本不知道结束点在哪里

 

小结 

凡是可作用于for循环的对象都是Iterable类型: 

凡是可作用于next()函数的对象都是Iterator类型, 它们表示一个惰性计算的序列: 

集合数据类型 如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。 

Python的 for 循环 本质上就是通过 不断调用  next () 函数实现的, 例如: 

 

>>> for x in [1,2,3,4,5]:

...     pass

 

实际上完全等价于:

#首先获得Iterator对象: 

it= iter ([1,2,3,4,5]) 

#循环: 

while True: 

   try:

      # 获得下一个值: 

      x= next (it) 

   except StopIteration: 

      # 遇到StopIteration就退出循环 

      break 

 

2-3

>>>  range (10)

range (0, 10)

# 立刻返回一个列表,其实这个在python3.0中就是一个迭代器;在python2中 xrange(10)是一个迭代器;range(10)返回列表

# for循环本质上就是迭代器,只不过是封装了,所以看不出来

查看更多关于20210330 迭代器与生成器的详细内容...

  阅读:31次