第十六篇 Python之迭代器与生成器

时间:2022-09-26 23:31:00

一.迭代器

一. 递归和迭代

生活实例说明什么是递归和迭代

A想去腾达大厦,问B怎么走路,B 说我不知道,我给你问问C,C也不知道,C又去问D,D知道,把路告诉了C,C又告诉B,B最后告诉A, 这就是递归。

A想去腾达大厦,问B怎么走,B说我不知道,但是C知道,你自己去问C吧,C说我要不知道, 但是D知道,你自己去问D吧,这就是迭代。

人类繁衍也是一种迭代,先有父,才有儿,有儿才有孙。

每次循环得到的结果都是依赖于上一次得到的结果而来的,这叫迭代。

迭代是一个重复的过程,每次重复即一次迭代,并且每次迭代的结果都是下一次迭代的初始值

二.什么是迭代器协议

1. 迭代器协议是指:对象必须提供一个next方法,执行该方法要么返回迭代中的下一项,要么就引起一个StopIteration异常,以终止迭代(只能往后走不能往前退)

2.可迭代对象:实现了迭代器协议的对象(如何实现:对象内部定义一个__iter__()方法,就是可迭代对象。

3.协议是一种约定,可迭代对象实现了迭代器协议,python()的内部工具(如for循环,sum,min,max函数等)遵循迭代器协议访问对象的。

4. 迭代器就可以可迭代对象,他两是一会事儿

为何要有迭代器?什么是可迭代对象?什么是迭代器对象?

#1、为何要有迭代器?
对于序列类型:字符串、列表、元组,我们可以使用索引的方式迭代取出其包含的元素。但对于字典、集合、文件等类型是没有索引的,若还想取出其内部包含的元素,
则必须找出一种不依赖于索引的迭代方式,这就是迭代器
#2、什么是可迭代对象? 可迭代对象指的是内置有__iter__方法的对象,即obj.__iter__,如下 'hello'.__iter__ (1,2,3).__iter__ [1,2,3].__iter__ {'a':1}.__iter__ {'a','b'}.__iter__ open('a.txt').__iter__ #3、什么是迭代器对象? 可迭代对象执行obj.__iter__()得到的结果就是迭代器对象 而迭代器对象指的是即内置有__iter__又内置有__next__方法的对象 文件类型是迭代器对象 open('a.txt').__iter__() open('a.txt').__next__() #4、注意: 迭代器对象一定是可迭代对象,而可迭代对象不一定是迭代器对象

迭代器对象的使用

dic={'a':1,'b':2,'c':3}
iter_dic=dic.__iter__() #得到迭代器对象,迭代器对象即有__iter__又有__next__,但是:迭代器.__iter__()得到的仍然是迭代器本身
iter_dic.__iter__() is iter_dic #True

print(iter_dic.__next__()) #等同于next(iter_dic)
print(iter_dic.__next__()) #等同于next(iter_dic)
print(iter_dic.__next__()) #等同于next(iter_dic)
# print(iter_dic.__next__()) #抛出异常StopIteration,或者说结束标志

#有了迭代器,我们就可以不依赖索引迭代取值了
iter_dic=dic.__iter__()
while 1:
    try:
        k=next(iter_dic)
        print(dic[k])
    except StopIteration:
        break
        
#这么写太丑陋了,需要我们自己捕捉异常,控制next,python这么牛逼,能不能帮我解决呢?能,请看for循环

总结一下

# l 里的所有内容都放在内存里了
l = [1,2,3,4,5]          

# 将列表转换成迭代器,只得到了一个迭代器对象,就是个内存地址,iter_l是可以节省内存的
iter_l = l.__iter__()    

# 只要有了iter_l 这个迭代器,或者可迭代对象,在任何地方都可以传输的
print(iter_l.__next__())    # 1
print(iter_l.__next__())    # 2
print(iter_l.__next__())    # 3
print(iter_l.__next__())    # 4
print(iter_l.__next__())    # 5
print(iter_l.__next__())    # StopIteration

# next()就是在调用iter_l.__next__()的方法,两者其实是一回事。
# 只不过next()是Python内置的函数方法;而__next__()是数据类型内置的方法。
print(next(iter_l))     # 1
print(next(iter_l))     # 2
print(next(iter_l))     # 3
print(next(iter_l))     # 4
print(next(iter_l))     # 5
print(next(iter_l))     # StopIteration

三. Python中强大的for循环机制

for 循环的本质:循环所有对象,全都是使用迭代器协议。

for循环的本质就是遵循迭代器协议访问对象,那么for循环的对象肯定都是迭代器了啊,那既然这样,for循环可以遍历(字符串,列表,元组,字典,集合,文件对象),那么这些类型的数据肯定都是可迭代对象啊?但是,为什么定义了一个列表 l = [1,2,3]却没有next()方法?打脸吗?

正本清源:

字符串,列表,元组,字典,集合,文件对象,这些都不是可迭代对象,只不过在for循环时,调用了他们内部的__iter__方法,把他们变成了可迭代对象。

然后for循环调用可迭代对象的__next__方法去取值,而且for循环会捕捉StopIteration异常,以终止迭代。

x = 'hello'
print(dir(x))
结果,x 字符串没有 next()方法,说明字符串没有遵循迭代器协议,那字符串就不是可迭代对象,同理,列表,元组,字典,集合等都不是可迭代对象。
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

那既然这些都不是可迭代对象,为什么for循环可以遍历那?
还是以x字符串为例说明:

# 1. 字符串x先调用内部方法__iter__(),把x变成可迭代对象。
iter_test = x.__iter__()

# 2. x已经变成了可迭代对象,那么就可以进行遍历了,如下。
print(iter_test.__next__())   # h
print(iter_test.__next__())   # e
print(iter_test.__next__())   # l
print(iter_test.__next__())   # l  
print(iter_test.__next__())   #0
# 3. 再往下就没有了,生出不儿子了,StopIteration异常了
print(iter_test.__next__())   

再回到for循环,理解一下原理

l = [1,2,3]
# 1. 先把列表 l变成一个可迭代对象,让他遵循迭代器协议,即l.__iter__(),然后把这个放在了for循环的l的位置。
# 2. 然后 for循环执行可迭代对象next方法进行遍历。
# 3. 上面字符串的x当next找不到对象的时候,就StopItertion异常,而 for 循环碰到 StopIteration时,就捕捉到了该异常,知道已经遍历完了。

for i in l:   # i_l = l.__iter__(), --> i_l.__next__()
    print(i)

到目前为止,列表的几种取值方式:
1. 索引方式:print(l[0))
2. 遵循迭代器协议访问方式,生成可迭代对象,iter_l = l.__iter__() ,然后就可以调用__next__()方法取值了
    print(iter_l.__next__())
    print(iter_l.__next__())
3. for循环方法方式

可以用索引的方式遍历列表吗? 当然可以。
index = 0
while index < len(l):
    print(l[index])
    index += 1
#基于for循环,我们可以完全不再依赖索引去取值了
dic={'a':1,'b':2,'c':3}
for k in dic:
    print(dic[k])

#for循环的工作原理
#1:执行in后对象的dic.__iter__()方法,得到一个迭代器对象iter_dic
#2: 执行next(iter_dic),将得到的值赋值给k,然后执行循环体代码
#3: 重复过程2,直到捕捉到异常StopIteration,结束循环

为何要有for循环?

基于上面讲的列表有三种访问方式,其实代表着 序列类型(字符串,列表,元组)都可以用上述的三种方式任意一种访问,你可以不用for循环访问,但是非序列类型(字典,集合,文件对象)呢?他们没有下标,不用for循环怎么遍历?所以,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用__iter__方法,将要遍历的对象转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就都可以通过for循环来遍历了,而且你看到的效果也确实如此,这就是无所不能强大至极的for循环。

四.迭代器的优缺点

#优点:
  - 提供一种统一的、不依赖于索引的迭代方式
  - 惰性计算,节省内存
#缺点:
  - 无法获取长度(只有在next完毕才知道到底有几个值)
  - 一次性的,只能往后走,不能往前退

二. 生成器

一. 什么是生成器?

可以理解为一种数据类型,这种数据类型自动实现了迭代器协议(其他的数据类型需要调用自己内置的__iter__方法),所以生成器就是可迭代对象

比如,列表要转换成遵循迭代器协议,需要 先调用__iter__()方法后,才能使用.__next__()方法
而 生成器不用先执行__iter__()方法,就意味着生成器可以直接调用__next__()方法
#只要函数内部包含有yield关键字,那么函数名()得到的结果就是生成器,并且不会执行函数内部代码

def func():
    print('====>first')
    yield 1
    print('====>second')
    yield 2
    print('====>third')
    yield 3
    print('====>end')

g=func()
print(g) #<generator object func at 0x0000000002184360>
# 生成器就是迭代器

g.__iter__
g.__next__
#2、所以生成器就是迭代器,因此可以这么取值
res=next(g)
print(res)

生成器分类及在Python中的表现形式:(Python里有两种不同的方式提供生成器)

1. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

(1)三元表达式

想知道生成器表达式,得先知道三元表达式
#
三元表达式 name = 'mamingc' res = 'good' if name == 'mamingc' else 'bad' print(res) # good # 什么是三元表达式?对上面的三元表达式进行解析 # 其实质还是个if条件判断,只不过书写形式不同于常规的if判断语句。 # 如果name == 'mamingc',则该条件为真,那么执行条件为真的语句,但是这个语句需要放到if的前面; # 如果条件不为真,则执行else语句,else的语句紧接着else后面写 # 然后将该条语句赋值给一个变量,这就就是三元表达式 # # 三元表达式转换成常规的if语句如下: name = 'lizhi' if name == 'lizhi': print("good") else: print("bad")

(2)列表解析

# 列表解析
l = ['鸡蛋%s' %i for i in range(10)]
l1 = [i for i in range(10)]
print(l)
# ['鸡蛋0', '鸡蛋1', '鸡蛋2', '鸡蛋3', '鸡蛋4', '鸡蛋5', '鸡蛋6', '鸡蛋7', '鸡蛋8', '鸡蛋9']
print(l1)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 什么是列表解析?对上面的列表解析进行解析:
# 首先,得有一个列表,for i in range(10) 在列表内进行了循环,
# 其次:循环得到的结果常规方法是append给了egg_list列表,而列表解析得到的结果则直接放到了for i in range(10)的前面
# 最后:将该语句赋值给一个变量
# 这就是列表解析

# 列表解析还可以这么玩
l2 = ['鸡蛋%s' %i for i in range(10) if i > 5]
print(l2)
# ['鸡蛋6', '鸡蛋7', '鸡蛋8', '鸡蛋9']

# 一元:主体一个for循环:for i in range(10)
# 二元:鸡蛋的处理:'鸡蛋%s' %i
# 三元:来一个if判断:if i > 5

# 列表解析:如果要处理的数据量很大,那就非常耗内存,因为还是要先生成一个列表,所以处理大量数据则需要用到生成器表达式。

(3)真正的生成器表达式来啦

# 生成器表达式
# 生成器表达式直接就是迭代器,就不要再费劲的先调用__iter__了
laomuji = ('鸡蛋%s' %i for i in range(10))   # 注意,把列表解析的[]换成(),就变成了生成器表达式
print(laomuji)   # <generator object <genexpr> at 0x015A88A0>
print(laomuji.__next__())   # 鸡蛋0
print(laomuji.__next__())   # 鸡蛋1
print(next(laomuji))      # 鸡蛋2       # next本质就是调用__next__

# 生成器表达式,自动实现了迭代器协议,就意味着下面肯定有一个next()方法,能实现next()才是生成器的核心。

生成器表达式总结:

1. 把列表解析的[]换成(),得到的就是生成器表达式

2. 列表解析与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存

3. Python使用迭代器协议,让for循环变得更加通用。大部分内置函数,也可以使用迭代器协议访问对象的。

例如,sum函数是Python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议,所以,我们可以直接这样计算一系列值的和

print(sum(x**2 for x in range(4)))   # 14

而不用多此一举的先构造一个列表:

print(sum([x **2 for x in range(4)]))  # 14
# 这么大的一个数,如果用列表的方式进行计算,电脑很可能会卡死
# 但是通过迭代器的方式计算,计算出结果的时间耗时比较久,但是电脑此时还能正常切换使用。这就能体现出省内存了。
import time
localtime = time.asctime( time.localtime(time.time()) )
print("开始时本地时间为 :", localtime)   #   开始时本地时间为 : Sat Jun 16 22:32:41 2018
print(sum(i for i in range(100000000000000000000000)))
# print(sum(i for i in range(100000)))
print("结束时本地时间为 :",localtime)    #   结束时本地时间为 :

 

2. 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行。

yield相当于return,return只能执行一个,执行一次,函数就退出了,而yield可以执行多次。

生成器函数的特性:

1. 有yield就行,yield的功能,相当于return,用来做返回值的

2. yield可以用来保留函数的运行状态,函数一运行到yield返回一个值,就停住了;如果需要再运行一次,就要再触发一次next,而且下一次的运行是从上一次next终止的位置开始运行的,而不是从函数的开头重新运行的。

3. 如果把函数写成生成器形式了,它有一个最大的优点:

def test():
    yield 1
    yield 2
    yield 3
g=test()              # 执行函数,有返回值,想要拿到函数的运行结果的返回值,把运行结果的返回值赋值给变量。test()只是相当于拿到了一个生成器
print('来自函数',g)     # 来自函数 <generator object test at 0x00DACD50>   生成器对象
print(g.__next__())    # 1
print(g.__next__())    # 2
print(next(g))    # 3
print(g.__next__())    # StopIteration
import time
def test():
    print('开始生孩子啦。。。。。。')
    print('开始生孩子啦。。。。。。')
    print('开始生孩子啦。。。。。。')
    yield '' # 相当于return
    time.sleep(3)
    print('开始生儿子啦')
    yield '儿子'

    time.sleep(3)
    print('开始生孙子啦')
    yield '孙子'


res=test()
print(res)
# 结果
<generator object test at 0x017A09C0>


print(res.__next__()) #test()
# 结果
开始生孩子啦。。。。。。
开始生孩子啦。。。。。。
开始生孩子啦。。。。。。
我


print(res.__next__()) #test()
# 结果
开始生儿子啦
儿子

print(next(res)) #test()
# 结果
开始生孙子啦
孙子
# 去庆丰吃包子
# 第一种形式,return方式,一次性把100个包子都做好了,才给客人吃。哪怕你来了多少人,都得等我这个100个包子都全部做好
# 了才给客人吃
def product_baozi():
    ret=[]
    for i in range(100):
        ret.append('一屉包子%s' %i)
    return ret
baozi_list=product_baozi()
print(baozi_list)

# 第二种形式,用yield方式,做一屉包子,来一个人就可以卖一屉,做好了就可以立马卖了。
# 就相当于来了客人,客人什么时候要,饭店就可以什么时候卖;而不是等所有包子都做好了才开始卖,
# 这么比较,yield就效率会高一些。
def product_baozi():
    for i in range(100):
        print('正在生产包子')
        yield '一屉包子%s' %i #i=1
        print('开始卖包子')
pro_g=product_baozi()

baozi1=pro_g.__next__()
# 结果正在生产包子       运行到 yield '一屉包子%s' %i 就停住了,
baozi1=pro_g.__next__() # 下一次next,就从上一次停住的位置开始运行,得到下面的结果 # 结果 开始卖包子 正在生产包子
## 下蛋的传说  ##
def xiadan():
    ret=[]
    for i in range(10000):
        ret.append('鸡蛋%s' %i)
    return ret

print(xiadan())
# return方式
#缺点1:占空间大,需要先找个篮子,把所有10000个蛋都先放到篮子里
#缺点2:效率低

def xiadan():
    for i in range(5):
        yield '鸡蛋%s' %i
# 生成器函数的好处呢,就不需要先把所有的蛋都放到一个篮子了,而是来一个人,下一个蛋你拿走就是了。
alex_lmj=xiadan()
# 来一个人,alex就给你下一个蛋你拿走
print(alex_lmj.__next__())     # 后面没人来拿鸡蛋,alex就不用下蛋了
# 结果
鸡蛋0

# 当再来一个人,alex再下一个蛋你拿走,而不需要等着了。
print(alex_lmj.__next__())
# 结果
鸡蛋1

为啥使用生成器,生成器的优点:

Python使用生成器对延迟操作提供了支持。所谓延迟操作,是指在需要的时候才产生结果,而不是立即产生结果。这也是生成器的主要好处。

生成器总结:

1. 是可迭代对象

2. 实现了延迟计算,省内存那

3. 生成器本质和其他的数据类型一样,都实现了迭代器协议,只不过生成器附加了一个延迟计算省内存的好处,其余的可迭代对象可没有这点好处,记住哦!!

 二. 生成器总结

综上已经对生成器有了一定的认识,下面以生成器函数为例进行总结。

  • 语法上和函数类似:生成器函数和常规函数几乎是一样的。他们都是使用def语句进行定义,差别在于,生成器使用yield语句返回一个值,而常规函数使用return语句返回一个值。return只能return一次,而yield可以保存当前状态,下次开始是从yield上一次停止的位置开始,而不用从函数开头执行。
  • 自动实现迭代器协议:对于生成器,Python会自动实现迭代器协议,以便应用到迭代背景中(如for循环,sum函数)。由于生成器自动实现了迭代器协议,所以,可以调用它的next方法,并且,在没有值可以返回的时候,生成器自动产生StopIteration异常
  • 状态挂起:生成器使用yield语句返回一个值。yield语句挂起该生成器函数的状态,保留足够的信息,以便之后从它离开的地方继续执行

生成器遵循迭代器协议,是可迭代对象。 生成器就是迭代器原理其实是这个,但是不能说。

优点一:生成器的好处是延迟计算,一次返回一个结果。也就是说,它不会一次生成所有的结果,这对于大数据量处理,将会非常有用

# 列表解析
sum([i for i in range(10000000)])   # 内存占用大,机器容易卡死

# 生成器表达式
sum(i for i in range(1000000))     # 几乎不占内存

优点二:生成器还能有效提高代码可读性

# 用前面下蛋的传说例子很能说明问题

# 首先,普通的return方式,实现功能用了5行
# 而,生成器函数则3行就实现了功能。

这里,至少有两个充分的理由说明,使用生成器比不使用生成器代码更加清晰:

  1. 使用生成器以后,代码行数更少。要记住,如果想把diam写成Pythonic,在保证代码可读性的前提下,代码行数越少越好
  2. 不使用生成器的时候,对于每次结果,首先看到的是result.append(index),其次,才是index。也就是说,每次看到的是一个列表的append操作,只是append的是我们想要的结果。使用生成器的时候,直接yield index,少了列表append操作的干扰,一眼就能看出,代码是要返回index。

这例子充分说明,合理使用生成器,能够有效提高代码可读性。只要完全接受了生成器的概念,理解了yield语句和return语句一样,也返回一个值。那么,就能理解为什么使用生成器比不使用生成器更好,能够理解使用生成器真的可以让代码变得更清晰易懂。

注意事项:生成器只能遍历一次(母鸡一生只能下一定数量的蛋,下完就不下了或者死掉了)

人口普查文件:
{'name':'北京','population':10}
{'name':'山东','population':1000000}
{'name':'山西','population':31}
{'name':'河北','population':3110330000}
{'name':'*','population':311030330}
# 需求:提取出每个省的人口数 及  总人口数
#
分析过程 def get_population(): with open('人口普查', 'r', encoding= 'utf-8') as f: for i in f: yield i g = get_population() # print(g.__next__()) # {'name':'北京','population':10}, # print(type(g.__next__())) # <class 'str'> # print(g.__next__()['population']) # 返回的是一行文件的记录,文件记录全都是字符串,所以会报TypeError: string indices must be integers # 前面内置函数学过一个eval()函数,可以把字符串内的数据类型进行运算,提取出数据类型 sheng1 = eval(g.__next__()) print(sheng1) # {'name': '北京', 'population': 10} print(type(sheng1)) # <class 'dict'> # 获取总人口 all_pop = sum(eval(i)['population'] for i in g) print(all_pop)
# 完整的代码及结果
def get_population():
    with open('人口普查', 'r', encoding= 'utf-8') as f:
        for i in f:
            yield i

g = get_population()
print("北京人口总数 %s " %eval(g.__next__())['population'])
print("山西人口总数 %s " %eval(g.__next__())['population'])
print("山东人口总数 %s " %eval(g.__next__())['population'])
print("河北人口总数 %s " %eval(g.__next__())['population'])
print("*人口总数 %s " %eval(g.__next__())['population'])
# 结果
北京人口总数 2000 
山西人口总数 100 
山东人口总数 310 
河北人口总数 210 
*人口总数 110 

# 获取总人口
all_pop = sum(eval(i)['population'] for i in g)
print(all_pop)
# 2730

生产者及消费者模型

#  以前可能会这么做
import time
def producer():
    ret=[]
    for i in range(100):
        time.sleep(0.1)
        ret.append('包子%s' %i)
    return ret

def consumer(res):
    for index,baozi in enumerate(res):
        time.sleep(0.1)
        print('第%s个人,吃了%s' %(index,baozi))

res=producer()
consumer(res)

先学习三种触发生成器函数往下走的方法

#yield 3相当于return 控制的是函数的返回值
# yield的另外一个特性,接受send传过来的值,赋值给x, x=yield
def test():
    print('开始啦')
    yield 1 #return 1   first=None
    print('第一次')
    yield 2
    print('第二次')
    third = yield
    print('第三次',third)
    yield 3
    print('第四次')

t=test()
# 第一种:直接调生成器下面的__next__()方法
res1=t.__next__()    # 第一次触发yield 后就停住了
print(res1)
# 结果
# 开始啦
# 1       # yield 1的返回值

# 第二种:用系统提供的内置函数next()函数+生成器函数
res2 = next(t)      #  第二次触发yeild 往下继续运行, 运行到yield 2 后就停住了
print(res2)
# 结果
# 第一次   # 从 yield 1 后面开始运行
# 2       # yield 2 的返回值


# 第三种:生成器下面的send()方法,功能同next,都可以让函数往下走
# 同时,send 还有一个功能,就是可以传递一个参数给yield,进行触发,保证函数能够继续往下走。
res=t.send(None)
print(res)
# 结果
# 第二次
# None

res3=t.send('函数停留在third那个位置,我就是给third赋值的')    # 第三次通过send触发生成器继续运行
print(res3)                                                # 运行到 third = yield 时,停住了,没有返回值,所以返回None
# 结果
# 第三次 函数停留在first那个位置,我就是给first赋值的
# 3

怎么实现并发的效果?
两个程序同时在等,A处理完了跳到B,B处理完了再跳到B,两个没有先后顺序

消费者生产者模型

# 消费者
def consumer(name):
    print('我是[%s],我准备开始吃包子了' %name)
    while True:         # 死循环
        baozi=yield     #
        time.sleep(1)
        print('%s 很开心的把【%s】吃掉了' %(name,baozi))

# 生产者
def producer():
    c1=consumer('wusir')
    c2=consumer('yunyuan')
    c1.__next__()
    c2.__next__()
    for i in range(10):
        time.sleep(1)
        c1.send('包子 %s' %i)
        c2.send('包子 %s' %i)
producer()