python之关键字return和yield的区别以及详细介绍 || python之生成器及其优点

前言

 return :在程序函数中返回某个值,返回之后函数不在继续执行,彻底结束。

 yield : 带有 yield 的函数是一个生成器,函数返回某个值时,会停留在某个位置,返回函数值后,会在前面停留的位置继续执行,直到程序结束。

简单理解: yield 就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后开始。

普通函数和生成器函数

假如要创建一个返回奇数数列的函数,普通函数的做法如下:

def odd_numbers(n):
    odd_num_list = []
    for i in range(n):
        if (i % 2) == 1:
            odd_num_list.append(i)

    return odd_num_list


for j in odd_numbers(10):
    print(j)

运行结果:

1
3
5
7
9

当将上述函数改成如下形式时,普通函数便变成了一个生成器函数。这时候调用 odd_numbers(10) 不会执行 odd_numbers 函数,而是返回一个生成器对象。

def odd_numbers(n):
    for x in range(n):
        if (x % 2) == 0:
            yield x


num = odd_numbers(10)
print(num)

运行结果:

<generator object odd_numbers at 0x7f9d77a095f0>

从生成器对象获取值

为了从生成器对象获取值,我们可以使用 for loop 、 next() 或者 list() 方法。

1、 for loop 方法:

def odd_numbers(n):
    for x in range(n):
        if (x % 2) == 1:
            yield x


num = odd_numbers(10)
for i in num:
    print(i)

2、next() 方法:

def odd_numbers(n):
    for x in range(n):
        if (x % 2) == 1:
            yield x


num = odd_numbers(10)
print(next(num))
print(next(num))
print(next(num))
print(next(num))
print(next(num))

3、 list() 方法

def odd_numbers(n):
    for x in range(n):
        if (x % 2) == 1:
            yield x


num = odd_numbers(10)
for i in list(num):
    print(i)

生成器对象只能被遍历一次

生成器对象只能被遍历一次,当我们再次使用时,生成器对象为空。

def odd_numbers(n):
    for x in range(n):
        if (x % 2) == 1:
            yield x


num = odd_numbers(10)
for i in num:
    print(i)

# 再次使用 num
print(list(num))

运行结果:

1
3
5
7
9 
[]  # 从输出中可以看出,当再次使用 num 时,num 变成了空。

yield 对比 return

 yield :

包含 yield 的函数在被调用时,返回一个生成器对象给调用者,只有在遍历对象时,函数的代码才会被执行。

生成器对象不占用内存。

适合处理数据量比较大的情况。

当数据量较大时,性能较好。

 return :

return 返回一个值给调用者。

需要给返回的值分配内存。

当处理的数据量较小时,比较方便。

当数据量较大时,由于需要使用大量内存,会拖慢性能。

总结

  • 返回的生成器对象在使用过一次后便不再可以使用,如果要获取值,需要遍历生成器对象。
  • 可以使用for loopnext()或者list()方法从 生成器对象获取值。
  • yieldreturn的最大区别是,yield 返回一个生成器对象给调用者,而return返回一个值给调用者。
  • 使用yield时,不会将值存储在内存中,这在处理的数据量很大时,比较有优势。

举例

def foo():
    print("starting...")
    while True:
        res = yield 4
        print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))

运行结果:

starting...
4
********************
res: None
4

详细解释:

1、程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,而是先得到一个生成器g(相当于一个对象)。

2、直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环。

3、程序遇到yield关键字,然后把yield想想成return,return了一个4之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果。

4、程序执行 print("*"*20) ,输出20个*

5、又开始执行下面的 print(next(g)) ,这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行res的赋值操作;这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,并没有给赋值操作的左边传参数),所以这个时候res赋值是None,所以接着下面的输出就是res:None。

6、程序会继续在while里执行,又一次碰到yield,这个时候同样return 出4,然后程序停止,print函数输出的4就是这次return出的4。

python之生成器及其优点

迭代器协议

生成器自动实现了迭代器协议。

1、迭代器协议是指:对象需要提供next方法,它要么返回迭代中的下一项,要么就引起一个StopIteration异常,以终止迭代。
2、可迭代对象就是:实现了迭代器协议的对象。
3、协议是一种约定,可迭代对象实现迭代器协议,Python的内置工具(如for循环,sum,min,max函数等)使用迭代器协议访问对象。

举个例子:在所有语言中,我们都可以使用for循环来遍历数组,Python的list底层实现是一个数组,所以,我们可以使用for循环来遍历list。如下所示:

>>> for n in [1, 2, 3, 4]:
...     print n

但是,其实Python的for循环不但可以用来遍历list,还可以用来遍历文件对象,如下所示:

with open(‘/etc/passwd’) as f: # 文件对象提供迭代器协议
        for line in f: # for循环使用迭代器协议访问文件
            print line

为什么在Python中,文件还可以使用for循环进行遍历呢?

这是因为,在Python中,文件对象实现了迭代器协议,for循环并不知道它遍历的是一个文件对象,它只管使用迭代器协议访问对象即可。正是由于Python的文件对象实现了迭代器协议,才得以使用如此方便的方式访问文件,如下所示:

f = open('/etc/passwd')
dir(f)
# ['__class__', '__enter__', '__exit__', '__iter__', '__new__', 'writelines', '...'

生成器

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

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

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

生成器函数

举例:使用生成器返回自然数的平方(注意返回的是多个值):

def gensquares(N):
    for i in range(N):
        yield i ** 2

for item in gensquares(5):
    print item

使用普通函数:

