python生成器有几种写法,python生成器函数例子

大家好,小编来为大家解答以下问题,python生成器有几种写法,python生成器函数例子,今天让我们一起来看看吧!

本文部分参考:Python迭代器,生成器–精华中的精华 https://www.cnblogs.com/deeper/p/7565571.html

一 迭代器和可迭代对象

迭代器是访问集合元素的一种方式。火车头采集器AI伪原创。迭代器只能往前不会后退。迭代器的一大优点是不要求事先准备好整个迭代过程中所有的元素,仅仅在迭代到某个元素时才计算该元素,而在这之前或之后,元素可以不存在或者被销毁。这个特点使得它特别适合用于遍历一些巨大的或是无限的集合,比如几个G的文件。

特点:

a)访问者不需要关心迭代器内部的结构,仅需通过next()方法或不断去取下一个内容

b)不能随机访问集合中的某个值 ,只能从头到尾依次访问

c)访问到一半时不能往回退

d)便于循环比较大的数据集合,节省内存

e)也不能复制一个迭代器。如果要再次(或者同时)迭代同一个对象,只能去创建另一个迭代器对象。

enumerate()的返回值就是一个迭代器,我们以enumerate为例:

a = enumerate(['a','b'])

for i in range(2):    #迭代两次enumerate对象
    for x, y in a:
        print(x,y)
    print(''.center(50,'-'))

结果:

0 a
1 b
-----------------------分割线------------------------
-----------------------分割线------------------------

可以看到再次迭代enumerate对象时,没有返回值。

我们可以用linux的文件处理命令vim和cat来理解一下:

a) 读取很大的文件时,vim需要很久,cat是毫秒级;因为vim是一次性把文件全部加载到内存中读取;而cat是加载一行显示一行

b) vim读写文件时可以前进,后退,可以跳转到任意一行;而cat只能向下翻页,不能倒退,不能直接跳转到文件的某一页(因为读取的时候这个“某一页“可能还没有加载到内存中)。

正式进入python迭代器之前,我们先要区分两个容易混淆的概念:可迭代对象(Iterable)和迭代器(Iterator)

1.1可迭代对象

定义:迭代器是一个对象,不是一个函数。只要它定义了可以返回一个迭代器的__iter__方法,或者定义了可以支持下标索引的__getitem__方法,那么它就是一个可迭代对象。

注意: 集合数据类型,如list、tuple、dict、set、str都是可迭代对象Iterable,却不是迭代器Iterator。

如何判断一个对象是可迭代对象呢?可以通过collections模块的Iterable类型判断:

from collections import Iterable
print(isinstance([], Iterable))
print(isinstance({}, Iterable))
print(isinstance('abc', Iterable))
print(isinstance({'name':'join','age':23},Iterable))
print(isinstance(set([2,3]),Iterable))

结果为:

True
True
True
True
True

再看:

from collections import Iterator
print(isinstance([], Iterator))
print(isinstance({}, Iterator))
print(isinstance('abc', Iterator))
print(isinstance({'name':'join','age':23},Iterator))
print(isinstance(set([2,3]),Iterator))

结果为:

False
False
False
False
False

1.2迭代器

定义:任何实现了__iter__()和__next__()(python2中实现next())方法的对象都是迭代器,__iter__返回迭代器自身,__next__返回容器中的下一个值。

这里我们来看一下迭代器和可迭代对象的使用方法。

问题:不适用for,while以及下表索引,怎么遍历一个list?

l1=[2,3,5]
iter_l1 = l1.__iter__() 
# 或者iter(iter_l1)
# iter(iter_l1)是python的内置函数,
# l1.__iter__()调用的是l1对象的__iter__()方法。
# 下面的next()函数和__iter__()函数类似。
print(next(iter_l1)) # 或print(iter_l1.__next__()
print(next(iter_l1))
print(next(iter_l1))
print(next(iter_l1))

结果为:

2
3
5
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-126-eb58865cb644> in <module>
      4 print(next(iter_l1))
      5 print(next(iter_l1))
----> 6 print(next(iter_l1))

结论:思路就是,先得到当前对象(当前对象是可迭代对象)的迭代器,然后每次执行next(iter_l1)就可以得到当前对象的一个值。

修改一下:

l1=[2,3,5]
iter_l1 = l1.__iter__() 
for i in iter_l1:
    print(i,end=',')

结果为:

2,3,5,

再次执行for语句如下:

for i in iter_l1:
    print(i,end=',')

此次返回结果为空,可以看到迭代器只能遍历一次对象,不能重复遍历。由于for语句可以自动处理StopIteration异常,所以这里没有报出StopIteration,而是没有任何结果。

看一个例子。我们想得到Fibonacci数列,思路是定义一个可迭代对象类Fib;然后在定义一个迭代器类FibIterator,使用方式是:
① 实例化一个可迭代对象: fib = Fib()
② 得到fib的一个迭代器: fib_iter = iter(fib)
③ 然后每次调用next(fib_iter)可得到fibonacci数列中的一个数。

