Python 学习 ---> 迭代器、生成器、yield

Python yield与实现:http://www.cnblogs.com/coder2012/p/4990834.html

官网文档:https://docs.python.org/zh-cn/3.11/glossary.html#term-iterator

1、迭代(iter)、迭代器(iterator)、generator(生成器)

为了搞清楚 yield 是用来做什么的,首先得知道 Python 中 generator(生成器) 的相关概念,要理解 generator(生成器) ,的先从 迭代(iteration)迭代器(iterator) 讲起。

迭代

迭代是重复反馈过程的活动,其目的通常是为了接近并到达所需的目标或结果。每一次对过程的重复被称为一次“迭代”,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。————  以上是 维基百科 对迭代的定义。

在 Python中,迭代 通常是通过 for ... in ...来完成的,而且只要是 可迭代对象(iterable),都能进行迭代。

示例:创建一个列表,你可以逐个遍历列表中的元素,而这个过程便叫做 迭代 。

可迭代对象(iterable)

可迭代对象(iterable) 是实现了 iterator.__iter__()方法的对象。

可迭代对象特点:一次只能返回一个成员的对象。

所有能够接受 for...in...操作的对象都是 可迭代对象,如:列表、字符串、文件等。

迭代器(iterator)

如果 可迭代对象 实现了 iterator.__iter__()方法后,又实现了 iterator.__next__()方法,那么iterator.__iter__()方法返回值 就叫做 迭代器(iterator),也叫  iterator对象,根据官方的说法,正是这个方法,实现了for ... in ...语句。

对一个iterablefor ... in ...进行迭代时,实际是先通过调用iter()方法得到一个iterator,假设叫做X。然后循环地调用X的next()方法取得每一次的值,直到iterator为空,返回的StopIteration作为循环结束的标志。for ... in ...会自动处理StopIteration异常,从而避免了抛出异常而使程序中断。如图所示

iterator.__next__()可以 显式 地获取一个元素。当调用 next()方法时,实际上产生了2个操作: 

  • 1. 更新 iterator 状态,令其指向后一项,以便下一次调用
  • 2. 返回当前结果

如果你学过 C++,它其实跟指针的概念很像(如果你还学过链表的话,或许能更好地理解)。

正是 __next__(),使得iterator能在每次被调用时,一次只能返回一个值,从而极大的节省了内存资源。另一点需要格外注意的是,iterator是消耗型的,即每一个值被使用过后,就消失了。因此,你可以将以上的操作2理解成pop

可以把 iterator 理解成保存数据的容器,但是这个容器只能遍历一次,遍历之后就变成了一个空的容器了,但不等于None。若要重复使用容器里面的数据,可以利用list()方法保存结果。

示例:

from collections.abc import Iterable, Iterator

a = [1, 2, 3]  # list 是一个 iterable
b = iter(a)    # 通过 iter()方法, 得到 iterator,iter() 实际上调用了__iter__()
print(isinstance(a, Iterable))  # True
print(isinstance(a, Iterator))  # False
print(isinstance(b, Iterable))  # True
print(isinstance(b, Iterator))  # True

# 可见, iterable是iterator,但iterator不一定是iterable

# iterator 是消耗型的,用一次少一次。对 iterator 进行遍历,iterator就空了!
# 通过 list 保存了 迭代器b数据,相当于遍历了迭代器b, 所以list(b)后,迭代器b里面数据就空了
c = list(b)
print(c)  # [1, 2, 3]

# c 已经遍历过了,所以 d 就为 空 了
d = list(b)
print(d)  # []

if b:
    # list(b)后,迭代器b里面数据就空了, 但是只是数据为空,b仍然是迭代器对象
    print(f"b 不是 None, type(b) ---> {type(b)}")
if b is None:
    print("b 是 None")

再来感受一下 next()

>>> e = iter(a)
>>> next(e)     #next()实际调用了__next__()方法,此后不再多说
1
>>> next(e)
2

itertools 模块

