Python学习之路day4-列表生成式、生成器、Iterable和Iterator

1、列表生成式

顾名思义,列表生成式就是用于生成列表的特殊语法形式的表达式。python

1.1 语法格式

[exp for iter_var in iterable]

工做过程:程序员

1.经过iter_var迭代iterable中的每一个元素算法

2.结合迭代的元素iter_var和exp表达式计算出结果编程

3.以列表形式返回每次迭代后exp表达式的计算值数组

因而可知咱们最终获得的是一个列表,所以整个表达式是放在列表符号[]中的。安全

以上语法格式仅仅是最简单的列表生成式形式,实际应用中还能够增长条件判断(过滤)和嵌套循环等稍微复杂一些的处理逻辑,相应增长的逻辑处理就是在 每次迭代时先对迭代的对象进行条件判断和嵌套循环处理,符合条件或处理完嵌套循环逻辑后再经过exp表达式来得到当前迭代过程当中的计算值。对应的形式以下:多线程

  • 带条件判断的列表生成式
[exp for iter_var in iterable if_exp]
  • 带嵌套循环的列表生成式
[exp for iter_var_A in iterable_A for iter_var_B in iterable_B]

1.2 应用场景

经过上述对列表生成式的语法形式不难看出,列表生成式是python提供的一种快速生成一个新的列表的简洁方式(一条语句中能够包含条件判断和嵌套循环)。它最主要的应用场景是:根据已存在的可迭代对象推导出一个新的list(可迭代对象便可应用于for循环的对象,下文会有更详解的介绍)。app

1.3 应用举例

下面结合实例来对比下使用列表生成式和不使用列表生成式的状况:函数

  • 生成一个简单列表

生成一个从1到10的整数组成的列表:性能

(1)不使用列表生成式

  1 list1 = []
  2 for i in range(1,11):
  3     list1.append(i)

(2)使用列表生成式

  1 list1 = [i for i in range(1, 11)]
  • 生成一个带条件判断的列表

生成一个从1到10之间由偶数组成的列表:

(1)不使用列表生成式

  1 list1 = []
  2 for i in range(1,11):
  3     if i % 2 == 0:
  4         list1.append(i)

(2)使用列表生成式

  1 list1 = [i for i in range(1, 11) if i % 2 == 0]
  • 生成一个带嵌套循环的列表

计算两个的全排列,并将结果以元组形式保存到一个新的列表中

(1)不使用列表生成式

  1 key_list = ['Python', 'PHP', 'JAVA']
  2 value_list = ['coding', 'learning']
  3 new_list = []
  4 for i in key_list:
  5     for j in value_list:
  6         new_list.append((i,j))
  7 print(new_list)

(2)使用列表生成式

  1 key_list = ['Python', 'PHP', 'JAVA']
  2 value_list = ['coding', 'learning']
  3 new_list = []
  4 new_list = [(i, j) for i in key_list for j in value_list]
  5 print(new_list)

上述多个示例充分说明,使用列表生成式明显要更方便简洁。

2、生成器

2.1 生成器的诞生背景

通过上文对列表生成式的讲解,咱们发现列表生成式彷佛很好很强大,但任何事物都有两面性,仔细分析列表生成式的过程和本质就能够看出列表生成式是直接生成一个新的列表,而后把全部的元素都一次性地存放在内存中,所以存在如下缺陷:

  • 内存容量老是有限的,所以列表容量有限(这点还能接受);
  • 当列表中的元素不少时,势必会占用大量的内存,而若是咱们偏偏仅仅须要访问其中的部分元素甚至说前面几个元素时,就会形成内存的极度浪费;
  • 与第二点相对应的是,此时系统反应很慢,生成须要的列表耗时很长。

所以当元素的数量达到必定的级别时,使用列表生成式就不太明智了。怎么解决这些问题呢?

