0
点赞
收藏
分享

微信扫一扫

『Python终结者』生成器 (Generator) 和Yield,据说学会能加薪1000

倚然君 2022-01-28 阅读 100

先来学几个英文单词,本文统一使用英文单词表示以下概念:

  • 「Iterable」:「可迭代对象」
  • 「Iterator」:「迭代器」
  • 「Generator」:「生成器」

本文的重点是「Generator」,之所以上来就说这3个概念,是因为:

我在面试Python程序员的时候,连续几个号称资深的程序员,回答Generator的问题都回答的不好。如果你能理解透彻这篇文章,对你的加薪作用可能不止一千。

虽然有标题党的嫌疑,但具有实在的意义。所以既然进来了,就耐心读下去吧。一周能理解透一个重要概念,假以时日,你就是高手。

Generator是Iterable/Iterator的简单写法

我们先用**「Iterable/Iterator」的方式来定一个「随机数生成器」,我把它命名为「Randable」**,它的功能是:

import random 
class Randable():

    def __init__(self, total):
        self.count = 0
        self.total = total

    def __iter__(self):
        return self

    def __next__(self):
        if self.count == self.total:
            raise StopIteration
        rand_num = random.randint(1, 100)
        self.count += 1
        return rand_num 

使用上面的Randable类生成88个随机数:

for i in Randable(88):
    print(i)

这是一个类,包含__init__,__iter__和__next__3个函数:

  • Randable(100)调用__init__函数创建了一个可以生成100个随机数的对象。
  • for语句循环这个对象的时候首先调用__iter__函数获取Iterator,也就是这个对象本身。
  • 然后for不停调用__next__函数做循环,直到抛出StopIteration异常。

整个过程有点小复杂,也难以理解。Generator的出现就是为了简化这种复杂的写法。

实现同样的功能,Generator只需要一个**「函数」**就够了:

def randgen(total):
    for _ in range(0, total):
        yield random.randint(1, 100)

调用过程不变:

for i in randgen(88):
    print(i)

Generator的原理

for循环过程

结合上面的Geneator的例子,我们看一下for循环的过程:

  • 调用generator函数randgen(88):,并不会马上执行函数中的代码,而是返回一个generator对象。
  • for循环通过Python内置的next函数调用这个对象,直到对象抛出StopIteration异常为止。

试验一下:

def randgen(total):
    for _ in range(0, total):
        yield random.randint(1, 100)

g = randgen(88)
print(type(g))

执行上面这段代码,会打印出:

<class 'generator'>

Generator特征

  • Generator函数中没有return语句,只有**「yield」语句。所以生成器就是:「有yield关键词的函数」**。
  • Generator也可以有return语句,return语句就相当于抛出了StopIteration异常,会结束函数。
  • 使用next()函数执行Generator中的代码,上面的for循环也是这个原理。
  • 当代码执行到**「yield」**语句的时候,yield会返回一个值给调用者,然后函数暂定在原地,等待下次调用。
  • 下次调用会从上次暂定的地方继续执行代码。这个过程会重复直到所有代码都执行完成,或者抛出了异常。

来看一个例子:

# Generator
def three_step():
  print('这是第一步,你好!!')
  yield
  print('这是第二步,你还好吗?')
  yield
  print('这是第三步,再见!')
  
s = three_step()
next(s)
next(s)
next(s)

运行一下,打印的结果如下:

---第一次调用
这是第一步,你好!!
---第二次调用
这是第二步,你还好吗?
---第三次调用
这是第三步,再见!
Traceback (most recent call last):
  File "/Users/zjueman/git/python/weixin/generator/gen.py", line 47, in <module>
    next(s)
StopIteration

说明一下:

  • yield关键词会让函数暂停,也可以没有返回值
  • 「可以把generator理解成有状态的函数」。一般的函数没有自己的状态,执行一次就结束了。但是generator有自己的状态可以被多次调用。
  • 实际上Generator背后就是一个类,所以它有状态。上一节中我们说过,Generator就是Iterable/Iterator类的一种简单写法。

Generator表达式

Genertor除了函数的写法之外,还可以用表达式的写法。它的写法和列表推导式类似,区别就是把中括号**[…]「改成小括号」(…)**。

