import collections
from collections.abc import Iterable, Iterator, Generator
# 字符串
astr = "XiaoMing"
print("字符串:{}".format(astr))
print(isinstance(astr, Iterable))
print(isinstance(astr, Iterator))
print(isinstance(astr, Generator))
# 列表
alist = [21, 23, 32,19]
print("列表:{}".format(alist))
print(isinstance(alist, Iterable))
print(isinstance(alist, Iterator))
print(isinstance(alist, Generator))
# 字典
adict = {"name": "小明", "gender": "男", "age": 18}
print("字典:{}".format(adict))
print(isinstance(adict, Iterable))
print(isinstance(adict, Iterator))
print(isinstance(adict, Generator))
# deque
adeque=collections.deque('abcdefg')
print("deque:{}".format(adeque))
print(isinstance(adeque, Iterable))
print(isinstance(adeque, Iterator))
print(isinstance(adeque, Generator))
学习协程的困难
在学习协程的过程中,我发现网上很难找到一篇系统而全面的文章,导致我们往往只能半知半解,学完后还是一脸懵逼。因此,学习协程的第一门课程是要先了解生成器,因为只有掌握了生成器的基础,才能更好地理解协程。
可迭代、迭代器、生成器
初学Python时,对于可迭代、迭代器和生成器这三个概念常常分不清楚,甚至会认为它们是等价的。实际上,它们是不同的。
可迭代的对象很好理解,我们熟悉的有字符串、列表、字典、元组、双端队列等。为了验证我所说的,我们可以使用collections.abc
模块(Python 2中没有),通过isinstance()
来判断一个对象是否可迭代(Iterable
)、是否是迭代器(Iterator
)、是否是生成器(Generator
)。
需要注意的是,这几个判断方法在大部分情况下适用,但并不是绝对适用,具体原因请参考后续的补充说明。
字符串:XiaoMing
True
False
False
列表:[21, 23, 32, 19]
True
False
False
字典:{'name': '小明', 'gender': '男', 'age': 18}
True
False
False
deque:deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
True
False
False
可迭代对象的补充说明
在补充说明可迭代对象时,有几点需要注意:
- 可以通过
dir()
方法查看对象是否有__iter__
属性,若有,则说明该对象是可迭代的。但是,如果对象没有__iter__
属性,并不能说明它不可迭代。这是因为,当对象没有__iter__
属性时,Python解释器会尝试按顺序(从索引0开始)通过__getitem__
方法获取元素,如果不抛出异常,则可以认为该对象是可迭代的。因此,判断对象是否可迭代最好的方法是通过for
循环或者iter()
函数进行实际运行。
以上是关于可迭代对象的补充说明。大家可以通过实际运行来验证这些概念,加深对可迭代、迭代器和生成器的理解。
from collections.abc import Iterable, Iterator, Generator
class MyList(object): # 定义可迭代对象类
def __init__(self, num):
self.end = num # 上边界
# 返回一个实现了__iter__和__next__的迭代器类的实例
def __iter__(self):
return MyListIterator(self.end)
class MyListIterator(object): # 定义迭代器类
def __init__(self, end):
self.data = end # 上边界
self.start = 0
# 返回该对象的迭代器类的实例;因为自己就是迭代器,所以返回self
def __iter__(self):
return self
# 迭代器类必须实现的方法,若是Python2则是next()函数
def __next__(self):
while self.start < self.data:
self.start += 1
return self.start - 1
raise StopIteration
if __name__ == '__main__':
my_list = MyList(5) # 得到一个可迭代对象
print(isinstance(my_list, Iterable)) # True
print(isinstance(my_list, Iterator)) # False
# 迭代
for i in my_list:
print(i)
my_iterator = iter(my_list) # 得到一个迭代器
print(isinstance(my_iterator, Iterable)) # True
print(isinstance(my_iterator, Iterator)) # True
# 迭代
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator))
迭代器
相比可迭代对象,迭代器只是多了一个函数。这个函数就是__next__()
,我们可以使用next()
方法来逐个获取元素值,而不再需要使用for
循环。
迭代器是在可迭代对象的基础上实现的。要创建一个迭代器,首先需要有一个可迭代对象。现在让我们来看看如何创建一个可迭代对象,并以可迭代对象为基础创建一个迭代器。
0
1
2
3
4
True
False
True
True
0
1
2
3
4
from collections.abc import Iterator
aStr = 'abcd' # 创建字符串,它是可迭代对象
aIterator = iter(aStr) # 通过iter(),将可迭代对象转换为一个迭代器
print(isinstance(aIterator, Iterator)) # True
next(aIterator) # a
next(aIterator) # b
next(aIterator) # c
next(aIterator) # d
# 使用列表生成式,注意不是[],而是()
L = (x * x for x in range(10))
print(isinstance(L, Generator)) # True
生成器
生成器的概念首次出现在Python 2.2中。引入生成器的目的是为了实现在计算下一个值时不浪费空间的结构。
前面我们提到,迭代器是在可迭代对象的基础上添加了next()
方法。而生成器则是在迭代器的基础上(可以使用for
循环,可以使用next()
方法),再实现了yield
关键字。
yield
相当于函数中的return
语句。每次调用next()
或进行for
循环时,yield
会将新的值返回,并在此处阻塞,等待下一次的调用。正是由于这个机制,生成器在Python编程中发挥了重要作用,实现了节省内存和异步编程。
下面是创建生成器的两种主要方法:
-
使用生成器函数:定义一个函数,并在函数体中使用
yield
关键字来产生值。每次调用生成器函数时,都会返回一个生成器对象。 -
使用生成器表达式:类似于列表推导式,但使用圆括号而不是方括号。生成器表达式可以直接生成一个生成器对象。
以上是关于生成器的介绍以及创建生成器的两种方法。生成器的特性使得它在Python编程中具有独特的优势,可以实现高效的内存管理和异步编程。
# 实现了yield的函数
def mygen(n):
now = 0
while now < n:
yield now
now += 1
if __name__ == '__main__':
gen = mygen(10)
print(isinstance(gen, Generator)) # True
def mygen(n):
now = 0
while now < n:
yield now
now += 1
if __name__ == '__main__':
gen = mygen(4)
# 通过交替执行,来说明这两种方法是等价的。
print(gen.send(None))
print(next(gen))
print(gen.send(None))
print(next(gen))
运行/激活生成器
可迭代对象和迭代器会将所有的值都生成并存放在内存中,而生成器则是在需要元素时临时生成,从而节省时间和空间。
那么,如何运行或激活生成器呢?由于生成器不会一次性生成所有元素,而是逐个执行并返回,我们需要激活生成器来执行它的代码块。下面介绍两种激活生成器的方法。
-
使用
next()
函数:通过调用next()
函数来激活生成器并获取下一个值。每次调用next()
函数时,生成器会从上次暂停的位置继续执行,并返回下一个值。 -
使用
for
循环:使用for
循环来遍历生成器对象,会自动激活生成器并获取每个值。for
循环会在每次迭代时自动调用next()
函数,直到生成器没有更多的值可返回。
通过以上两种方法,我们可以激活生成器并逐个获取其生成的值。
以上是关于如何运行/激活生成器的介绍。通过实际运行下面的例子,你就能更好地理解了。
0
1
2
3
from inspect import getgeneratorstate
def mygen(n):
now = 0
while now < n:
yield now
now += 1
if __name__ == '__main__':
gen = mygen(2)
print(getgeneratorstate(gen))
print(next(gen))
print(getgeneratorstate(gen))
print(next(gen))
gen.close() # 手动关闭/结束生成器
print(getgeneratorstate(gen))
生成器的执行状态
生成器在其生命周期中会有以下四个状态:
-
GEN_CREATED
:等待开始执行状态,表示生成器已经创建但还未开始执行。 -
GEN_RUNNING
:解释器正在执行状态(只有在多线程应用中才能看到这个状态),表示生成器正在被解释器执行。 -
GEN_SUSPENDED
:在yield
表达式处暂停状态,表示生成器在执行过程中遇到yield
表达式并暂停。 -
GEN_CLOSED
:执行结束状态,表示生成器已经执行完毕。
通过以下代码示例,可以感受一下生成器的不同执行状态。为了简化代码理解难度,我不会举例GEN_RUNNING
状态,如果你对此感兴趣,可以尝试在多线程应用中观察。
`python
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator()
print(gen.giframe.flasti) # 输出: -1,表示生成器处于GEN_CREATED状态
print(next(gen))
print(gen.giframe.flasti) # 输出: 6,表示生成器处于GEN_SUSPENDED状态
print(next(gen))
print(gen.giframe.flasti) # 输出: 12,表示生成器处于GEN_SUSPENDED状态
print(next(gen))
print(gen.giframe.flasti) # 输出: 18,表示生成器处于GEN_SUSPENDED状态
执行完所有yield语句后,生成器处于GEN_CLOSED状态
`
以上是关于生成器的执行状态的介绍。如果有任何疑问,可以在后台回复给我。
GEN_CREATED
0
GEN_SUSPENDED
1
GEN_CLOSED
生成器的异常处理
在生成器的工作过程中,如果生成器不满足生成元素的条件,就会/应该抛出异常(StopIteration
)。通过列表生成式构建的生成器,其内部已经自动帮我们实现了抛出异常这一步。
让我们来看一个例子,以验证这一点:
`python
gen = (x for x in range(5))
print(next(gen)) # 输出: 0
print(next(gen)) # 输出: 1
print(next(gen)) # 输出: 2
print(next(gen)) # 输出: 3
print(next(gen)) # 输出: 4
print(next(gen)) # 抛出StopIteration异常
`
通过以上代码,我们可以看到,在使用列表生成式构建的生成器中,当生成器没有更多的值可返回时,会自动抛出StopIteration
异常。
以上是关于生成器的异常处理的说明。生成器在工作过程中,通过抛出异常来表示生成结束,我们可以在代码中进行相应的异常处理。
def mygen(n):
now = 0
while now < n:
yield now
now += 1
raise StopIteration
if __name__ == '__main__':
gen = mygen(2)
next(gen)
next(gen)
next(gen)
def jumping_range(N):
index = 0
while index < N:
# 通过send()发送的信息将赋值给jump
jump = yield index
if jump is None:
jump = 1
index += jump
if __name__ == '__main__':
itr = jumping_range(5)
print(next(itr))
print(itr.send(2))
print(next(itr))
print(itr.send(-1))
自定义生成器的异常处理
在自定义生成器时,我们也应该在不满足生成元素条件的情况下抛出异常。让我们修改上面的代码来演示这一点:
`python
def my_generator():
for x in range(5):
yield x
if x == 3:
raise StopIteration("StopIteration exception raised")
gen = my_generator()
try:
while True:
print(next(gen))
except StopIteration as e:
print(e)
`
通过以上代码,我们在生成器中添加了一个条件,当x
等于3时,抛出StopIteration
异常。在主程序中,我们使用while
循环和next()
函数来逐个获取生成器的值,当生成器抛出异常时,我们捕获并打印异常信息。
以上是关于自定义生成器的异常处理的示例。
从生成器过渡到协程:yield
通过前面的介绍,我们知道生成器引入了暂停函数执行的功能(yield
)。当生成器暂停时,我们可以向其发送一些信息。这种向暂停的生成器发送信息的功能在Python 2.5中通过PEP 342引入,并催生了Python中协程的诞生。
根据维基百科的定义,协程是为非抢占式多任务产生子程序的计算机程序组件,允许不同入口点在不同位置暂停或开始执行程序。
需要注意的是,协程并不属于语言本身的概念,而是一种编程模型的概念。
协程和线程有一些相似之处,多个协程和线程一样,只会交替串行执行。但也有一些不同之处,线程之间需要频繁进行切换、加锁和解锁,从复杂度和效率的角度来看,与协程相比,这是一个痛点。协程通过使用yield
来暂停生成器,可以将程序的执行流程交给其他子程序,从而实现不同子程序之间的交替执行。
下面通过一个简单的演示来展示如何向生成器中发送消息。
`python
def my_generator():
while True:
x = yield
print(“Received:”, x)
gen = my_generator()
next(gen) # 激活生成器
gen.send(“Hello”) # 向生成器发送消息
`
通过以上代码,我们创建了一个生成器,使用yield
暂停生成器的执行,并在接收到消息时打印出来。我们通过send()
方法向生成器发送消息,生成器会在yield
处恢复执行,并打印出接收到的消息。
以上是关于向生成器发送消息的示例。
0
2
3
2
解释输出结果
让我们解释一下为什么会有这样的输出结果,重点是jump = yield index
这个语句。
在上面的代码中,我们创建了一个生成器函数my_generator()
,其中有一个关键的语句jump = yield index
。这个语句可以分成两部分来理解:
-
yield index
:这部分表示生成器会在这里暂停执行,并将index
的值返回给调用者。也就是说,每次调用生成器的send()
方法时,会将发送的值赋给index
,并在此处暂停。 -
jump =
:这部分表示将yield
表达式的值赋给jump
变量。也就是说,当生成器恢复执行时,会将send()
方法发送的值赋给jump
。
在主程序中,我们激活生成器并调用了send()
方法来向生成器发送消息。首先,我们调用next(gen)
来激活生成器并执行到第一个yield
语句处,此时index
的值为0。然后,我们调用gen.send("Jump")
来向生成器发送消息,这个消息会被赋值给jump
变量。生成器恢复执行,继续执行后面的代码,并打印出index
和jump
的值。
因此,输出结果为:
index: 0, jump: None
index: 1, jump: Jump
以上是对输出结果的解释。
实践和理解
以上这些内容都是关于协程并发的基础必备知识。请务必亲自去实践并理解它们,否则后面的内容将会变得枯燥无味、晦涩难懂。
通过实践和理解这些基础知识,你将能够更好地理解和应用后续的内容。
祝你在学习协程并发的过程中取得进步!