python生成器

前言

        前面我们了解了python的迭代器,知道了迭代器的工作原理和使用方式。在python中生成器和迭代器的功能差不多,都可以被next函数迭代,都可以被for循环使用。因为生成器的本质还是一个迭代器,生成器也是一种迭代器类型,生成器类中也定义了__iter__方法和__next__方法。返回生成器同样要使用__iter__方法,迭代生成器同样要使用__next__方法。生成器就像整型、字符串等类型一样,在python中已经内置了,我们直接使用就行了不用费力的去写一个类型,但你非要自己去实现一个整型类、字符串类、生成器类也是可以的。所以生成器就是迭代器的一种实现方式(升级版),因为定义一个迭代器需要创建一个类,还要在类中实现__iter__方法和__next__方法;而定义一个生成器只需创建一个函数就行了,大大简化了实现迭代对象的成本和流程。

基本结构

def xxx():
    逻辑语句(可有可无)
    yield 对象
    逻辑语句(可有可无)

        如果一个函数中包含了yield关键字,那它就是一个生成器。生成器不能像函数一样执行,因为生成器执行时就像类的实例化一样,返回的是一个(生成器)对象。请看如下代码:

def generator():
    yield 1


print(generator())

执行结果如下:

我们可以看到生成器执行后得到的是一个对象,而且这个对象的类型是生成器类型。跟类的实例化一模一样,其实就是实例化出了一个生成器对象,所以我们使用生成器要像使用一个类对象一样,要用变量来接收生成器对象。

        想要迭代生成器,必须使用next函数或者send方法。next函数我们知道是python中用来迭代对象的内置函数;send方法是生成器中的实例方法,它能给生成器传值同时也会执行生成器。

yield关键字

        在函数中yield可以像return一样返回对象,return返回对象后函数就结束了,但yield返回对象后函数不会结束,会保持在yield返回对象这行语句上。这时的函数处于一种既不是激活又不是结束的挂起状态,这时我们可以去执行其他的代码,其他代码执行完了还可以回来接着执行yield后面的代码。我们平时执行代码都是一个函数执行完了才能执行下一个函数,为什么生成器可以可以不用执行完就执行其他函数。其实这也很好理解,我们知道生成器看似是一个函数,执行后得到的却是一个生成器对象,而生成器的本质就是迭代器的一种实现方式。我们在定义迭代器时会设置某些属性来记录迭代状态,使迭代器在迭代一次后可以去执行其他代码,执行完其他代码后可以再回来接着迭代。你只要把yield想成是在改变生成器中的某个属性,从而记录迭代状态,就很好理解了。同时你一定不要把生成器想象成一个函数,虽然是用函数来定义生成器,但本质是迭代器对象。

        当我们第一次用next函数迭代生成器时,会从生成器的第一行代码开始执行,直到遇见yield关键字时返回对象记录迭代状态并停止执行后面的代码。当我们第二次用next函数迭代生成器时,才会继续执行yield后面的代码,直到遇到下一个yield或者结束迭代。如果有下一个yield就会返回对象记录迭代状态并停止执行后面的代码,如果没有下一个yield就结束迭代。

def generator():
    print('return 1')
    yield 1
    print('return 2')
    yield 2
    print('over')


number_generator = generator()  # 生成器实例化
next(number_generator)  # 第一次迭代生成器
print('执行其他代码')
next(number_generator)  # 第二次迭代生成器
print('执行其他代码')
next(number_generator)  # 第三次迭代生成器

执行结果如下:

前两次迭代都没有问题,第三次迭代就抛出了StopIteration错误。因为生成器中只有两个yield,所以它只能被成功迭代两次,迭代第三次时就会抛出StopIteration错误终止迭代。

        如果我们需要生成器迭代1000次,那么就需要使用1000个yield。如果我们在一个函数中写1000个yield,那这个函数将非常庞大,庞大到我们不想使用生成器。所以生成器不适用于返回很多没有规律的数据,更适用于返回一系列有规律的数据,这一点迭代器也一样。如果是有规律的数据我们就可以使用while循环或者for循环,再通过规律中提取出来的算法就能不断返回数据,而且定义生成器的函数也不用很庞大。例如斐波那契数列的生成:

def fibonacci(number: int):
    """
    生成number个斐波那契数\n
    :param number: 个数
    :return: Generator
    """
    if number < 1:
        raise ValueError('number cannot be less than 1')
    yield 1
    front = 0
    back = 1
    state = 1
    while state < number:
        yield front + back
        front, back = back, front + back
        state += 1


for i in fibonacci(20):
    print(i, end=' ')

执行结果如下:

例如生成一系列素数:

