(转)python中生成器与迭代器到底有什么区别?

链接:https://blog.csdn.net/weixin_44706915/article/details/116702292

1. 迭代器

我们先来看一段代码

list1 = [1,2,3]
for i in list1:
    print(i)

-------------
1
2
3

这是初学者也能一眼看懂得代码,可是这个是怎么实现的呢。

是因为在python中几乎所有的容器都有__iter__内置函数,而这些函数都会返回一个迭代器。

我们对迭代器有两点要求。或者说迭代器应该具备迭代器协议。

  • 1.该对象需提供next方法,返回迭代器的下一项
  • 2.如果没有下一项即迭代完成时会抛出一个StopIteration异常。

下面我们来看一段代码

iterator_1 = iter(list1)
iterator_2 = iter(list1)
next(iterator_1)
>1
next(iterator_1)
>2
next(iterator_1)
>3
next(iterator_1)
> StopIteration                             Traceback (most recent call last)
<ipython-input-18-4dd2bd2ae244> in <module>
----> 1 next(iterator_1)
StopIteration: 

我们可以看出迭代器的元素只迭代一次,当继续迭代时则会抛出StopIteration异常

print("迭代器二", next(iterator_2))
for i in iterator_2:
    print(i)

---------------
迭代器二 1
2
3

我们可以看出我们一同实例化的迭代器2不受影响,可以继续迭代。

又因为我们调用了next(iterator_2)已经迭代掉了一个元素,所以我们在for循环打印的只有2和3.

如果我们再次for循环会发现打印的则为空。

这下我们全明白了。

原来我们for循环列表字典等,并不是真正的for循环列表本身。而是for循环他们返回的一个迭代器对象,这些迭代器对象只能迭代一次。(事实上不这样实现可能会实现无限次迭代的死循环现象)

事实上除了for外,sum,min,max函数等内置对象都会使用迭代器协议访问对象。也就是说他们会先调用这些对象中的__iter__方法,如果没有的话则会去找__getitem__方法。

这些就是关于迭代器的知识,事实上我们真正使用中去新建迭代器的情况很少。大多数情况下我们也只会是在类中定义__getitem__方法使元素变得可迭代,而把其他的交给迭代器协议。

 


 

2. 生成器

我们先来看一段代码

def generator_test(n):
    for i in range(n):
        yield i ** 2
generator1 = generator_test(3)
next(generator1)
>0
next(generator1)
>1
next(generator1)
>4
next(generator1)
>StopIteration                             Traceback (most recent call last)
<ipython-input-39-a22adda396a1> in <module>
----> 1 next(generator1)

这时候我们会疑惑了,这不就是迭代器吗,为什么又单独称他为生成器呢。

我们先来说一下生成器

  • 1.生成器本身是一种特殊的迭代器。
  • 2.生成器会自动实现迭代器协议,也就是说只要我们 yield 后,自动就生成了 next 对象包括 StopIteration 等结构。
  • 3.生成器使用 yield 语句返回一个值。yield 语句挂起该生成器函数的状态,保留足够的信息。对生成器函数的第二次(或第n次)调用,跳转到函数上一次挂起的位置。生成器不仅“记住”了它的数据状态,生成还记住了程序执行的位置。

 


 

3. 二者区别

  • 1.迭代器是访问容器的一种方式,也就是说容器已经出现。我们是从已有元素拓印出一份副本,只为我们此次迭代使用。而生成器则是自己生成元素的。
  • 2.在用法上生成器只需要简单函数写法,配合 yield 就能实现。而迭代器真正开发中很难使用到。我们可以把生成器看做,python 给我们提供的特殊接口实现的迭代器。

如果到这里你还不明白,我们就再研究仔细一点

def generator_test(n):
    for i in range(n):
        yield i ** 2

generator1 = generator_test(3)

list1 = list(dir(generator1))       # 生成器所有的方法。dir() 可以列出对象的属性
list2 = list(dir(iter([1, 2, 3])))  # 迭代器所有的方法

# ['__del__', '__name__', '__qualname__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
print(list(filter(lambda x: x not in list2, list1)))

# ['__length_hint__', '__setstate__']
print(list(filter(lambda x: x not in list1, list2)))

gi_yieldfrom、gi_running、send、close 这些都不难看出是保存状态,挂起等的方法。这也就是我们上面所说的生成器是如何实现的。

我们在来看下生成器不含有的迭代器方法。['__length_hint__', '__setstate__']

这两个方法第一个则是迭代器数量的,第二个则是读取pickle等特殊文件的。这两个方法可能是生成器不需要的。但是也只有这两个方法。

好,我们来下个定义

生成器是实现自己独有方法的迭代器,我们可以把他看成迭代器的子类

 


 

4. 生成器的巧妙使用

4.1 生成器推导式

// 生成器表达式只需要把方括号换成原括号即可
[i**2 for i in old_list]   # 列表推导式,   []
(i**2 for i in old_list)   # 生成器表达式, () 

sum([i**2 for i in old_list])
sum(i**2 for i in old_list)

// python为了更好的封装,我们在进行sum函数时候可以省略最外层的符号
sum(i for i in range(5))
sum((i for i in range(5)))   //推荐这种写法

那么使用生成器的优势究竟在哪呢

print(sum([k for k in range(1000000000000)]))

print(sum(k for k in range(1000000000000)))

分别运行,如何查看“python”进程,
第一个一小会的时间,内存就到7,8个G,而且还持续上涨。
第二个占用很小的内存

我们可以查看一下机器的内存,第一个列表推导式的方式大概率会内存爆掉,因为我们的生成器式是使用才迭代,只迭代一次不会存在内存中。所以内存会比较稳健的求出结果。

 

4.2 生成器函数优雅在什么地方

来看下代码,很明显,使用生成器的方法。无论是可读性还是代码的优雅都要高于第一种。

# for 循环写法
def get_even_numbers(the_list):
    result_list = []
    for i in range(len(the_list)):
        if the_list[i] % 2 == 0:
            result_list.append(i)
    return result_list

# yield 写法
def get_even_numbers_by_gen(the_list):
    for i in range(len(the_list)):
        if the_list[i] % 2 == 0:
            yield i

 

4.3 yield from 方法

# for 循环 写法
def yield_for_test(*iterables):
    for i in iterables:
        for j in i:
            yield j

# yield from 写法
def yield_from_test(*iterables):
    for i in iterables:
        yield from i

if __name__ == '__main__':
    print(list(yield_for_test([1, 2, 3], 'abc')))      // [1, 2, 3, 'a', 'b', 'c']
    print(list(yield_from_test([1, 2, 3], 'abc')))     // [1, 2, 3, 'a', 'b', 'c']

 


 

5. 总结

关于可迭代对象,迭代协议,迭代器,生成器的意义

1.让 for 更通用

2.节省内存,不用一次性加载全部内容

 

posted on 2022-06-17 10:46  bruce_he  阅读(8)  评论(0编辑  收藏  举报