itertools模块包含了许多用来操作可迭代对象的函数。

想复制一个生成器?想连接两个生成器?想把多个值组合到一个嵌套列表里面?使用 map/zip 而不用重新创建一个列表?那么就:import itertools 吧。

让我们来看看四匹马赛跑可能的到达结果:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

迭代的内部机理:
        迭代是一个依赖于可迭代对象(需要实现__iter__()方法)和迭代器(需要实现__next__()方法)的过程。
        可迭代对象是任意你可以从中得到一个迭代器的对象。
        迭代器是让你可以对可迭代对象进行迭代的对象。

2、生成器

什么是 生成器 ?

常说的 "生成器",就是 "带有 yield 的函数"。

带yield的函数是一个生成器,而不在是一个函数,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。

生成器 这样一个函数它记住上一次返回时在函数体中的位置。
对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。生成器不仅 “记住” 了它的数据状态;生成器还 “记住” 了它的流控制构造。

生成器的特点:

  • 1. 生成器是一个函数,而且函数的参数都会保留。
  • 2. 迭代到下一次的调用时,所使用的参数都是第一次所保留下的,即是说,在整个所有函数调用的参数都是第一次所调用时保留的,而不是新创建的
  • 3. 节约内存
  • 一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用 yield 关键字而不是 return如果一个 def 的主体包含 yield,这个函数会自动变成一个生成器(即使它包含一个 return)。创建一个生成器就这么简单。。。

生成器 也是 迭代器,也只能对它们进行一次迭代,原因在于它们并没有将所有数据存储在内存中,而是即时生成这些数据。

生成器表达式

my_generator = (x*x for x in range(3))
for i in my_generator:
   print(i)

这一段代码和上面 迭代 那段很相似,唯一不同的地方是使用了()代替 []。但是,这样的后果是你无法对 my_generator  进行第二次遍历,因为生成器只能被使用一次:它首先计算出结果0,然后忘记它再计算出1,最后是4,一个接一个。

生成器 示例

a = (elem for elem in [1, 2, 3])
print(f"a ---> {a}")


def fib():
    a, b = 0, 1
    while True:
        yield b
        a, b = b, a + b


print(f"fib ---> {fib}")

# fib 是 一个函数,但是fib中有yield关键字,所以函数返回值是一个生成器
b = fib()
print(f"b ---> {b}")

迭代器、生成器 的 关系

其实说白了,generator 就是 iterator 的一种,以更优雅的方式实现的 iterator

官方的说法是:Python’s generators provide a convenient way to implement the iterator protocol.

你完全可以像使用 iterator 一样使用 generator,但是请记住他们两个的定义不一样:

  • 定义一个 iterator,你需要分别实现 __iter__() 方法和 __next__() 方法,
  • 但 generator 只需要一个小小的 yield ( 好吧,generator expression 的使用比较简单,就不展开讲了。)

3、yield 关键字

Python 中 生成器 是使用  yield 关键字 来实现的。

  • 1. yield 是一个用法跟 return 很相似的关键字,但是使用yield的函数返回的是一个 生成器。
  • 2. yield 可以暂停一个函数并返回中间结果。使用 yield 的函数 将 保存执行环境,即函数的参数都会保留,并且在必要时恢复。到下一次的调用时,所有参数都恢复,所使用的参数都是第一次所保留下的参数和环境,然后从先前暂停的地方开始执行,直到遇到下一个 yield 再次 暂停。

单步调试 查看 yield 执行

示例:

def func_test():
    for i in range(5):
        yield i #
        print(i + 100)


t = func_test()
for i in t:
    print(i)
    pass

可以 单步调试 上面这个代码,就可以 验证 上面 两个 特点。

前文讲到 iterator 通过 __next__()方法实现了每次调用,返回一个单一值的功能。而 yield 就是实现 generator 的 __next__()方法的关键!先来看一个最简单的例子:

>>> def g():
...     print("1 is")
...     yield 1
...     print("2 is")
...     yield 2
...     print("3 is")
...     yield 3
...
>>> z = g()
>>> z
<generator object g at 0x7f0d2387c8b8>
>>> next(z)
1 is
1
>>> next(z)
2 is
2
>>> next(z)
3 is
3
>>> next(z)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

解释:

  • 第一次调用 next()方法时,函数似乎执行到 yield 1,就暂停了。
        (执行next方法,使程序运行到 yield 处暂停并返回1,然后交出控制权,然后 next 接收控制权,并获得 yield 的返回值 1)
  • 第二次调用 next()方法时,函数从 yield 1之后开始执行的,并再次暂停。
  • 第三次调用 next()方法时,从第二次暂停的地方开始执行。
  • 第四次调用 next()方法时,抛出StopIteration 异常。

事实上,generator 确实在遇到 yield 之后暂停了,确切点说,是先返回了 yield 表达式的值,再暂停的。当再次调用 next()时,从先前暂停的地方开始执行,直到遇到下一个 yield。这与上文介绍的对iterator调用next()方法,执行原理一般无二。

有些教程里说 generator 保存的是算法,而我觉得用 中断服务子程序 来描述 generator 或许能更好理解这样你就能将 yield 理解成一个中断服务子程序的 断点没错,是中断服务子程序的断点。我们每次对一个 generator对象调用 next()时,函数内部代码执行到 "断点" yield,然后返回这一部分的结果,并保存上下文环境,"中断" 返回。

怎么样,是不是瞬间就明白了yield 的用法,

yield、next()函数、send()函数

再来看另一段代码。

>>> def gen():
...     while True:
...         s = yield
...         print(s)
...
>>> g = gen()
>>> g.send("111")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> next(g)
>>> g.send("222")
222

generator其实有第二种调用方法(恢复执行),即通过 send(value) 方法将 value 作为 yield 表达式的当前值,你可以用该值再对其他变量进行赋值,

这一段代码就很好理解了。

  • g = gen()   定义一个变量,用来保存生成器
  • g.send("111") 时,因为生成器没有执行,所以报错
  • netxt(g) ,生成器执行到 yield 处并暂停,然后返回。
  • g.send("222") ,由于yield的缘故被暂停了。此时,send(value) 方法传入的值作为 yield 表达式的值,函数中又将该值赋给了变量 s,然后 print 函数打印 s,循环再遇到 yield,暂停返回。如此循环,直到结束

注意:

调用 send(value) 时要注意,要确保 generator 是在 yield 处被暂停了,如此才能向 yield表达式传值,否则将会报错(如上所示),可通过 next()方法或 send(None)使 generator执行到 yield

单步调试 查看 next()、send() 函数 执行

再来看一段 yield 更复杂的用法,或许能加深你对 generator 的 next()与 send(value)的理解。

def echo(value=None):
    while 1:
        value = (yield value)
        print("The value is", value)
        if value:
            value += 1
            print('add +1 value', value)
        print('**************************************')


# 调用send(value)时要注意,要确保generator是在yield处被暂停了,
# 如此才能向yield表达式传值,否则将会报错
# 可通过next()方法或send(None)使generator执行到yield。
# 生成器(generator) 有两种方法 恢复执行:1. send() 方法。2. next() 方法

g = echo(1)     # 返回一个 生成器
print(next(g))  # 通过 next() 方法 使 生成器 执行到 yield 处暂停
g.send(2)       # send(value)方法传入的值作为yield表达式的值
g.send(5)
next(g)
next(g)
next(g)
"""
执行结果:
1
The value is 2
add +1 value 3
**************************************
The value is 5
add +1 value 6
**************************************
The value is None
**************************************
The value is None
**************************************
The value is None
**************************************
"""

上述代码既有 yield value 的形式,又有 value = yield 形式,看起来有点复杂。但以 yield 分离代码进行解读,就不太难了。

  • 第一次调用 next()方法,执行到 yield value表达式,保存上下文环境暂停返回 1。
  • 第二次调用 send(value)方法,从 value = yield 开始,打印,再次遇到 yield value 暂停返回。
  • 后续的调用 send(value) 或 next()都是如此。