class FibIterator():
    '''
    定义迭代器类
    '''
    def __init__(self,num,a,b,current):
        self.num = num
        self.a = a
        self.b = b
        self.current = current
    def __iter__(self):
        return self
    def __next__(self):
        if(self.num-1>=0):
            self.num = self.num-1
            self.current = self.a
            self.a = self.b
            self.b = self.b+self.current   #以上两步赋值操作可省略中间变量直接写为self.a,self.b = self.b,self.a+self,b 
            return self.current
        else: raise StopIteration
         
class Fib:
    '''
    定义可迭代对象所属类
    '''
    def __init__(self,num): #num表示该数列的长度
        self.a = 1
        self.b = 2
        self.current=self.a
        self.num = num
    def __iter__(self):
        return FibIterator(self.num,self.a,self.b,self.current)
        
fib = Fib(20)
fib_iter = iter(fib)
for i in range(20):
    print(next(fib_iter),end=',')

结果为:

1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,

这里定义的Fibonacci类的__init__函数有一个参数num,表示一共得到几个fibonacci数。

由于可迭代对象是实现了__iter__()方法的,迭代器对象是实现了__iter__()和__next__()方法的,能否只定义一个迭代器类呢?

试一下。

class Fib:
    def __init__(self,num):
        self.num = num
        self.a = 1
        self.b = 2
        self.current = self.a
    def __iter__(self):
        return self
    def __next__(self):
        if(self.num-1>=0):
            self.num = self.num-1
            self.current = self.a
            self.a = self.b
            self.b = self.b+self.current   #以上两步赋值操作可省略中间变量直接写为self.a,self.b = self.b,self.a+self,b 
            return self.current
        else: raise StopIteration
        
fib = Fib(10)
for i in fib:
    print(i,end=',')

结果为:

1,2,3,5,8,13,21,34,55,89,

当我们再次执行for语句时,

for i in fib:
    print(i,end=',')

同上面的分析一样,结果为空。如果想再次得到fibonacci数列的前10个数,就必须重新实例化Fib对象。

结论:为什么不只保留Iterator的接口而还需要设计Iterable呢?

因为迭代器迭代一次以后就空了,那么如果list,dict也是一个迭代器,迭代一次就不能再继续被迭代了,这显然是反人类的;所以通过__iter__每次返回一个独立的迭代器,就可以保证不同的迭代过程不会互相影响。

另外,迭代器是惰性的,只有在需要返回下一个数据时它才会计算。所以,Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

下面的例子得到全体自然数(下面我们用生成器可以得到更简单的写法)。

class Natural:
    def __init__(self):
        pass
    def __iter__(self):
        return NaturalIterator()
class NaturalIterator:
    def __init__(self):
        self.beg=0
        self.current=self.beg
    def __iter__(self):
        return self
    def __next__(self):
        self.current += 1
        return self.current   

# 显示前20个自然数
n1=Natural()
n1_iter = iter(n1)
for i in range(20):
    print(next(n1_iter),end=',')

结果为:

1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,

作业:使用迭代器和可迭代对象实现得到全体fibonacci数。

二 生成器

2.1 生成器的定义和使用

定义:生成器其实是一种特殊的迭代器,它不需要再像上面的类一样写__iter__()和__next__()方法了,只需要一个yiled关键字。

生成器一定是迭代器(反之不成立)。

Python有两种不同的方式提供生成器:

  1. 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。 yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
  2. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象, 而不是一次构建一个结果列表

我们先用一个例子说明一下:

def generate_even():
    i=0
    while True:
        if i%2==0:
            yield i
        i += 1

g=generate_even()
dir(g) # 可以看到里面有__iter__()方法和__next__()方法,所以生成器也是迭代器。

for i in range(10):
    print(g.__next__(),end=',')

结果为:

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

现在解释一下上面的代码:

我们知道,一个函数只能返回一次,即return以后,这次函数调用就结束了;

但是生成器函数可以暂停执行,并且通过yield返回一个中间值,当生成器对象的__next__()方法再次被调用的时候,生成器函数可以从上一次暂停的地方继续执行,直到下一次遇到yield语句(此时会返回yield后面的值,如果有的话)或者触发一个StopIteration。

了解协同程序:
a) 生成器的另外一个方面是协同程序的概念。协同程序是可以运行的独立函数调用,可以暂停或者挂起,并从程序离开的地方继续或者重新开始。

b) 可以在调用者和被调用的之间协同程序通信。

c) 在程序暂停时可以传参:举例来说,当协同程序暂停时,我们仍可以从其中获得一个中间的返回值,当调用回到程序中时,能够传入额外或者改变了的参数,但是仍然能够从我们上次离开的地方继续,并且所有状态完整。

生成器表达式类似于列表推导式,只是把[]换成(),这样就创建了一个生成器。

gen = (x for x in range(10))

下面我们用生成器来实现前面的fibonacci数列和全体自然数。

# 生成前n个fibonacci数
def fib(n):
    a, b = 0, 1
    count=0
    while True:
        if(count>n):
            break
        count += 1
        yield b
        a, b = b, a+b