def primes_generator(n):
    """
    素数生成器\n
    :param n: 生成的最后一个素数小于等于n
    :return: Generator
    """
    yield 2
    primes = list(range(3, n + 1, 2))
    while primes:
        yield primes[0]
        primes = [i for i in primes if i % primes[0] > 0]


for i in primes_generator(100):
    print(i, end=' ')

 执行结果如下:

实例方法

        生成器中提供了3个实例方法,分别是send、close和throw。

send方法

        send方法是生成器类中的实例方法,send和next都可以迭代生成器。它们的区别是send可以接收一个参数,并把这个参数传递给yield语句,所以yield语句前面是可以存在变量和赋值运算符的。就像下面这样:

def xxx():
    逻辑语句(可有可无)
    value = yield 对象
    逻辑语句(可有可无)

因为yield语句需要在执行结束后才能把send传递过来的值赋给变量,所以使用send第一次迭代生成器时必须传入None。如果第一次迭代传入的值不为None,就需要完成赋值。但是yield不能立即执行完,必须等到下一个send或next才能执行结束,所以此时是无法完成赋值的。所以python规定使用send第一次迭代生成器时必须传入None。就相当于第一个send是用来启动第一个yield的,第一个yield并不能接收第一个send传过来的值。请看如下代码:

def generator():
    print('start')
    value = yield 1
    print(f'第一个yield结束: value={value}')
    value = yield 2
    print(f'第二个yield结束: value={value}')


number_generator = generator()
number_generator.send('hello')
print(f'返回值 number={number}')

执行结果如下:

我们第一次迭代就用send传入非None的值,python直接报错了连生成器的第一行代码都不会执行,报错显示无法将非None变量发送到刚启动的生成器。所以我们第一次迭代生成器要么直接使用next,要么就使用send(None)。

def generator():
    print('start')
    value = yield 1
    print(f'第一个yield结束: value={value}')
    value = yield 2
    print(f'第二个yield结束: value={value}')


number_generator = generator()
number = number_generator.send(None)
print(f'返回值 number={number}')
number = number_generator.send('hello')
print(f'返回值 number={number}')
number = number_generator.send('world')

执行结果如下:

现在就可以正常迭代了。

close方法

        close方法是生成器类中的实例方法,是用来关闭生成器的,可以在生成器未迭代完之前就直接跳转到迭代结束的状态。

def generator():
    print('start')
    value = yield 1
    print(f'第一个yield结束: value={value}')
    value = yield 2
    print(f'第二个yield结束: value={value}')


number_generator = generator()
number = number_generator.send(None)
print(f'返回值 number={number}')
number_generator.close()  # 提前结束迭代
number = number_generator.send('hello')
print(f'返回值 number={number}')

执行结果如下:

我们可以看到close提前把生成器的状态改为了结束状态,我们想要继续迭代时就会抛出StopIteration错误,说明生成器的迭代周期已经结束。

throw方法

        throw方法是生成器类中的实例方法,用来在生成器中抛出指定的错误。throw可以接收3个参数,第一个为错误类型、第二个为错误信息、第三个为错误回溯类型。

        错误类型就是Exception以及它的子类ValueError、TypeError、StopIteration等等,或者我们通过继承Exception来自定义的错误类型。

        错误信息就是一段字符串,用来告诉别人为什么要抛出错误,解释程序出错的原因。

        错误回溯类型就是通过获得堆栈信息,在抛出错误时得到错误的代码行、错误类型、错误信息,这个我们不用传参,就用python默认的错误回溯就行。

        我们可以使用throw方法抛出错误,达到跳过某些迭代值的作用。

import sys
import traceback


def generator():
    n = 1
    while True:
        try:
            yield n
        except Exception:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            traceback.print_exception(exc_type, exc_value, exc_traceback)
        n += 1


number_generator = generator()
print(next(number_generator))
number_generator.throw(ValueError, '故意抛出错误')
print(next(number_generator))
print(next(number_generator))

执行结果如下:

通过故意抛出错误就把2给跳过了,但是要记得捕获yield语句的错误,不然程序就直接停止了。

结语

        其实生成器也不是什么复杂的东西,真正复杂的是能持续产生数据的算法。只要我们能从一些事物之间找到他们的联系,我们再把这种联系转换成一种算法,就可以根据一种事物推算出另一种事物,再从另一种事物推算出下一种事物,一直推算到算法的终结。就像科学家每天都在不断的研究,其本质就是为了找出万事万物之间的联系。爱因斯坦穷其一生都想找出能统一解释宇宙的规律,最后推出了相对论。到了现在科学家们又推出了弦理论,如果有一天我们真能找到一种能和万事万物都产生联系的算法,那么我们只需要一个最基本的变量,就能推算出万事万物了,那不就又是一个宇宙了吗?或许我们的宇宙就是如此,只是不知这个基本变量是什么呢,光子、弦、暗物质?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值