第一次接触到yield
这个关键字是在Python里面。伴随着Generator和Comprehension了解到的。当时一直没觉得它多重要,还以为它只是个另外一个用起来放便点的语法糖。后来接触了ES6,接触了co
,我才意识到,这是个了不得的东西。
Python的Generator
Python对「Comprehension」的支持非常友好,相同的语法,可以用在List Comprehension, Set Comprehension, Dict Comprehension, Generator……
[i * 2 for i in range(10) if i ** 2 < 10]
#> [0, 2, 4, 6]
{i * 2 for i in range(10) if i ** 2 < 10}
#> {0, 2, 4, 6}
{k.upper(): v for k, v in {"a": 1, "b": 2}.items()}
#> {'A': 1, 'B': 2}
(i * 2 for i in range(10) if i ** 2 < 10)
#> <generator object <genexpr> at 0x7f9c53a99410>
上例中的最后一个,就是所谓的Generator。它还有另外一种产生方法。先定义一个特殊的函数
def blah():
for i in range(10):
if i ** 2 < 10:
yield i * 2
然后就可以用如下方法产生一个Generator
blah()
#> <generator object blah at 0x7f9c53a99410>
通过__next__
方法,我们可以看到,Generator以一种极为怪异的方式运作。最简单的理解方式,是把yield
看作一个特殊的return
,它们在某种程度上的确相似。
a = blah()
a.__next__()
#> 0
a.__next__()
#> 2
a.__next__()
#> 4
a.__next__()
#> 6
a.__next__()
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
看上去很怪异,因为「Generator function」和「function」根本就是两种不同的东西。而在Python里,却使用同样的语法def name():
来创建它们。
调用「Generator function」返回的是一个Generator对象,而不是那个“函数”的执行结果。
Javascript的Generator
在这一点上,Javascript比Python做得好。引入Generator的时候,Javascript定义了一个新的关键字function*
,两者就被显式地区分开了。你一眼就能注意到,这不是一个普通的函数。
我觉得这是少数Javascript做得比Python好的地方。
function* blah() {
for (var i = 0; i < 10; i++)
if ((i * i) < 10)
yield i * 2
}
和Python一样,通过一个特定的方法(next)来使用
a = blah()
//> Generator { }
a.next()
//> Object { value: 0, done: false }
a.next()
//> Object { value: 2, done: false }
a.next()
//> Object { value: 4, done: false }
a.next()
//> Object { value: 6, done: false }
a.next()
//> Object { value: undefined, done: true }
对参数的支持情况
Generator的next
方法支持参数这一点非常重要,它是让Generator能够包装异步调用的基础特性。JS里面,继续使用next
就行了。
function* blah() {
for (var i = 0; i < 10; i++) {
t = yield i * 2
console.log('t is ', t)
}
}
传递参数给next
,就仿佛是赋值给那个yield
表达式一样。
a = blah()
//> Generator { }
a.next("a")
//> Object { value: 0, done: false }
a.next("b")
// t is b
//> Object { value: 2, done: false }
a.next("c")
// t is c
//> Object { value: 4, done: false }
Python的__next__
不支持这种特性
def blah():
for i in range(10):
t = yield i * 2
print('t is ', t)
当你给__next__
传递参数的时候
a = blah()
a.__next__(1)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: expected 0 arguments, got 1
然而Python的Generator有个叫做send
的方法,它可以完成这个工作。send
必须传一个参数,而且初次调用必须传None。所以细节上和JS有点区别,但大体上是一致的。
a = blah()
a.send(None)
#> 0
a.send("b")
# t is b
#> 2
a.send("c")
# t is c
#> 4
至于为什么不把__next__
和send
合并为一个就不得而知了。其实跳出来看看,对应Generator的行为,取名叫send
的确更合语境。
异常与Generator
对于Generator而言,在next
之外,最重要的一个方法就是throw
了。在Generator函数里面,异常是可以被捕获的,即使异常的发生地点不在本函数内部。这是Generator的一大特点。
Javascript和Python在异常的处理这块出奇的一致,名字都一样使用throw
。
为了方便,先写一个永远返回"ok"的Generator函数
var cnt = 0
function* blah() {
while (true) {
try { yield ++cnt }
catch (e) { console.log(`error: ${e.message}`) }
}
}
现在对它做点测试
a = blah()
a.next()
//> Object {value: 1, done: false}
a.next()
//> Object {value: 2, done: false}
a.next()
//> Object {value: 3, done: false}
a.throw(new Error("haha"))
// error: haha
//> Object {value: 4, done: false}
a.throw(new Error("haha"))
// error: haha
//> Object {value: 5, done: false}
a.next()
//> Object {value: 6, done: false}
Python如何呢,同样的实验
cnt = 0
def blah():
global cnt
while True:
try:
cnt += 1
yield cnt
except Exception as e:
print("error:", str(e))
同样的测试
a = blah()
a.__next__()
#> 1
a.__next__()
#> 2
a.__next__()
#> 3
a.throw(Exception("haha"))
# error: haha
#> 4
a.throw(Exception("haha"))
# error: haha
#> 5
a.__next__()
#> 6