这是一个列表推导式:

import sys
# 生成11万的数字的平方
nums_squared_list = [i * 2 for i in range(10000)]

这是Generator表达式:

import sys
# 生成11万的数字的平方
nums_squared_gen = (i * 2 for i in range(10000))

前者会在内存中生成10000个数字,放在列表中。

后者不会马上生成,当你每次用next(nums_squared_gen)函数去调用它的时候,它会生成一个并返回。

Generator的性能优势

Generator因为可以被循环,经常被拿来和list做对比。它最让人津津乐道的是它的性能优越性。

假如你开了一家汉堡店,有个大客户向你订购1000万个汉堡。你会一次性生产完这些汉堡吗?

傻的汉堡店主会这样:

聪明的汉堡店主会这样:

那如果有个需求,让你生成100亿个随机数,再求和。你会这样写吗?

# 请不要尝试下面的代码,因为你的电脑可能会卡死!!!
rand_nums = []
for i in range(1, 10000000000):
  rand_nums.append(random.randint(1, 100))

print(sum(rand_nums))

如果这样写,你的程序会在内存中生成100亿个整数,这也许会占满你的内存。

正确的写法是使用Generator,就用我们上面的randgen吧:

print(sum(randgen(10000000000)))

前面使用list的时候,要先在内存中生成100亿个数字,然后再求和,这占空间又费时间。

而用Generator是每次用到的时候才生成1个,不用那么多空间。

我们可以测试一下前面的推导式的例子中占用的内存情况:

>>> import sys
>>> nums_squared_list = [i * 2 for i in range(10000)]
>>> sys.getsizeof(nums_squared_list)
87624 # 列表推导式占用了87624字节的内存

>>> nums_squared_gen = (i ** 2 for i in range(10000))
>>> print(sys.getsizeof(nums_squared_gen))
120  #  Generator只占用了87624字节的内存

这个例子中只生成10000个数字,区别还没那么大。如果是生成100亿个数字,区别会更大,因为Generator占用的内存基本是恒定的,和数字多少无关。

如果你曾经在写代码的时候犯了**「傻汉堡店主」**的问题,那么不要羞愧,因为Python语言的设计者们都犯过这样的错误!

在Python 2中很多标准库使用列表形式,出现内存问题。所以在Python 3中很多标准库都改用了Generator。

比如:

  • range()函数在Python 2中返回的是一个列表,在Python 3中返回的是一个Genator。
  • 字符串的迭代器也是一个Generator
 print(iter('abcpython终结者2'))
print(iter(range(1, 10000)))

打印结果:

<str_iterator object at 0x7fa30ba3e3a0>
<range_iterator object at 0x7fa30ba3e2d0>

帮你熟悉Generator的几个代码例子

我们再来多看几个代码例子,有的很简单,目的是为了给你增加更多的代码感觉。

1.range是一个Generator,所以多大的range内存都不会爆

for i in range(5):
   print(i)

2.三次方生成器

def mygenerator(n):
   for i in range(1, n, 2):
      yield i**3

3.表达式形式的三次方生成器

mygenerator = (i**3 for i in range(1,10,2))

4.Generator不只是用在for循环中,我们可以手动用next()函数调用它

def mygenerator(n):
   for i in range(1, n, 2):
      yield i * (i + 1)
my_gen = mygenerator(6)
next(my_gen)
2
next(my_gen)
12
next(my_gen)
30
next(my_gen)
> StopIteration error

高级的Generator方法

普通的Generator执行到yield就暂停,可以返回一个值或者不返回。

Generator除了可以返回值,它还可以接收调用者传值进来,这就要使用send()方法。

除了send()方法,还有throws,close()方法。

能通过yield返回值,也能够通过send()接收值,这就不是普通的Generator,而是进入了协程coroutine的领域了,需要专门的文章来讲,「我们下次再终结」

基本的Generator有上面这些知识就足够了。

关于Python技术储备

学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!

一、Python所有方向的学习路线

Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

二、学习软件

工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。

三、入门学习视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

四、实战案例

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

五、面试资料

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。


这份完整版的Python全套学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

举报

相关推荐

0 条评论