python进阶系列 - 13讲 生成器generator

60 阅读2分钟

生成器可以暂停和恢复的函数,返回一个可迭代的对象

那为啥我们需要一个生成器了?

生成器不像列表,本质是懒加载的,只在需要时才会生成元素。 所以,当处理大型数据集时,生成器会更加有效。 生成器也是普通函数,仅仅使用yield语句代替return而已。

简单的例子:

def my_generator():
    yield 1
    yield 2
    yield 3

print(my_generator)
for i in my_generator():
    print(i)

结果:

<function my_generator at 0x7fc138c900e0>
1
2
3

执行生成器函数

使用生成器函数时,需要使用next()函数来获取下一个值。

当调用next()第一次时,执行从函数开始到第一个yield语句的位置,并继续执行直到第一个yield语句的右边的值被返回。

如果一直调用next()但已经没有对应的yield,那么会抛出StopIteration异常,相当于生成器已经没有数据了,但还在试图获取数据,则直接报错了!

代码:

def countdown(num):
    print('Starting')
    while num > 0:
        yield num
        num -= 1
cd = countdown(3)
print(next(cd))
print(next(cd))
print(next(cd))
print(next(cd))

结果:

Starting
3
2
1
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
/var/folders/p9/ny6y7zmj0qdblv697s3_vj040000gn/T/ipykernel_99634/880898684.py in <module>
      8 print(next(cd))
      9 print(next(cd))
---> 10 print(next(cd))

StopIteration: 

生成器用于循环

可以直接用for进行遍历,直到无元素返回。

代码:

def countdown(num):
    print("Starting")
    while num > 0:
        yield num
        num -= 1

cd = countdown(3)
for x in cd:
    print(x)

结果:

Starting
3
2
1

生成器用于迭代

我们可以将生成器,类比为列表,使用相关的内置函数如sumsorted一样可以很好的工作。如下代码:

def countdown(num):
    print("Starting")
    while num > 0:
        yield num
        num -= 1

cd = countdown(3)
# 直接调用sum
sum_cd = sum(cd)
print(sum_cd)

cd = countdown(3)
# 直接调用sorted
sorted_cd = sorted(cd)
print(sorted_cd)

结果:

Starting
6
Starting
[1, 2, 3]

生成器的优势:节省内存!

既然生成器的值是懒加载的,那么它就可以节省内存。

例如,当需要时,生成器才会需要的数据,后续数据还未开始进行计算处理,所以生成器可以在所有元素生成之前开始使用。

我们通过两个例子,看对比使用与不用生成器是的内存 对比。

不使用生成器

下面代码计算[0-1000000)的和,但中间使用列表来存储所有数字,如下代码:

def firstn(n):
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
    return nums

sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")

结果:

499999500000
8697472 bytes

使用生成器

类似上面使用列表,这次我们改为yield来返回数字,如下代码:

def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")

结果:

499999500000
128 bytes

看到了吗?内存占比只有128 bytes 了?神奇不

实例: 斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……。

我们用yield来返回每个斐波那契数,如下代码:

def fibonacci(limit):
    a, b = 0, 1 # 初始值
    while a < limit:
        yield a
        a, b = b, a + b
fib = fibonacci(30)
print(list(fib))

结果:

[0, 1, 1, 2, 3, 5, 8, 13, 21]

生成器表达式

就像列表推导一样,生成器也可以使用同样的语法,只是用圆括号()而不是方括号[]

小心不要混淆它们,因为生成器表达式比列表推导式慢得多,因为它们调用函数的时间开销比列表推导多得多。

示例代码:

import sys
# 生成器表达式
mygenerator = (i for i in range(1000) if i % 2 == 0)
print(sys.getsizeof(mygenerator), "bytes")

# 列表表达式
mylist = [i for i in range(1000) if i % 2 == 0]
print(sys.getsizeof(mylist), "bytes")

结果:

120 bytes
4272 bytes

生成器的概念

我们也可以将类实现为一个可迭代对象, 但必须实现__iter____next__从而可以跟踪当前状态(这里是当前数字),并且处理StopIteration

看下面的代码,可以很好地理解生成器的内部原理:

class firstn:
    def __init__(self, n):
        self.n = n
        self.num = 0
    def __iter__(self):
        return self
 
    # __next__ 方法
    def __next__(self):
        if self.num < self.n:
            cur = self.num
            self.num += 1
            return cur
        else:
            raise StopIteration()
firstn_object = firstn(1000000)
print(sum(firstn_object))

结果:

499999500000

小节

生成器函数,外表看上去像是一个函数,但是没有用return语句一次性的返回整个结果对象列表,取而代之的是使用yield语句一次返回一个结果。

生成器函数返回一个迭代器,for循环等迭代环境对这个迭代器不断调用next函数,不断的运行到下一个yield语句,逐一取得每一个返回值,直到没有yield语句可以运行,最终引发StopIteration异常。

希望大家能理解到生成器的精髓!

感谢你的阅读。欢迎大家点赞、收藏、支持!

pythontip 出品,Happy Coding!

公众号: 夸克编程

我们的小目标: 让天下木有难学的Python