def gensquares(N):
    res = []
    for i in range(N):
        res.append(i*i)
    return res

for item in gensquares(5):
    print item

生成器表达式

使用列表推导,将会一次产生所有结果:【列表生成式】

squares = [x**2 for x in range(5)]
print(squares)  # [0, 1, 4, 9, 16]

将列表推导的中括号,替换成圆括号,就是一个生成器表达式:

squares = (x**2 for x in range(5))
print(squares) # <generator object at 0x00B2EC88>
print(next(squares))  # 0
print(next(squares))  # 1
print(next(squares))  # 4
print(list(squares))  # [9, 16]

Python不但使用迭代器协议,让for循环变得更加通用。

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

sum(x ** 2 for x in xrange(4)) 

再看生成器

前面已经对生成器有了感性的认识,我们以生成器函数为例,再来深入探讨一下Python的生成器:

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

举例:

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

大家可以在自己电脑上试试下面两个表达式,并且观察内存占用情况。对于前一个表达式,我在自己的电脑上进行测试,还没有看到最终结果电脑就已经卡死,对于后一个表达式,几乎没有什么内存占用。

sum([i for i in xrange(10000000000)])
sum(i for i in xrange(10000000000))

除了延迟计算,生成器还能有效提高代码可读性

例如,现在有一个需求,求一段文字中,每个单词出现的位置。

不使用生成器的情况:

def index_words(text):
    result = []
    if text:
        result.append(0)
      for index, letter in enumerate(text, 1):
        if letter == ' ':
            result.append(index)
    return result

使用生成器的情况:

def index_words(text):
    if text:
        yield 0
    for index, letter in enumerate(text, 1):
         if letter == ' ':
            yield index

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

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

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

使用生成器的注意事项

直接举例:

我们直接来看例子,假设文件中保存了每个省份的人口总数,现在,需要求每个省份的人口占全国总人口的比例。显然,我们需要先求出全国的总人口,然后在遍历每个省份的人口,用每个省的人口数除以总人口数,就得到了每个省份的人口占全国人口的比例。

def get_province_population(filename):
     with open(filename) as f:
        for line in f:
            yield int(line)
gen = get_province_population('data.txt')
all_population = sum(gen)
#print all_population
for population in gen:
     print population / all_population

执行上面这段代码,将不会有任何输出,这是因为,生成器只能遍历一次。在我们执行sum语句的时候,就遍历了我们的生成器,当我们再次遍历我们的生成器的时候,将不会有任何记录。所以,上面的代码不会有任何输出。

因此,生成器的唯一注意事项就是:生成器只能遍历一次。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

前言

生成器是一个简单的方式来完成迭代。简单来说,Python的生成器是一个返回可以迭代对象的函数。

怎样创建生成器

在一个一般函数中使用 yield 关键字,可以实现一个最简单的生成器,此时这个函数变成一个生成器函数。
yield 与 return 返回相同的值,区别在于return返回后,函数状态终止,而yield会保存当前函数的执行状态,在返回后,函数又回到之前保存的状态继续执行。

生成器函数与一般函数的不同

1、生成器函数包含一个或者多个 yield 。

2、当调用生成器函数时,函数将返回一个对象,但是不会立刻向下执行。

3、像 __iter__() 和 __next__() 方法等是自动实现的,所以我们可以通过 next() 方法对对象进行迭代。

4、一旦函数被 yield ,函数会暂停,控制权返回调用者。

5、局部变量和它们的状态会被保存,直到下一次调用。

6、函数终止的时候,StopIteraion会被自动抛出。

举例

# 简单的生成器函数
def my_gen():
     n=1
     print("first")
     # yield区域
     yield n

     n+=1
     print("second")
     yield n

     n+=1
     print("third")
     yield n

 a=my_gen()
 print("next method:")
 # 每次调用a的时候,函数都从之前保存的状态执行
 print(next(a))
 print(next(a))
 print(next(a))

 print("for loop:")
 # 与调用next等价的
 b=my_gen()
 for elem in my_gen():
     print(elem)

运行结果:

next method:
first
1
second
2
third
3
for loop:
first
1
second
2
third
3

使用循环的生成器

# 逆序yield出对象的元素
def rev_str(my_str):
    length=len(my_str)
    for i in range(length-1,-1,-1):
        yield my_str[i]

for char in rev_str("hello"):
    print(char)

运行结果:

o
l
l
e
h

生成器的表达式

Python中,有一个列表生成方法,比如:

# 产生1,2,3,4,5的一个列表
[x for x in range(5)]

如果把[]换成(),那么会成为生成器的表达式。

print((x for x in range(5)))  # <generator object <genexpr> at 0x0000017BD3427CF0> 

举例:

a = (x for x in range(10))
b = [x for x in range(10)]

# 这是错误的,因为生成器不能直接给出长度
# print("length a:",len(a))

# 输出列表的长度
print("length b:", len(b))

b = iter(b)
# 二者输出等价,不过b是在运行时开辟内存,而a是直接开辟内存
print(next(a))
print(next(b))

为什么使用生成器

1、更容易使用,代码量较小。

2、内存使用更加高效。比如列表是在建立的时候就分配所有的内存空间,而生成器仅仅是需要的时候才使用。

3、代表了一个无限的流。如果我们要读取并使用的内容远远超过内存,但是需要对所有的流中的内容进行处理,那么生成器是一个很好的选择,比如可以让生成器返回当前的处理状态,由于它可以保存状态,那么下一次直接处理即可。

posted @ 2021-10-22 19:22  习久性成  阅读(1685)  评论(0编辑  收藏  举报