若是列表元素能够按照某种既定的算法推算出来,那咱们是否能够在循环的过程当中不断推算出后续的元素呢?这样就没必要建立完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。(这段文字源自传送门

2.2 生成器的本质理解

从上面的论述能够提炼出生成器的如下本质过程:
生成器按照指定的算法,结合循环不断推算出后续的元素(每循环一次就推算一个),所以并非简单地一上来就一股脑地生成出全部的数据,而是在调用(循环)时才生成。列表的长度在动态变化,可长可短(取决于循环的次数),所以很是有利于控制对内存的占用,可节省大量内存空间(须要使用多少就申请分配多少)。
这也使得经过生成器生成的元素几乎是没有限制的,相应的操做返回时间也很理想。

须要注意生成器的一个特性是,在循环推算过程当中,它只能记录的当前的位置,并日后推算,不能返回来往前“回顾”(即只能next,不能prev)。

好了,阐述这么多,回归到目的用途上,生成器也是用来生成数据的--按照既定的某种算法不断生成后续的新的数据,直到再也不循环(调用新数据)或者说知足了某个指定的条件后结束。

2.3 生成器的构造方式

可经过如下两种方式来构造生成器:

  • 经过相似列表生成式的方式来构造,把列表生成式中的列表符号[]替换为函数符号()便可(指定的算法须要经过函数来定义呀)
  • 使用包含yield关键字的函数来构造

若是循环的逻辑算法比较简单,可直接使用第一种方式,反之算法比较复杂时(某些列表很难用列表生成式写出来,可是用函数就很容易实现),就只能经过第二种包含yield的函数(这是生成器与普通函数在形式上的惟一区别)来构造生成器了。

仍是经过实例来形象理解吧。

(1) 经过相似列表生成式的方式来构造

  1 gen1 = ( n for n in range(11) if n % 2 == 0)
  2 print(type(gen1))
  3 print(gen1)
  4 
  5 输出:
  6 <class 'generator'>
  7 <generator object <genexpr> at 0x0000000001DF0830>

(2) 经过函数来构造

  1 def gen2():
  2     for i in range(11):
  3         if i % 2 == 0:
  4             yield n
  5 print(type(gen2()))
  6 print(gen2())
  7 
  8 输出:
  9 <class 'generator'>
 10 <generator object gen2 at 0x0000000001E20830>

从这里能够看出对于比较简单的推算算法,若是经过相似列表生成式的方式和函数均可以构造,那么用相似列表生成式的方式显然更简单快捷。
但对于相似下面复杂的情形,咱们只能选择经过函数来构造生成器:
好比,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数均可由前两个数相加获得:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契数列用列表生成式写不出来,可是,用函数把它打印出来却很容易:

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         print(b)
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'
  8 
  9 '''注意,赋值语句:a, b = b, a + b至关于:
 10 t = (b, a + b) # t是一个tuple
 11 a = t[0]
 12 b = t[1]
 13 但没必要显式写出临时变量t就能够赋值。
 14 '''
 15 

仔细观察,能够看出,fib函数其实是定义了斐波拉契数列的推算规则,能够从第一个元素开始,推算出后续任意的元素,这种逻辑其实很是相似generator。
这里把fib函数转换成生成器:

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'

注意这里把普通的函数转换成生成器的时候只有一个变化: 把原来的print转换成了yield.
一旦一个函数定义中包含yield关键字,那么这个函数就再也不是一个普通函数,而是一个generator:
<generator object fib at 0x0000000001DF0830>

2.4 访问生成器的数据

可经过如下两种方式来访问生成器中的数据:

  1. 经过__next__()方法
      1 gen1 = ( n for n in range(11) if n % 2 == 0)
      2 print(gen1.__next__())
      3 print(gen1.__next__())
      4 print(gen1.__next__())
      5 print(gen1.__next__())
      6 print(gen1.__next__())
      7 print(gen1.__next__())
      8 
      9 输出:
     10 0
     11 2
     12 4
     13 6
     14 8
     15 10
     
      1 def fib(max):
      2     n, a, b = 0, 0, 1
      3     while n < max:
      4         yield b
      5         a, b = b, a + b
      6         n = n + 1
      7     return 'done'
      8 f = fib(10)
      9 print(f.__next__())
     10 print(f.__next__())
     11 print(f.__next__())
     12 print(f.__next__())
     13 print(f.__next__())
     14 
     15 输出:
     16 1
     17 1
     18 2
     19 3
     20 5
  2. 经过for循环去迭代
    上文__next__()方法访问生成器的数据例子中,访问数据很麻烦,只能一个个地去next,对于可生成不少个元素的生成器而言,须要获取全部的元素时,__next__()显得无能为力。此时简单的for循环便可搞定。
    例 1:
      1 gen1 = ( n for n in range(11) if n % 2 == 0)
      2 for i in gen1:
      3     print(i)
      4 
      5 输出:
      6 0
      7 2
      8 4
      9 6
     10 8
     11 10

    例2:
      1 def fib(max):
      2     n, a, b = 0, 0, 1
      3     while n < max:
      4         yield b
      5         a, b = b, a + b
      6         n = n + 1
      7     return 'done'
      8 f = fib(10)
      9 for i in f:
     10     print(i)
     11 
     12 输出:
     13 1
     14 1
     15 2
     16 3
     17 5
     18 8
     19 13
     20 21
     21 34
     22 55

2.5 关于StopIteration

上文中在访问生成器的数据时,没有阐述StopIteration,这里单独列出来。
当生成器中的数据被访问完毕后仍然尝试访问时,会抛出StopIteration异常,意思是不能再迭代了。经过__next__()方法访问生成器中的数据时可能会触发该异常,而经过for循环不会产生该异常,缘由是for循环访问时有明确的循环结束条件。

1. 列表生成式生成器抛出StopIteration异常:

  1 gen1 = ( n for n in range(11) if n % 2 == 0)
  2 print(gen1.__next__())
  3 print(gen1.__next__())
  4 print(gen1.__next__())
  5 print(gen1.__next__())
  6 print(gen1.__next__())
  7 print(gen1.__next__())
  8 print(gen1.__next__())  #不断地经过__next__()方法尝试访问生成器的数据,直到“越界”
  9 
 10 程序输出:
 11 0
 12 2
 13 4
 14 6
 15 8
 16 10
 17 Traceback (most recent call last):
 18   File "D:/python/S13/Day4/1.py", line 26, in <module>
 19     print(gen1.__next__())
 20 StopIteration         #最后一次尝试访问抛出了异常


2. yield 函数生成器抛出StopIteration异常:

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'
  8 f = fib(5)
  9 for i in range(7):
 10     print(f.__next__())
 11     i += 1
 12 
 13 输出:
 14 1
 15 1
 16 2
 17 3
 18 5
 19 Traceback (most recent call last):
 20   File "D:/python/S13/Day4/1.py", line 51, in <module>
 21     print(f.__next__())
 22 StopIteration: done

能够看出yield 函数生成器同样能够抛出StopIteration异常,在引起StopIteration异常后return定义的返回值会打印输出。换句话说,若是想得到生成器函数的返回值,只能经过不断地访问生成器的数据直到抛出StopIteration。

3. 捕获StopIteration异常
Python也具有相应的异常处理机制,这里咱们来捕获StopIteration异常

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         a, b = b, a + b
  6         n = n + 1
  7     return 'done'
  8 f = fib(5)
  9 while True:
 10     try:
 11         x = f.__next__()
 12         print("f:",x)
 13     except StopIteration as e:
 14         print("Generator return value:",e.value)   #当try中预期须要执行的代码块执行出错时,就会执行except中的代码块
 15         break
 16 
 17 程序输出:
 18 f: 1
 19 f: 1
 20 f: 2
 21 f: 3
 22 f: 5
 23 Generator return value: done

这里只是简单演示下如何捕获StopIteration异常,关于异常处理的更多细节,将在后续的笔记中深刻展开。

2.6 yield的特殊性
咱们已经知道yield关键字能够把一个函数转换为生成器,yield语句用来代替普通函数中的return来返回结果,咱们每尝试访问一个生成器中的元素时,若是没有抛出异常,yield就返回一次结果。这个过程当中存在一个特殊性:yield语句每次返回结果后就挂起函数的状态,以便下次从离开它的地方继续执行。

听起来彷佛不太好理解,先来一段更详细的阐述把(引用自 https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/?cmp=dwnpr&cpb=dw&ct=dwcon&cr=cn_51CTO_dl&ccy=cn):
简单地讲,yield 的做用就是把一个函数变成一个 generator,带有 yield 的函数再也不是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是彻底同样的,因而函数继续执行,直到再次遇到 yield。

  1 def fib(max):
  2     n, a, b = 0, 0, 1
  3     while n < max:
  4         yield b
  5         print("继续迭代,呵呵")
  6         a, b = b, a + b
  7         n = n + 1
  8     return 'done'
  9 f = fib(5)
 10 while True:
 11     try:
 12         x = f.__next__()
 13         print("f:", x)
 14         print("我已经循环一次了,插播点广告把") #获取一次返回值后能够执行其余的任务,下一次迭代后又能继续回到原来中断的位置
 15     except StopIteration as e:
 16         print("Generator return value:",e.value)
 17         break
 18 
 19 输出:
 20 f: 1
 21 我已经循环一次了,插播点广告把
 22 继续迭代,呵呵
 23 f: 1
 24 我已经循环一次了,插播点广告把
 25 继续迭代,呵呵
 26 f: 2
 27 我已经循环一次了,插播点广告把
 28 继续迭代,呵呵
 29 f: 3
 30 我已经循环一次了,插播点广告把
 31 继续迭代,呵呵
 32 f: 5
 33 我已经循环一次了,插播点广告把
 34 继续迭代,呵呵
 35 Generator return value: done

再来一个展现得更清楚的简单例子:

  1 def test(min, max):
  2     for n in range(min, max):
  3         yield n
  4         print('Break point')
  5 
  6 f = test(1, 4)
  7 print(f.__next__())
  8 print(f.__next__())  # 执行两次__next__()方法访问生成器的元素
  9 
 10 输出:
 11 1
 12 Break point #结果只输出了一个断点,也就是yield后面的程序,说明只能是第二次访问元素时输出的
 13 2


上述程序中咱们经过两次执行__next()__方法来访问生成器的元素,结果只有一个break point的输出,这是第二次的__next__()访问的输出,若是注释掉第二个next,咱们会发现输出中没有break point,也就是yield后面的代码。

这足以说明yield语句执行后程序处于中断状态,同时保留了程序执行的状态和位置,当咱们执行其余任务后继续迭代时程序还能回到以前中断的状态和位置,这就给了咱们经过生成器进行并行计算的机会,哈哈。

2.7 send方法与生成器并行计算

前文已经提到,能够经过__next__()方法来访问生成器中的元素,其实质是__next__()方法唤醒了yield(yield返回一次后保持中断状态),yield再返回下一次迭代的数据。for循环访问生成器的数据也能够视为经过循环唤醒yield返回数据。
除此以外生成器中还有一个send()方法可用来唤醒yield,其不一样之处在于send()不只能唤醒yield,并且能给yield传值(该值将成为当前yield表达式的结果)

注意:
经过send()方法来访问生成器的元素时,send()方法第一次传入的参数必须为None,或者在第一次传入参数以前先调用__next__()方法,以便生成器先进入yield表达式,不然会报错。

下面经过实例来演示下:

  1 def consumer(name):
  2     print("%s 准备吃包子啦!"%name)
  3 
  4     while True:
  5         baozi = yield
  6         print("包子[%s]来了,被[%s]吃了" % (baozi, name))
  7 
  8 c = consumer("Maxwell")
  9 c.__next__()  # 不使用__next__()方法会报错,也能够用c.send(None)替代
 10 c.send("肉松馅")   # 调用yield,同时给yield传一个值
 11 c.send("韭菜馅")      # 再次调用yield,同时给yield传一个值
 12 
 13 输出:
 14 Maxwell 准备吃包子啦!
 15 包子[肉松馅]来了,被[Maxwell]吃了
 16 包子[韭菜馅]来了,被[Maxwell]吃了

从上面的示例程序能够看出,本来yield只能返回None,但经过send()方法传入参数后,该参数直接变成yield的值返回了。

总结下send()和__next__()方法的区别:
1. __next__()方法能够唤醒yield,只能get yield的返回值,只读访问生成器当前迭代的返回值;
2.send()方法不能能够唤醒yield,还能传值给yield,而且set yield的返回值,至关于写覆盖方式访问生成器当前迭代的返回值
3.第一次使用send以前须要确保生成器已经走到yield这一步(即已经中断过一次),可经过先执行__next__()或send(None)来确保,不然会报错,而__next__()没有这个限制。

下面进入生成器经过协程进行并行计算的章节。
先了解下基本理论吧(如下文字引自 http://blog.csdn.net/dutsoft/article/details/54729480):
协程,又称微线程。英文名Coroutine。
子程序,或者称为函数,在全部语言中都是层级调用,好比A调用B,B在执行过程当中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。因此子程序调用是经过栈实现的,一个线程就是执行一个子程序。

协程不一样于线程,线程是抢占式的调度,而协程是协同式的调度,协程须要本身作调度。
子程序调用老是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不一样。协程看上去也是子程序,但执行过程当中,在子程序内部可中断,而后转而执行别的子程序,在适当的时候再返回来接着执行。

协程优点是极高的执行效率。由于子程序切换不是线程切换,而是由程序自身控制,所以,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优点就越明显。用来执行协程多任务很是合适。

协程没有线程的安全问题。一个进程能够同时存在多个协程,可是只有一个协程是激活的,并且协程的激活和休眠又程序员经过编程来控制,而不是操做系统控制的。
由于协程是一个线程中执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可得到极高的性能。

Python对协程的支持是经过generator实现的。在generator中,咱们不但能够经过for循环来迭代,还能够不断调用next()函数获取由yield语句返回的下一个值。可是Python的yield不但能够返回一个值,它还能够接收调用者发出的参数。

来一个生产者消费者的实际例子:

  1 import time
  2 
  3 def consumer(name):
  4     print("%s 准备吃包子啦!"%name)
  5 
  6     while True:
  7         baozi = yield
  8 
  9         print("包子[%s]来了,被[%s]吃了"%(baozi,name))
 10 
 11 
 12 def producer(name):
 13     c = consumer("A")
 14     c2 = consumer("B")
 15     c.__next__()
 16     c2.__next__()
 17     print("老子准备吃包子啦!")
 18     for i in range(5):
 19         time.sleep(1)
 20         print("作了一个包子,分两半")
 21         c.send(i)
 22         c2.send(i)
 23 
 24 producer("Maxwell")
 25 
 26 程序输出:
 27 A 准备吃包子啦!
 28 B 准备吃包子啦!
 29 老子准备吃包子啦!
 30 作了一个包子,分两半
 31 包子[0]来了,被[A]吃了
 32 包子[0]来了,被[B]吃了
 33 作了一个包子,分两半
 34 包子[1]来了,被[A]吃了
 35 包子[1]来了,被[B]吃了
 36 作了一个包子,分两半
 37 包子[2]来了,被[A]吃了
 38 包子[2]来了,被[B]吃了
 39 作了一个包子,分两半
 40 包子[3]来了,被[A]吃了
 41 包子[3]来了,被[B]吃了
 42 作了一个包子,分两半
 43 包子[4]来了,被[A]吃了
 44 包子[4]来了,被[B]吃了

简单分析下执行过程:消费者实际上是一个生成器,生产者作出包子后,经过send方法把值传递给消费者并调用切换到消费者执行,消费者又经过yield把结果返回,并回到生产者这里。所以生产者不只仅是给消费者传递包子,还会等包子被吃了的消息返回后再继续生产下一轮的包子,每次循环是一个轮回,在一个线程内由生成者和消费者相互协做完成,故而称之为协程。

3、Iterable

可直接用于for循环的对象称为可迭代对象,即Iterable。
属于可迭代的数据类型有:

  • 集合数据类型:如list、tuple、dict、set、str等
  • 生成器(generator)

可经过isinstance()来判断一个对象是不是Iterable对象(注意后面的判断类型是Iterable):

  1 >>> from collections import Iterable
  2 >>> isinstance((),Iterable)
  3 True
  4 >>> isinstance([],Iterable)
  5 True
  6 >>> isinstance({},Iterable)
  7 True
  8 >>> isinstance('python',Iterable)
  9 True
 10 >>> isinstance((x for x in range(10)),Iterable)
 11 True
 12 >>> isinstance(100,Iterable)
 13 False
 14 >>>

4、Iterator

能够被__next()__函数调用并不断返回下一个值的对象称为迭代器(Iterator)。生成器符合这必定义,所以生成器也是一种迭代器。

如何理解迭代器(Iterator):
实际上,Python中的Iterator对象表示的是一个数据流,Iterator能够被__next__()函数调用并不断返回下一个数据,直到没有数据能够返回时抛出StopIteration异常错误。能够把这个数据流看作一个有序序列,但咱们没法提早知道这个序列的长度。同时,Iterator的计算是惰性的,只有经过__next___()函数时才会计算并返回下一个数据。(此段内容来自 这里
p.s.:生成器彻底符合上述特征。

isinstance()也能够用来判断一个对象是不是迭代器,但须要注意的是后面的判断类型参数是Iterator

  1 >>> from collections import Iterator
  2 >>> list1=['Python', 'Java', 'PHP']
  3 >>> isinstance(list1,Iterator)
  4 False
  5 >>> print(list1.__next__())
  6 Traceback (most recent call last):
  7   File "<stdin>", line 1, in <module>
  8 AttributeError: 'list' object has no attribute '__next__'
  9 >>> gen1=(x for x in range(11) if x % 2 == 0)
 10 >>> print(type(gen1))
 11 <class 'generator'>
 12 >>> isinstance(gen1, Iterator)
 13 True
 14 >>> isinstance('abc', Iterator)
 15 False
 16 >>>
 17 

5、Iterable、Iterator与Generator之间的关系

  1. 生成器对象既是可迭代对象,又是迭代器
    生成器对象可直接用于for循环,同时能够被__next()__函数调用并不断返回下一个值,直到没有数据能够返回时抛出StopIteration异常错误。所以生成器同时符合可迭代对象和迭代器的定义。
  2. 迭代器必定是可迭代对象,反之则不必定
    迭代器可直接用于for循环,所以必定是可迭代对象,但可迭代对象不必定能被__next()__函数调用并不断返回下一个值。例如list、dict、str等集合数据类型是可迭代对象,但不能经过__next__()函数调用,因此不是迭代器。可是它们能够经过iter()函数转换为一个迭代器对象。
      1 >>> from collections import Iterator
      2 >>> list1=['Python', 'Java', 'PHP']
      3 >>> isinstance(iter(list1),Iterator)
      4 True
      5 >>> isinstance(list1,Iterator)
      6 False
      7 >>> print(list1.__next__())
      8 Traceback (most recent call last):
      9   File "<stdin>", line 1, in <module>
     10 AttributeError: 'list' object has no attribute '__next__'
     11 >>>
相关文章
相关标签/搜索