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

时间:2021-09-21 10:45:40

一、列表生成式

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

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循环的对象,下文会有更详解的介绍)。

1.3 应用举例

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

  • 生成一个简单列表

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

(1)不使用列表生成式

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

(2)使用列表生成式

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

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

(1)不使用列表生成式

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

(2)使用列表生成式

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

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

(1)不使用列表生成式

 key_list = ['Python', 'PHP', 'JAVA']
value_list = ['coding', 'learning']
new_list = []
for i in key_list:
for j in value_list:
new_list.append((i,j))
print(new_list)

(2)使用列表生成式

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

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

二、生成器

2.1 生成器的诞生背景

经过上文对列表生成式的讲解,我们发现列表生成式似乎很好很强大,但任何事物都有两面性,仔细分析列表生成式的过程和本质就可以看出列表生成式是直接生成一个新的列表,然后把所有的元素都一次性地存放在内存中,因此存在以下缺陷:

  • 内存容量总是有限的,因此列表容量有限(这点还能接受);
  • 当列表中的元素很多时,势必会占用大量的内存,而如果我们恰恰仅仅需要访问其中的部分元素甚至说前面几个元素时,就会造成内存的极度浪费;
  • 与第二点相对应的是,此时系统反应很慢,生成需要的列表耗时很长。

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

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

2.2 生成器的本质理解

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

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

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

2.3 生成器的构造方式

可通过以下两种方式来构造生成器:

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

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

还是通过实例来形象理解吧。

(1) 通过类似列表生成式的方式来构造

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

(2) 通过函数来构造

 def gen2():
for i in range(11):
if i % 2 == 0:
yield n
print(type(gen2()))
print(gen2()) 输出:
<class 'generator'>
<generator object gen2 at 0x0000000001E20830>

