python进阶:yield与yield from

时间:2022-03-14 01:11:35

yield与生成器

前面我们介绍过生成器:迭代器、可迭代对象、生成器的区别和联系

使用了 yield 的函数被称为生成器(generator)。

他最大的特点是:在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

调用一个生成器函数,返回的是一个迭代器对象。

生成器来实现斐波那契数列:

def fib(stop):
    n, a, b = 1, 0, 1
    while n < stop:
        yield b
        a, b = b, a + b
        n += 1
    return "没有数据了"

f = fib(10)  # f 是一个迭代器,由生成器返回生成

while True:
    print(next(f), end=" ")
# 当迭代遇到没有元素可取,也会抛出StopIteration异常

send

send方法 可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置。

def gen_func():
    # 1.下面这段代码可以:产出值,接收值
    # html变两用来接受send函数传进来的值
    html = yield "http://xxx.com"
    print(f"html:{html}")
    yield 1
    yield 2
    yield 3
    return "aa"


if __name__ == "__main__":
    # 启动生成器由两种方法,next(),send()
    gen = gen_func()
    # 调用next函数会执行第一个next,并把yield后的值返回,并暂停执行
    url = next(gen)
    print(url)  # http://xxx.com
    # 调用send方法,会把send的值传入生成器,被函数内部的html变量接受了
    # 并会执行到下一个yield,yield后的1被下面的aa变量接受了再次暂停
    aa = gen.send("html")
    print(f"aa:{aa}")
    # 结果如下:
    # http://xxx.com
    # html:html
    # aa:1

如果send方法没传入值,内部print的html为None,send的返回值为yield的值。

注意第一次调用send时,只能send一个None,因为一开始还没运行到yield,必须先启动一次生成器,第一种是send(None),第二种调用next():

def gen_func():
    # 1.下面这段代码可以:产出值,接收值
    html = yield "http://xxx.com"
    print(html)
    yield 1
    yield 2
    yield 3
    return "aa"

if __name__ == "__main__":
    # 启动生成器由两种方法,next(),send()
    gen  = gen_func()
    url = gen.send("html")
    print(url)
# TypeError: can't send non-None value to a just-started generator

正确如下:

def gen_func():
    # 1.下面这段代码可以:产出值,接收值
    html = yield "http://xxx.com"
    print(html)
    yield 1
    yield 2
    yield 3
    return "aa"

if __name__ == "__main__":
    # 启动生成器由两种方法,next(),send()
    gen  = gen_func()
    # 不能第一次就send一个非None的值,send(None)或next(gen)
    url = gen.send(None)
    print(url)
    # send 可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置
    url = gen.send("html")
    print(url)
	# http://xxx.com
	# html
	# 1

close

close用来关闭生成器,关闭后,再次调用next时,抛出StopIteration:

def gen_func():
    # 1.下面这段代码可以:产出值,接收值
    yield "http://xxx.com"
    yield 1
    yield 2
    yield 3
    return "aa"

if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))  
    gen.close()
    next(gen)
# 最后一次调用next时,抛出StopIteration

throw

在yield处抛出异常:

def gen_func():
    yield "http://xxx.com"
    yield 2
    yield 3
    return "aa"


if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))
    gen.throw(Exception, "download error") # 实际是在 yield "http://xxx.com"出抛异常
    print(next(gen))
    # http://xxx.com
    # in gen_func  yield 3, Exception: download error

yield from

在python3.3 新加的语法。

看下面一个例子:

from itertools import chain

my_list = [1, 2, 3]
my_dict = {"a":1, "b":2}
for val in chain(my_list, my_dict, range(5,9)):
    print(val)

上面例子主要是使用itertools工具包的chain方法把两个字典链接到一起了。

如果通过yield from也一样可以实现上面的效果:


my_list = [1, 2, 3]
my_dict = {"a": 1, "b": 2}


def my_chain(*args, **kwargs):
    """这个方法用来把若干个可迭代对象通过yield from返回"""
    for my_iterable in args:
        yield from my_iterable


for val in my_chain(my_list, my_dict, range(5, 9)):
    print(val)

yield from后面一般是一个迭代器或生成器,作用就是把课迭代对象的每个元素通过yield返回。同样的,含有yield from的函数返回的也是一个生成器。

通过yield from可以实现委派模式:

  • 调用方:调用委派生成器的客户端(调用方)代码
  • 委托生成器:包含yield from表达式的生成器函数
  • 子生成器:yield from后面加的生成器函数

如下,利用委派模式求平均数:

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        count += 1
        total += new_num
        average = total/count


# 委托生成器
def proxy_gen():
    while True:
        yield from average_gen()


# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 预激下生成器
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0


if __name__ == '__main__':
    main()

委托生成器的作用是:在调用方与子生成器之间建立一个双向通道。

所谓的双向通道是什么意思呢?

调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count

    # 每一次return,都意味着当前协程结束。
    return total,count,average


# 委托生成器
def proxy_gen():
    while True:
        # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
        total, count, average = yield from average_gen()
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))


# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)      # 结束协程
    # 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程


if __name__ == '__main__':
    main()

运行后输出

10.0
15.0
20.0
计算完毕!!
总共传入 3 个数值, 总和:60,平均数:20.0

yield from 还帮助我们做了很多异常处理:

#一些说明
"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象
"""

_i = iter(EXPR)

try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value

else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

总结

  1. 迭代器(即可指子生成器)产生的值直接返还给调用者
  2. 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。
  3. 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
  4. 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器。
  5. 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。
  6. 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。
  7. 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。