Jean's Blog

一个专注软件测试开发技术的个人博客

0%

yield关键字和生成器,nonlocal关键字和global关键字使用总结

yield关键字

要通俗理解yield,可结合函数的返回值关键字return,yield是一种特殊的return。说的特殊的return,是因为执行遇到yield时,立即返回,这与return的相似之处。不同之处在于:下次进入函数直接到yield的下一个语句,而return后在进入函数,还是从函数体的第一行代码开始执行。

带yield的函数时生成器,通常与next函数结合用。下次进入函数,意思是使用next函数进入到函数体内。

下面,举个例子说明yield的基本用法。

1
2
3
4
5
6
7
8
>>> def f():
... print('enter f...')
... return 'hello'
...
>>> ret = f()
enter f...
>>> ret
'hello'

上面定义了普通函数f,f()就会立即执行函数。但是,我们在新定义函数f,带有yield,是生成器函数f:

1
2
3
4
5
6
7
>>> def f():
... print('enter f...')
... yield 4
... print('i am next sentence of yield')
...
>>> g = f()
>>>

执行f(),并未打印任何信息。但是得到了一个生成器对象g,再执行next(g),输出下面信息:

1
2
3
>>> next(g)
enter f...
4

再执行next(g)时,输出下面信息

1
2
3
4
5
>>> next(g)
i am next sentence of yield
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

输出信息 i am next sentence of yield,表明它直接进入 yield 的下一句。

同时,抛出一个异常 StopIteration,意思是已经迭代结束。

此处确实已经迭代结束,所以此异常无需理会。还可以捕获此异常,以此为依据判断迭代结束。

yield与生成器

函数带有yield,就是一个生成器,英文generator,它的重要优点之一节省内存。查看下面的例子:

  1. 定义一个函数

    1
    2
    3
    4
    5
    def myrange(stop):
    start = 0
    while start < stop:
    yield start
    start += 1
  2. 使用生成器myrange

    1
    2
    for i in myrange(10):
    print(i)
  3. 返回结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/yield_sample_01.py
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9

    Process finished with exit code 0

    整个过程空间复杂度都是O(1),这得益于yield关键字,遇到就返回且再进入执行下一句的机制。

如果不使用yield,也就是使用普通方法,如下定义myrange:

1
2
3
4
5
6
7
8
9
10
11
def myrange(stop):
start = 0
result = []
while start < stop:
result.append(start)
start += 1
return result


for i in myrange(10):
print(i)

不用yield关键字,创建一个序列myrange函数,空间复杂度都是O(n)。

我们想查看下演示过程,可将代码贴到pythontutor中一步一步执行展示,网址:https://pythontutor.com

send函数

带yield的生成器对象里还封装了一个send方法。下面的例子:

1
2
3
4
5
6
7
8
9
def f():
print('enter f...')
while True:
result = yield 4
if result:
print('send me a value is:%d' % (result,))
return
else:
print('no send')

如下调用:

1
2
3
4
g = f()
print(next(g))
print('ready to send')
print(g.send(10))

执行结果

1
2
3
4
5
6
7
8
9
10
11
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/yield_sample_03.py
enter f...
4
ready to send
send me a value is:10
Traceback (most recent call last):
File "/Users/lvjing/PycharmProjects/python_base_project/yield_sample_03.py", line 15, in <module>
print(g.send(10))
StopIteration

Process finished with exit code 1

分析输出的结果:

  • g = f():创建生成器对象,什么都不打印
  • print(next(g)): 进入f,打印enter f...,并yield后返回值4,并打印4
  • print('ready to send')
  • print(g.send(10)):send值10赋给result,执行到上一次yield语句的后一句打印出send me a value is:10
  • 遇到return后返回,因为f是生成器,同时提示StopIteration

通过以上分析,可以体会到send函数的用法:它传递给yield左侧的result变量。

return后抛出迭代终止的异常,此处可看做是正常的提示。

更多使用yield案例

1.完全展开list

下面的函数deep_flatten定义中使用了yield关键字,实现嵌套list的完全展开。

1
2
3
4
5
6
def deep_flatten(lst):
for i in lst:
if type(i) == list:
yield from deep_flatten(i)
else:
yield i

deep_flatten函数,返回结果为生成器,如下所示:

1
2
3
4
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/yield_sample_04.py
<generator object deep_flatten at 0x10532d510>

Process finished with exit code 0

返回的gen生成器,与for结合

1
2
3
4
gen = deep_flatten([1, ['s', 3], 4, 5])

for i in gen:
print(i)

打印结果为:

1
2
3
4
5
6
7
8
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/yield_sample_04.py
1
s
3
4
5

Process finished with exit code 0

yield from表示再进入到deep_flatten生成器。

下面为返回值s的帧示意图:

image-20220420120738407

2.列表分组

1
2
3
4
5
6
7
8
9
10
11
from math import ceil


def divide_iter(lst, n):
if n <= 0:
yield lst
return
i, div = 0, ceil(len(lst) / n)
while i < n:
yield lst[i * div:(i + 1) * div]
i += 1

调用

1
2
print(list(divide_iter([1, 2, 3, 4, 5], 0)))
print(list(divide_iter([1, 2, 3, 4, 5], 2)))

结果展示

1
2
3
4
5
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/yield_sample_05.py
[[1, 2, 3, 4, 5]]
[[1, 2, 3], [4, 5]]

Process finished with exit code 0

这是一个很经典yield使用案例,优雅的节省内存,做到O(1)空间复杂度。

nonlocal关键字

关键字nonlocal常用与函数嵌套中,声明变量为非局部变量

如下,函数f里嵌套一个函数auto_increase。实现功能:不大于10时自增,否则置零后,再从零自增。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def f():
i = 0

def auto_increase():
if i >= 10:
i = 0
i += 1

ret = []
for _ in range(28):
auto_increase()
ret.append(i)
print(ret)


f()

调用函数 f,会报出如下异常:

1
2
3
4
5
6
7
8
9
10
11
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/nonlocal_sample_01.py
Traceback (most recent call last):
File "/Users/lvjing/PycharmProjects/python_base_project/nonlocal_sample_01.py", line 16, in <module>
f()
File "/Users/lvjing/PycharmProjects/python_base_project/nonlocal_sample_01.py", line 11, in f
auto_increase()
File "/Users/lvjing/PycharmProjects/python_base_project/nonlocal_sample_01.py", line 5, in auto_increase
if i >= 10:
UnboundLocalError: local variable 'i' referenced before assignment

Process finished with exit code 1

在IDE中,会直接提示报错

image-20220420131240804

我们看到if i >= 10折行报错,i引用前未被赋值。

为什么会这样呢?明明i一开始已经就被定义!原来,最靠近变量的i的函数时auto_increase,而不是f,i并没有在auto_increase中赋值,所以报错。

解决方法:使用nonlocal声明i不是auto_increase内的局部变量。

因此修改方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def f():
i = 0

def auto_increase():
# 使用nonlocal告诉编译器,i不是局部变量
nonlocal i
if i >= 10:
i = 0
i += 1

ret = []
for _ in range(28):
auto_increase()
ret.append(i)
print(ret)


f()

调用f(),正常输出结果。

1
2
3
4
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/nonlocal_sample_01.py
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8]

Process finished with exit code 0

global关键字

为什么要有global关键字呢?一个变量被多个函数引用,想让全局变量被所有函数共享。可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
i = 5


def f():
print(i)


def g():
print(i)
pass


f()
g()

f和g两个函数都能共享变量i,而且程序没有报错:

1
2
3
4
5
/Users/lvjing/PycharmProjects/python_base_project/venv/bin/python /Users/lvjing/PycharmProjects/python_base_project/global_sample_01.py
5
5

Process finished with exit code 0

从上面看出,全局变量这种方式是可以的,global的价值还没有看出来,但是我们在函数中将i的值进行修改,例如实现递增,如下:

1
2
3
4
def h():
i += 1

h()

此时执行程序,就会报错。我们使用IDE,就会提示错误信息

image-20220420133102837

原来编译器在解释 i+=1 时,会解析 i 为函数 h() 内的局部变量。很显然,在此函数内,解释器找不到对变量 i 的定义,所以报错。

这时,就要用上global关键字了。在函数h内,显示的告诉解释器i为全局变量,然后,解释器会在函数外面寻找i的定义,执行完i+=1后,i还为全局变量,值加1:

1
2
3
4
5
6
7
def h():
global i
i += 1


h()
print(i)