f = fib(20)
for item in f:
    print(item,end=',')

结果为:

1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,
def Natural():
    n=0
    while True:
        yield n
        n += 1
g_n1=Natural()
for i in range(20):
    print(next(g_n1),end=',')

结果为:

0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,

2.2 send方法

生成器函数最大的特点是可以接受外部传入的一个变量,并根据变量内容计算结果后返回。这是生成器函数最难理解的地方,也是最重要的地方,协程的实现就全靠它了。

看一个小猫吃鱼的例子:

def cat():
    print('我是一只hello kitty')
    while True:
        food = yield
        if food == '鱼肉':
            yield '好开心'
        else:
            yield '不开心,人家要吃鱼肉啦'

中间有个赋值语句food = yield,可以通过send方法来传参数给food,试一下:

情况1)

miao = cat()    #只是用于返回一个生成器对象,cat函数不会执行
print(''.center(50,'-'))
print(miao.send('鱼肉'))

结果:

Traceback (most recent call last):
--------------------------------------------------
  File "C:/Users//Desktop/Python/cnblogs/subModule.py", line 67, in <module>
    print(miao.send('鱼肉'))
TypeError: can't send non-None value to a just-started generator

看到了两个信息:

a)miao = cat() ,只是用于返回一个生成器对象,cat函数不会执行

b)can’t send non-None value to a just-started generator;不能给一个刚创建的生成器对象直接send值

改一下,情况2)

miao = cat()
miao.__next__()
print(miao.send('鱼肉'))

那么到底send()做了什么呢?send()的帮助文档写的很清楚,’’‘Resumes the generator and “sends” a value that becomes the result of the current yield-expression.’’’;可以看到send依次做了两件事:

a)回到生成器挂起的位置,继续执行

b)并将send(arg)中的参数赋值给对应的变量,如果没有变量接收值,那么就只是回到生成器挂起的位置

但是,我认为send还做了第三件事:

c)兼顾__next__()作用,挂起程序并返回值,所以我们在print(miao.send(‘鱼肉’))时,才会看到’好开心’;其实__next__()等价于send(None)

所以当我们尝试这样做的时候:

def cat():
    print('我是一只hello kitty')
    while True:
        food = yield
        if food == '鱼肉':
            yield '好开心'
        else:
            yield '不开心,人家要吃鱼肉啦'

miao = cat()
print(miao.__next__())
print(miao.send('鱼肉'))
print(miao.send('骨头'))
print(miao.send('鸡肉'))

就会得到这个结果:

我是一只hello kitty
None
好开心
None
不开心,人家要吃鱼肉啦

我们按步骤分析一下:

a)执行到print(miao.next()),执行cat()函数,print了”我是一只hello kitty”,然后在food = yield挂起,并返回了None,打印None

b)接着执行print(miao.send(‘鱼肉’)),回到food = yield,并将’鱼肉’赋值给food,生成器函数恢复执行;直到运行到yield ‘好开心’,程序挂起,返回’好开心’,并print’好开心’

c)接着执行print(miao.send(‘骨头’)),回到yield ‘好开心’,这时没有变量接收参数’骨头’,生成器函数恢复执行;直到food = yield,程序挂起,返回None,并print None

d)接着执行print(miao.send(‘鸡肉’)),回到food = yield,并将’鸡肉’赋值给food,生成器函数恢复执行;直到运行到yield’不开心,人家要吃鱼肉啦’,程序挂起,返回’不开心,人家要吃鱼肉啦’,并print ‘不开心,人家要吃鱼肉啦’

大功告成,那我们优化一下代码:

def cat():
    msg = '我是一只hello kitty'
    while True:
        food = yield msg
        if food == '鱼肉':
            msg = '好开心'
        else:
            msg = '不开心,人家要吃鱼啦'

miao = cat()
print(miao.__next__())
print(miao.send('鱼肉'))
print(miao.send('鸡肉'))

我们再看一个更实用的例子,一个计数器。

def counter(start_at = 0):
    count = start_at
    while True:
        val = yield count
        if val is not None:
            count = val
        else:
            count += 1

count = counter(5)
print(count.__next__())
print(count.send(0))

结果为5,0而不是5,6的原因:

①执行print(count.next()),程序运行到val = yield count(第一个yield语句)后挂起,然后返回yield后面的值,所以结果为5。

②执行print(count.send(0)),程序恢复到挂起点val = yield count,将send的参数0赋值给接受变量val,然后继续执行下面的语句,由于val=0,所以if val is not None条件为真,count = val,接着又来到yield语句(val = yield count),此时程序挂起,返回yield后面的值count=0。

综上:当执行next()方法时,程序会恢复到挂起点,依次执行yield语句下面的语句,最后再返回yield后面的值;而不是先返回yield后面的值,再执行yield后面的语句。

最后给出一张图说明Iterable,Iterator,Generator的关系:
gen2

补充几个小例子:

a)使用生成器创建一个range

def range(n):
    count = 0
    while count < n:
        yield count
        count += 1

猜你喜欢

转载自blog.csdn.net/mynote/article/details/132359010