迭代器和生成器

Posted TKOP_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了迭代器和生成器相关的知识,希望对你有一定的参考价值。

迭代器和生成器

笔记为《 javascript 高级程序设计》第四版第 7 章的内容,简单学习了一下 ES6 的两个新增高级特性(迭代器和生成器)。

1 迭代器

1.1 概述

迭代( iteration )的意思是”重来“或”再来“。在软件开发领域则是按照顺序反复多次执行一段程序的意思,通常需要有明确的终止条件。ES6 新增的迭代器(iterator)和生成器(generator) 两个高级特性可以更加清晰、高效、方便地实现迭代。

1、较早的迭代是使用循环或其他辅助结构(例如 Array.prototype.forEach() )进行。

let collection = ['tkop1', 'tkop2', 'tkop3'];
for (let index = 0; index < collection.length; index++) 
	console.log(collection[index];

  • 迭代之前需要事先知道如何使用数据结构(必须知道数组类数据结构怎么获取长度和元素),但是可迭代对象的数据结构和操作方式不尽相同。
  • 遍历顺序并不是数据结构固有的。其他类型的数据不是依靠索引作为元素的遍历顺序,具有特定的隐式遍历顺序。

2、使用 ES6 特定结构

let collection = ['tkop1', 'tkop2', 'tkop3'];
collection.forEach((item, index, arr) => console.log(item));
  • 没有遍历数据时各项的获取问题(不用关心数组中每项是怎么遍历到的)。
  • 但是没有标识迭代具体的终止位置,且该辅助结构只适用于数组,回调的结构处理数据也不够灵活。

1.2 迭代器模式

1、理解可迭代对象

  • 实现了正式的 iterable 接口,而且可以通过迭代器 iterator 消费。
  • 他们包含的元素都是有限的,而且都具有无歧义的遍历顺序。

2、迭代器的概念

迭代器是按需创建的一次性对象(理解:记录迭代过程中某时刻迭代状态的对象)。每个迭代器都会关联一个可迭代对象(实现了 iterable 接口(可迭代协议)的数据结构)并暴露该对象迭代相关的 API。迭代器无需了解其关联对象的结构,只需要知道怎么取得连续的值。

1.2.1 可迭代协议

实现 iterable 接口的要求:

  • 支持迭代的自我识别能力(不太明白)。
  • 创建实现 iterator 接口的对象的能力。暴露一个用于生成迭代器的工厂函数。该属性使用 Symbol.iterator 作为键,是“默认迭代器”。

实现了iterable 接口的内置类型有字符串、数组、映射、集合、arguments 对象、NodeList 等 DOM 集合类型

// 检查某数据结构是否存在默认迭代器属性可以通过检查是否暴露 Symbol.iterator 工厂函数
let num = 404;
let obj =  name: 'tkop404' ;
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined

let str = '404';
let arr = ['tkop301', 'tkop302', 'tkop303'];
let map = new Map().set('a', 1);
let set = new Set().add('a');
console.log(str[Symbol.iterator]); // [Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]); // [Function: values]
console.log(map[Symbol.iterator]); // [Function: entries]
console.log(set[Symbol.iterator]); // [Function: values]

// 调用该工厂函数返回实现了 iterator 接口的对象。
console.log(str[Symbol.iterator]()); // StringIterator 
console.log(arr[Symbol.iterator]()); // Array Iterator 
console.log(map[Symbol.iterator]()); // MapIterator 'a' => 1
console.log(set[Symbol.iterator]()); // SetIterator 'a'

实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性(不需要显式调用工厂函数生成迭代器)。这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数创建一个迭代器。

// 接收可迭代对象的原生语言特性包括:
// for-of循环、数组结构、扩展运算符、Array.from()方法、创建集合、创建映射
// Promise.all() 和 Promise.race() 接收期约组成的可迭代对象。yield* 操作符
let arr = ['tkop301', 'tkop302', 'tkop303'];

// for-of 遍历
for (let item of arr) 
    console.log(item); // tkop301 tkop302 tkop303


// 数组结构
let [name1, name2, name3] = arr;
console.log(name2); // tkop302

// 扩展操作符
let myName = [...arr];
console.log(myName); // [ 'tkop301', 'tkop302', 'tkop303' ]

// Array.from()
let arr0 = Array.from(arr);
console.log(arr0); // [ 'tkop301', 'tkop302', 'tkop303' ]

// Set 构造函数
let set = new Set(arr);
console.log(set); // Set(3)  'tkop301', 'tkop302', 'tkop303' 

// Map 构造函数
let pairs = arr.map((item, index) => [item, index]);
console.log(pairs); // [ [ 'tkop301', 0 ], [ 'tkop302', 1 ], [ 'tkop303', 2 ] ]
let map = new Map(pairs);
console.log(map); // Map(3)  'tkop301' => 0, 'tkop302' => 1, 'tkop303' => 2 

1.2.2 迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器(对象)使用 next() 方法遍历可迭代对象中的数据。迭代器对象的几个重要特征。

1、迭代器每次调用 next() 方法都会返回一个 iteratorResult 对象通过该对象可以知道迭代器的当前位置(迭代器返回的下一个值)。iteratorResult 对象包含 done 和 value 属性,done 属性的属性值为布尔值,表示是否还可以调用 next() 获取下一个值;value 保存可迭代对象的下一个值,如果 done 为 false(可迭代数据结构已被耗尽),则 value 值为 undefined。

let arr = ['tkop301', 'tkop302', 'tkop303'];
let iter = arr[Symbol.iterator]();

console.log(iter.next()); //  value: 'tkop301', done: false 
console.log(iter.next()); //  value: 'tkop302', done: false 
console.log(iter.next()); //  value: 'tkop303', done: false 
console.log(iter.next()); //  value: undefined, done: true 
console.log(iter.next()); //  value: undefined, done: true 

2、每个迭代器都表示对可迭代对象的一次性有效遍历。各迭代器是没有联系的,他们独立地遍历可迭代对象。他们也不知道怎么获取下一个值或者可迭代对象有多大。

3、迭代器并不与可迭代对象的某个时刻快照绑定(动态),而仅仅是通过使用游标来记录遍历的历程。

let arr = ['tkop301', 'tkop302', 'tkop303'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); //  value: 'tkop301', done: false 

// 迭代期间可迭代对象被修改,迭代器也会反映相应的变化。
arr[1] = 'tkop';
console.log(iter.next()); //  value: 'tkop302', done: false 
console.log(iter.next()); //  value: 'tkop303', done: false 
console.log(iter.next()); //  value: undefined, done: true 

1.2.3 自定义迭代器

任何实现 iterator 接口的对象都可以作为迭代器使用。下面 Counter 类只能被迭代一定的次数。

class Counter 
    constructor(limit) 
        this.count = 1;
        this.limit = limit;
    
    // 可迭代对象只能够创建一个迭代器(具体体现为使用结果)
    next() 
        return this.count <= this.limit ?  value: this.count++, done: false  :  value: undefined, done: true ;
    
    [Symbol.iterator]() 
    		// 指向迭代器本身,和原生的迭代器实现一样
        return this;
    


let counter = new Counter(3);

// 如果解开这里的 for 遍历,下面的所有next()方法返回均为 value: undefined, done: true 
// for (let i of counter) 
//     console.log(i);
// 

let iter1 = counter[Symbol.iterator]();
let iter2 = counter[Symbol.iterator]();
console.log(iter1.next()); //  value: 1, done: false 
console.log(iter2.next()); //  value: 2, done: false 
console.log(iter1.next()); //  value: 3, done: false 

从上面的代码示例可以看出这个可迭代对象只能够创建一个迭代器,没有实现多个独立的迭代器,将 for-of 遍历部分的代码解开后,这个特点更加明显(这个迭代对象只会被遍历一次)。为了让一个迭代对象能够创建多个迭代器,就必须每创建一个迭代器就对应一个独立的计数器。为此可以使用闭包。

class Counter 
    constructor(limit) 
        this.count = 1;
        this.limit = limit;
    
    [Symbol.iterator]() 
        let count = 1,
            limit = this.limit;
        return 
            next() 
                return count <= limit ?  done: false, value: count++  :  done: true, value: undefined ;
            ,
        ;
    


let counter = new Counter(3);
for (let i of counter) 
    console.log(i);

let iter1 = counter[Symbol.iterator]();
let iter2 = counter[Symbol.iterator]();
console.log(iter1.next());
console.log(iter2.next());
console.log(iter1.next());

最终输出如下(眼见为实):

但是这个自定义的迭代器对象跟原生的迭代器对象还是存在不同,它创建出来的迭代器没有实现 iterable 接口(调用 Symbol.iterator 后返回的迭代器对象没有 Symbol.iterator 属性,没有返回相同的迭代器)

let arr = ['tkop301', 'tkop302', 'tkop303'];
let iter1 = arr[Symbol.iterator]();
console.log(iter1.next());
let iter2 = iter1[Symbol.iterator]();
console.log(iter2.next());
// console.log(iter1 === iter2); // true

let counter = new Counter(3);
let iter3 = counter[Symbol.iterator]();
console.log(iter3.next());
let iter4 = iter3[Symbol.iterator](); // 报错,具体看下图
console.log(iter4.next());

1.2.4 提前终止迭代器

迭代器对象中的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道他不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况有:

  • for-of 循环通过 break、continue、return 或者 throw 提前退出。(发现 continue 并没有关闭迭代器)。
  • 解构操作并未消费所有的值。
class Counter 
    constructor(limit) 
        this.limit = limit;
    
    [Symbol.iterator]() 
        let count = 1,
            limit = this.limit;
        return 
            next() 
                return count <= limit ?  value: count++, done: false  :  value: undefined, done: true ;
            ,
            return() 
                console.log('我要提前结束迭代');
                return  done: true ;
            ,
        ;
    


let counter = new Counter(3);
// 结果为1、2、'我要提前结束迭代'
for (let i of counter) 
    if (i > 2) break; // if (i > 2) return
    console.log(i);

try 
    for (let i of counter) 
        if (i === 3) throw 'err';
        console.log(i);
    
 catch (e) 

// 输出:'我要提前结束迭代'、 1、2
let [a, b] = counter;
console.log(a, b);

注意:如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。例如数组的迭代器是不能关闭的。

let arr = ['tkop301', 2, 3, 4, 5, 6];
let iter = arr[Symbol.iterator]();
iter.return = function () 
    console.log('停止遍历');
    return  done: true ;

for (let i of iter) 
    console.log(i); // 'tkop301',2,3
    if (i > 2) break; // '停止遍历'

console.log(iter.next()); //  value: 4, done: false 
for (let i of iter) 
    console.log(i); // 5,6

因为 return() 方法是可选的,所以并非所有的迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。这是因为调用 return() 不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用。

2 生成器

生成器是 ES6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种新能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程

2.1 生成器基础

生成器的形式是一个函数,声明函数时在函数名称的前面加一个星号(*)则表示它是一个生成器(生成器函数)。只要可以定义函数的地方就可以定义生成器(不能使用箭头函数)。并且标识生成器函数的星号不受两侧的空格影响。

// 生成器函数声明
function* generatorFn() ;
// 生成器函数表达式的形式
let generatorFn = function* () ;
// 作为对象字面量方法的生成器函数
let obj = 
	*generatorFn() 

// 作为实例方法的生成器函数
class MyObj 
	*generatorFn() 

// 作为静态方法的生成器函数
class MyObj 
	static *generatorFn() 

1、调用生成器函数并不会执行函数体代码,但是会返回一个与之关联的生成器对象。该生成器对象一开始处于暂停执行的状态。

2、生成器对象与迭代器对象相似,也实现了 iterator 接口,因此具有 next() 方法。调用 next() 开始执行生成器函数的函数体,并使得生成器变成下一个状态。

3、生成器调用 next() 方法的返回值与迭代器类似。done 属性表示生成器的状态,函数体执行完成(return)时,它的属性值会变为 true。故函数体为空(或者没有特定中断指令)的生成器函数中间不会停留,调用一次 next() 就会让生成器对现象状态变为 done: true。

4、value 属性则是生成器函数当前状态下的返回值,没有则默认返回 undefined 。

5、生成器对象实现了 iterator 接口,他们默认的迭代器是自引用。

function* generatorFn() 
    console.log('初次调用不会执行函数体,只会返回一个生成器对象');
    return 'tkop';


let generatorObj = generatorFn(); // 这里没有任何控制台打印内容
console.log(generatorObj); // generatorFn <suspended>
console.log(generatorObj.next()); 
// '初次调用不会执行函数体,只会返回一个生成器对象'
// value: 'tkop', done: true

let iteratorObj = generatorObj[Symbol.iterator]();
console.log(generatorObj === iteratorObj); // true

2.2 通过 yield 中断执行

前面在 next() 方法的返回部分提到生成器函数体内部没有中断指令时,执行 next() 不会在该函数内部停留。现在 yield 关键字则可以让生成器停止或者执行(实现生成器函数执行过程中在特定位置中断执行或者接着继续执行)

2.2.1 yield 关键字基本语法

生成器函数在生成器调用 next() 方法后,在遇到 yield关键字之前会正常执行。遇到这个关键字后则会停止执行,函数作用域状态会被保留。停止执行的生成器函数会在生成器对象再次调用 next() 方法后恢复执行。

function* generatorFn() 
    console.log('generatorExe');
    yield '1';
    console.log('1-2');
    yield '2';
    console.log('2-3');
    return '3';


let generatorObject = generatorFn();
let generatorObject2 = generatorFn();

console.log(generatorObject.next()); // generatorExe    value: '1', done: false 
console.log(generatorObject.next()); // '1-2'  value: '2', done: false 

console.log(generatorObject2.next()); 

Python迭代器和生成器

Python的迭代器集成在语言之中,迭代器和生成器是Python中很重要的用法,本文将深入了解迭代器和生成器

首先,我们都知道for循环是一个基础迭代操作,大多数的容器对象都可以使用for循环,那么,我们从for循环开始

你有没有想过,for循环的内部实现原理呢?

其实,在Python中,for循环是对迭代器进行迭代的语法糖,内部运行机理就是:首先底层对循环对象实现迭代器包装(调用容器对象的__iter__方法)返回一个迭代器对象,每循环一步,就调用一次迭代器对象的__next__方法,直到循环结束时,自动处理StopIteration这个异常。

对于像list,dict等容器对象而言,都可以使用for循环,但是它们并不是迭代器,它们属于可迭代对象。

什么是可迭代对象呢?

最简单的解释:实现了迭代方法可以被迭代的对象,可以使用isinstance()方法进行判断。

举个例子:

In [1]: from collections import Iterable, Iterator
In [2]: a = [1, 2, 3]
In [3]: isinstance(a, Iterable)
Out[3]: True
In [4]: b = a.__iter__()
In [5]: isinstance(b, Iterator)
Out[5]: True

可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。

以上,可以看到,在迭代过程中,实际调用了迭代器的__next__方法进行迭代。

那么,什么是迭代器?

实现了迭代器协议的对象就是迭代器,所谓的迭代器协议可以简单归纳为:

  1. 实现__iter__()方法,返回一个迭代器
  2. 实现next方法,返回当前元素并指向下一个元素,如果当前位置已无元素,则抛出StopIteration异常 。

迭代器和可迭代对象的区别是:迭代器可以使用next()方法不断调用并返回下一个值,除了调用可迭代对象的__iter__方法来将可迭代对象转换为迭代器以外,还可以使用iter()方法。

举个例子来验证以上说法:

In [1]: iter_data = iter([1, 2, 3])
In [2]: print(next(iter_data))
1
In [3]: print(next(iter_data))
2
In [4]: print(next(iter_data))
3
In [5]: print(next(iter_data))
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-16-425d66e859b8> in <module>
----> 1 print(next(iter_data))

为什么要用迭代器?

很重要的一点是,Python把迭代器内建在语言之中的,我们在遍历一个容器对象时并不需要去实现具体的遍历操作。

迭代器时一个惰性序列,仅仅在迭代至当前元素时才计算该元素的值,在此之前可以不存在,在此之后可以随时销毁,也就是说,在迭代过程中不是将所有元素一次性加载,这样便不需要考虑内存的问题。通过定义迭代器协议,我们可以随时实现一个迭代器。

什么时候用迭代器?
具体在什么场景下可以使用迭代器:

  • 数列的数据规模巨大
  • 数列有规律,但是不能使用列表推导式描述。

举个最简单的例子:

class Fib(object):
    def __init__(self):
        self._a = 0
        self._b = 1

    def __iter__(self):
        return self

    def __next__(self):
        self._a, self._b = self._b, self._a + self._b
        return self._a


if __name__ == '__main__':
    for index, item in enumerate(Fib()):
        print(item)
        if index >= 9:
            break

什么是生成器?

生成器,顾名思义,就是按照一定的模式生成一个序列,是一种高级的迭代器,Python中有一个专门的关键字(yield)来实现生成器。

如果一个函数,使用了yield语句,那么它就是一个生成器函数,当调用生成器函数函数时,它返回一个迭代器,不过这个迭代器时一个生成器对象。

举个例子:

from itertools import islice

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

if __name__ == '__main__':
    fib_data = fib()
    print(list(islice(fib_data, 0, 10)))

可以看到,使用生成器后,代码简洁了很多!在上述代码中添加:

print(type(fib_data))
print(dir(fib_data))

可以看到函数返回的是一个generator对象,且对象实现了迭代器协议。

但是,使用生成器必须要注意的一点是:生成器只能遍历一次

什么时候用生成器呢?

生成器可以使用更少的中间变量来写流式代码, 相比于其它容器对象占用的内存和CPU资源更少一些。当需要一个将返回一个序列或在循环中执行的函数时,就可以使用生成器,因为当这些元素被传递到另一个函数中进行后续处理时,一次返回一个元素可以有效的提升整体性能,最重要的是,比迭代器简洁!

除此以外,生成器还有两个很棒的用处:

  1. 实现with语句的上下文管理器协议
  2. 实现协程

什么是生成器表达式?

列表推导式,大家应该都用到,但是由于内存的限制,列表的容量是有限的,如果要创建一个有几百万个元素的列表,会占用很多的储存空间,当我们只需要访问几个元素时,其它元素占用的空间就白白浪费了。

这种时候你可以用生成器表达式啊,生成式表达式是一种实现生成器的便捷方式,将列表推导式的中括号替换为圆括号,生成器表达式是一种边循环边计算,使得列表的元素可以在循环过程中一个个的推算出来,不需要创建完整的列表,从而节省了大量的空间。

In [1]: a = (item for item in range(10))

In [2]: type(a)
Out[2]: generator

In [3]: next(a)
Out[3]: 0

In [4]: next(a)
Out[4]: 1

以上。

代码可参考:my github

以上是关于迭代器和生成器的主要内容,如果未能解决你的问题,请参考以下文章

迭代器和生成器

《javascript高级程序设计》学习笔记 | 7.2.迭代器模式

Python核心技术与实战——十四|深入了解迭代器和生成器

迭代器和生成器

Python迭代器和生成器

迭代器和生成器