但是,这里就引出了另一个问题,yield 作为一个暂停恢复的点,代码从 yield 处恢复,又在下一个 yield 处暂停。可见,在一次 next()(非首次) 或 send(value)调用过程中,实际上存在 2 个 yield

  • 一个作为恢复点的 yield
  • 一个作为暂停点的yield

因此,也就有 2 个 yield 表达式。send(value)方法是将值传给恢复点yield。调用next()表达式的值时,其恢复点yield的值总是为None,而将暂停点的yield表达式的值返回。为方便记忆,你可以将此处的恢复点记作当前的(current),而将暂停点记作下一次的(next),这样就与next()方法匹配起来啦。

generator还实现了另外两个方法throw(type[, value[, traceback]])close()。前者用于抛出异常,后者用于关闭generator.不过这2个方法似乎很少被直接用到,本文就不再多说了,有兴趣的同学请看这里

示例解析:

# generation.py
def gen():
    for x in range(4):
        tmp = yield x
        if tmp == "hello":
            print("world")
        else:
            print(f"12345abcd_{str(tmp)}")


c = gen()
next(c)
next(c)
c.send("python")

"""
12345abcd_None
12345abcd_python
"""

执行到 yield 时,gen 函数暂时停止并保存,返回 x 的值,同时 tmp 接收 send 的值(ps:yield x 相当于 return x ,所以第一次c.next()结果是0。第二次 next(c) 时,继续在原来暂停的地方执行,因为没有send 值,所以 tmp 为 None。next(c) 等价 c.send(None))。下次c.send(“python”),send发送过来的值,next(c) 等价 c.send(None)

了解了next()如何让包含yield的函数执行后,我们再来看另外一个非常重要的函数send(msg)。其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。因此,我们可以看做c.next() 和 c.send(None) 作用是一样的。

需要提醒的是,第一次调用时,请使用next()语句或是send(None),不能使用send发送一个非None的值,否则会出错的,因为没有Python yield语句来接收这个值。

4、协程、

理解了这些,我们就可以向协同程序发起攻击了,所谓协同程序也就是是可以挂起,恢复,有多个进入点。其实说白了,也就是说多个函数可以同时进行,可以相互之间发送消息等。  

非标准模块 multitask.py

非标准模块 下载:https://github.com/dongjiawei316/multitask

使用 multitask 的简单代码:

def tt():
    for x in range(4):
        print('tt' + str(x))
        yield


def gg():
    for x in range(4):
        print('xx' + str(x))
        yield


t = multitask.TaskManager()
t.add(tt())
t.add(gg())
t.run()

python-multitasking

下载地址:https://pypi.org/search/?q=multitask,pypi 地址:https://pypi.org/project/python-multitasking/

安装:pip install python-multitasking   。使用示例:

import multitasking
import time
import random
import requests
import signal
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# kill all tasks on ctrl-c
signal.signal(signal.SIGINT, multitasking.killall)


# or, wait for task to finish on ctrl-c:
# signal.signal(signal.SIGINT, multitasking.wait_for_tasks)


@multitasking.task  # <== this is all it takes :-)
def hello(index):
    global global_list_flag
    url = 'https://www.baidu.com/'
    print(f'{index} : {url}')
    req = requests.get(url, verify=False)
    print(f'{index} : {req.status_code}')
    global_list_flag[index-1] = 1


if __name__ == "__main__":
    count = 10
    global_list_flag = [0 for _ in range(count)]
    for i in range(0, count):
        hello(i + 1)
    # https://www.yuanrenxue.com/python/python-asyncio-demo.html
    multitasking.wait_for_tasks()

如果不是使用生成器,那么要实现上面现象,即函数交错输出,那么只能使用线程了,所以生成器给我们提供了更广阔的前景。 