从这里可以看出对于比较简单的推算算法,如果通过类似列表生成式的方式和函数都可以构造,那么用类似列表生成式的方式显然更简单快捷。
但对于类似下面复杂的情形,我们只能选择通过函数来构造生成器:
比如,著名的斐波拉契数列(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]
但不必显式写出临时变量t就可以赋值。
'''

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

 def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

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

2.4 访问生成器的数据

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

  1. 通过__next__()方法
     gen1 = ( n for n in range(11) if n % 2 == 0)
    print(gen1.__next__())
    print(gen1.__next__())
    print(gen1.__next__())
    print(gen1.__next__())
    print(gen1.__next__())
    print(gen1.__next__()) 输出:
    0
    2
    4
    6
    8
    10
     def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
    yield b
    a, b = b, a + b
    n = n + 1
    return 'done'
    f = fib(10)
    print(f.__next__())
    print(f.__next__())
    print(f.__next__())
    print(f.__next__())
    print(f.__next__()) 输出:
    1
    1
    2
    3
    5
  2. 通过for循环去迭代
    上文__next__()方法访问生成器的数据例子中,访问数据很麻烦,只能一个个地去next,对于可生成很多个元素的生成器而言,需要获取所有的元素时,__next__()显得无能为力。此时简单的for循环即可搞定。
    例 1:
     gen1 = ( n for n in range(11) if n % 2 == 0)
    for i in gen1:
    print(i) 输出:
    0
    2
    4
    6
    8
    10

    例2:

     def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
    yield b
    a, b = b, a + b
    n = n + 1
    return 'done'
    f = fib(10)
    for i in f:
    print(i) 输出:
    1
    1
    2
    3
    5
    8
    13
    21
    34
    55

2.5 关于StopIteration

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

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

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

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

 def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
f = fib(5)
for i in range(7):
print(f.__next__())
i += 1 输出:
1
1
2
3
5
Traceback (most recent call last):
File "D:/python/S13/Day4/1.py", line 51, in <module>
print(f.__next__())
StopIteration: done

可以看出yield 函数生成器一样可以抛出StopIteration异常,在引发StopIteration异常后return定义的返回值会打印输出。换句话说,如果想获得生成器函数的返回值,只能通过不断地访问生成器的数据直到抛出StopIteration。

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

 def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
f = fib(5)
while True:
try:
x = f.__next__()
print("f:",x)
except StopIteration as e:
print("Generator return value:",e.value) #当try中预期需要执行的代码块执行出错时,就会执行except中的代码块
break 程序输出:
f: 1
f: 1
f: 2
f: 3
f: 5
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。

 def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
print("继续迭代,呵呵")
a, b = b, a + b
n = n + 1
return 'done'
f = fib(5)
while True:
try:
x = f.__next__()
print("f:", x)
print("我已经循环一次了,插播点广告把") #获取一次返回值后可以执行其他的任务,下一次迭代后又能继续回到原来中断的位置
except StopIteration as e:
print("Generator return value:",e.value)
break 输出:
f: 1
我已经循环一次了,插播点广告把
继续迭代,呵呵
f: 1
我已经循环一次了,插播点广告把
继续迭代,呵呵
f: 2
我已经循环一次了,插播点广告把
继续迭代,呵呵
f: 3
我已经循环一次了,插播点广告把
继续迭代,呵呵
f: 5
我已经循环一次了,插播点广告把
继续迭代,呵呵
Generator return value: done

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

 def test(min, max):
for n in range(min, max):
yield n
print('Break point') f = test(1, 4)
print(f.__next__())
print(f.__next__()) # 执行两次__next__()方法访问生成器的元素 输出:
1
Break point #结果只输出了一个断点,也就是yield后面的程序,说明只能是第二次访问元素时输出的
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表达式,否则会报错。

下面通过实例来演示下:

 def consumer(name):
print("%s 准备吃包子啦!"%name) while True:
baozi = yield
print("包子[%s]来了,被[%s]吃了" % (baozi, name)) c = consumer("Maxwell")
c.__next__() # 不使用__next__()方法会报错,也可以用c.send(None)替代
c.send("肉松馅") # 调用yield,同时给yield传一个值
c.send("韭菜馅") # 再次调用yield,同时给yield传一个值 输出:
Maxwell 准备吃包子啦!
包子[肉松馅]来了,被[Maxwell]吃了
包子[韭菜馅]来了,被[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不但可以返回一个值,它还可以接收调用者发出的参数。

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

 import time

 def consumer(name):
print("%s 准备吃包子啦!"%name) while True:
baozi = yield print("包子[%s]来了,被[%s]吃了"%(baozi,name)) def producer(name):
c = consumer("A")
c2 = consumer("B")
c.__next__()
c2.__next__()
print("老子准备吃包子啦!")
for i in range(5):
time.sleep(1)
print("做了一个包子,分两半")
c.send(i)
c2.send(i) producer("Maxwell") 程序输出:
A 准备吃包子啦!
B 准备吃包子啦!
老子准备吃包子啦!
做了一个包子,分两半
包子[0]来了,被[A]吃了
包子[0]来了,被[B]吃了
做了一个包子,分两半
包子[1]来了,被[A]吃了
包子[1]来了,被[B]吃了
做了一个包子,分两半
包子[2]来了,被[A]吃了
包子[2]来了,被[B]吃了
做了一个包子,分两半
包子[3]来了,被[A]吃了
包子[3]来了,被[B]吃了
做了一个包子,分两半
包子[4]来了,被[A]吃了
包子[4]来了,被[B]吃了

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

三、Iterable

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

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

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

 >>> from collections import Iterable
>>> isinstance((),Iterable)
True
>>> isinstance([],Iterable)
True
>>> isinstance({},Iterable)
True
>>> isinstance('python',Iterable)
True
>>> isinstance((x for x in range(10)),Iterable)
True
>>> isinstance(100,Iterable)
False
>>>

四、Iterator

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

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

isinstance()也可以用来判断一个对象是否是迭代器,但需要注意的是后面的判断类型参数是Iterator

 >>> from collections import Iterator
>>> list1=['Python', 'Java', 'PHP']
>>> isinstance(list1,Iterator)
False
>>> print(list1.__next__())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__next__'
>>> gen1=(x for x in range(11) if x % 2 == 0)
>>> print(type(gen1))
<class 'generator'>
>>> isinstance(gen1, Iterator)
True
>>> isinstance('abc', Iterator)
False
>>>

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

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