第一次接觸到yield這個(gè)關(guān)鍵字是在Python里面。伴隨著Generator和Comprehension了解到的。當(dāng)時(shí)一直沒(méi)覺(jué)得它多重要,還以為它只是個(gè)另外一個(gè)用起來(lái)放便點(diǎn)的語(yǔ)法糖。后來(lái)接觸了ES6,接觸了co,我才意識(shí)到,這是個(gè)了不得的東西。
Python的Generator
Python對(duì)「Comprehension」的支持非常友好,相同的語(yǔ)法,可以用在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>
上例中的最后一個(gè),就是所謂的Generator。它還有另外一種產(chǎn)生方法。先定義一個(gè)特殊的函數(shù)
def blah():
for i in range(10):
if i ** 2 < 10:
yield i * 2
然后就可以用如下方法產(chǎn)生一個(gè)Generator
blah()
#> <generator object blah at 0x7f9c53a99410>
通過(guò)__next__方法,我們可以看到,Generator以一種極為怪異的方式運(yùn)作。最簡(jiǎn)單的理解方式,是把yield看作一個(gè)特殊的return,它們?cè)谀撤N程度上的確相似。
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
看上去很怪異,因?yàn)椤窯enerator function」和「function」根本就是兩種不同的東西。而在Python里,卻使用同樣的語(yǔ)法def name():來(lái)創(chuàng)建它們。
調(diào)用「Generator function」返回的是一個(gè)Generator對(duì)象,而不是那個(gè)“函數(shù)”的執(zhí)行結(jié)果。
Javascript的Generator
在這一點(diǎn)上,Javascript比Python做得好。引入Generator的時(shí)候,Javascript定義了一個(gè)新的關(guān)鍵字function*,兩者就被顯式地區(qū)分開(kāi)了。你一眼就能注意到,這不是一個(gè)普通的函數(shù)。
我覺(jué)得這是少數(shù)Javascript做得比Python好的地方。
function* blah() {
for (var i = 0; i < 10; i++)
if ((i * i) < 10)
yield i * 2
}
和Python一樣,通過(guò)一個(gè)特定的方法(next)來(lái)使用
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 }
對(duì)參數(shù)的支持情況
Generator的next方法支持參數(shù)這一點(diǎn)非常重要,它是讓Generator能夠包裝異步調(diào)用的基礎(chǔ)特性。JS里面,繼續(xù)使用next就行了。
function* blah() {
for (var i = 0; i < 10; i++) {
t = yield i * 2
console.log('t is ', t)
}
}
傳遞參數(shù)給next,就仿佛是賦值給那個(gè)yield表達(dá)式一樣。
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)
當(dāng)你給__next__傳遞參數(shù)的時(shí)候
a = blah()
a.__next__(1)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: expected 0 arguments, got 1
然而Python的Generator有個(gè)叫做send的方法,它可以完成這個(gè)工作。send必須傳一個(gè)參數(shù),而且初次調(diào)用必須傳None。所以細(xì)節(jié)上和JS有點(diǎn)區(qū)別,但大體上是一致的。
a = blah()
a.send(None)
#> 0
a.send("b")
# t is b
#> 2
a.send("c")
# t is c
#> 4
至于為什么不把__next__和send合并為一個(gè)就不得而知了。其實(shí)跳出來(lái)看看,對(duì)應(yīng)Generator的行為,取名叫send的確更合語(yǔ)境。
異常與Generator
對(duì)于Generator而言,在next之外,最重要的一個(gè)方法就是throw了。在Generator函數(shù)里面,異常是可以被捕獲的,即使異常的發(fā)生地點(diǎn)不在本函數(shù)內(nèi)部。這是Generator的一大特點(diǎn)。
Javascript和Python在異常的處理這塊出奇的一致,名字都一樣使用throw。
為了方便,先寫(xiě)一個(gè)永遠(yuǎn)返回"ok"的Generator函數(shù)
var cnt = 0
function* blah() {
while (true) {
try { yield ++cnt }
catch (e) { console.log(`error: ${e.message}`) }
}
}
現(xiàn)在對(duì)它做點(diǎn)測(cè)試
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如何呢,同樣的實(shí)驗(yàn)
cnt = 0
def blah():
global cnt
while True:
try:
cnt += 1
yield cnt
except Exception as e:
print("error:", str(e))
同樣的測(cè)試
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