如果仅仅是实现上面的效果,其实很简单,我们可以自己写一个。主要思路就是将生成器对象放入队列,执行send(None)后,如果没有抛出StopIteration,将该生成器对象再加入队列。

# python 2.X 叫 Queue
# python 3.X 叫 queue
import queue
import multitask


def tt():
    for x in range(4):
        print('tt' + str(x))
        yield


def gg():
    for x in range(4):
        print('xx' + str(x))
        yield


class Task(object):
    def __init__(self):
        self._queue = queue.Queue()

    def add(self, gen):
        self._queue.put(gen)

    def run(self):
        while not self._queue.empty():
            for i in range(self._queue.qsize()):
                try:
                    gen = self._queue.get()
                    gen.send(None)
                except StopIteration:
                    pass
                else:
                    self._queue.put(gen)


t = Task()
t.add(tt())
t.add(gg())
t.run()

当然,multitask 实现的肯定不止这个功能,有兴趣的童鞋可以看下源码,还是比较简单易懂的。

有这么一道题目,模拟多线程交替输出:

def thread1():
    for x in range(4):
        yield x


def thread2():
    for x in range(4, 8):
        yield x


threads = []
threads.append(thread1())
threads.append(thread2())


def run(threads):  # 写这个函数,模拟线程并发
    pass


run(threads)

如果上面 class Task 看懂了,那么这题很简单,其实就是考你用yield模拟线程调度,解决如下:

def thread1():
    for x in range(4):
        yield x


def thread2():
    for x in range(4, 8):
        yield x


td_list = list()
td_list.append(thread1())
td_list.append(thread2())


def run(thread_list):
    for td in thread_list:
        try:
            print(next(td))
        except StopIteration:
            pass
        else:
            thread_list.append(td)


run(td_list)

4. 总  结

  1. 可迭代对象(Iterable)是实现了__iter__()方法的对象,通过调用iter()方法可以获得一个迭代器(Iterator)。

  2. 迭代器(Iterator)是实现了__iter__()__next__()的对象。

  3. for ... in ...的迭代,实际是将可迭代对象转换成迭代器,再重复调用next()方法实现的。

  4. 生成器(generator)是一个特殊的迭代器,它的实现更简单优雅

  5. yield 是生成器实现__next__()方法的关键。它作为生成器执行的暂停恢复点,可以对yield表达式进行赋值,也可以将yield表达式的值返回。

yield 语句将你的函数转化成一个能够生成一种能够包装你原函数体的名叫生成器 的特殊对象的工厂。

当生成器被迭代时,它将会从起始位置开始执行函数一直到达下一个yield,然后挂起执行,计算返回传递给yield的值,它将会在每次迭代的时候重复这个过程直到函数执行到达函数的尾部,举例来说:

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'
for i in simple_generator():
    print i
	
输出结果为:
one
two
three

这种效果的产生是由于在循环中使用了可以产生序列的生成器,生成器在每次循环时执行代码到下一个yield,并计算返回结果,这样生成器即时生成了一个列表,这对于特别是大型计算来说内存节省十分有效。

假设你想实现自己的可以产生一个可迭代一定范围数的range函数(特指Python 2.x中的range),你可以这样做和使用:

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range
for i in myRangeNaive(10):
    print i

但是这样并不高效,原因1:你创建了一个你只会使用一次的列表;原因2:这段代码实际上循环了两次。
由于Guido和他的团队很慷慨地开发了生成器因此我们可以这样做:

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return
for i in myRangeSmart(10):
    print i

现在,每次对生成器迭代将会调用next()来执行函数体直到到达yield语句,然后停止执行,并计算返回结果,或者是到达函数体尾部。在这种情况下,第一次的调用next()将会执行到yield n并返回n,下一次的next()将会执行自增操作,然后回到while的判断,如果满足条件,则再一次停止并返回n,它将会以这种方式执行一直到不满足while条件,使得生成器到达函数体尾部。

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值