Python Tricks - Looping & Iteration(4)

Generators Are Simplified Iterators

In the chapter on iterators we spent quite a bit of time writing a class-based iterator. This wasn’t a bad idea from an educational perspective—but it also demonstrated how writing an iterator class requires a lot of boilerplate code. To tell you the truth, as a “l(fā)azy” developer, I don’t like tedious and repetitive work.

And yet, iterators are so useful in Python. They allow you to write pretty for -in loops and help you make your code more Pythonic and efficient. If there only was a more convenient way to write these iterators in the first place…

Surprise, there is! Once more, Python helps us out with some syntactic sugar to make writing iterators easier. In this chapter you’ll see how to write iterators faster and with less code using generators and the yield keyword.

Infinite Generators

Let’s start by looking again at the Repeater example that I previously used to introduce the idea of iterators. It implemented a class-based iterator cycling through an infinite sequence of values. This is what the class looked like in its second (simplified) version:

class Repeater:
  def __init__(self, value):
    self.value = value

  def __iter__(self):
    return self

  def __next__(self):
    return self.value

If you’re thinking, “that’s quite a lot of code for such a simple iterator,” you’re absolutely right. Parts of this class seem rather formulaic, as if they would be written in exactly the same way from one class-based iterator to the next.

This is where Python’s generators enter the scene. If I rewrite this iterator class as a generator, it looks like this:

def repeater(value):
  while True:
    yield value

We just went from seven lines of code to three. Not bad, eh? As you can see, generators look like regular functions but instead of using the return statement, they use yield to pass data back to the caller.

Will this new generator implementation still work the same way as our class-based iterator did? Let’s bust out the for-in loop test to find out:

>>> for x in repeater('Hi'):
...   print(x)
'Hi'
'Hi'
'Hi'
'Hi'
'Hi'
...

Yep! We’re still looping through our greetings forever. This much shorter generator implementation seems to perform the same way that the Repeater class did. (Remember to hit Ctrl+C if you want out of the infinite loop in an interpreter session.)

Now, how do these generators work? They look like normal functions, but their behavior is quite different. For starters, calling a generator function doesn’t even run the function. It merely creates and returns a generator object:

>>> repeater('Hey')
<generator object repeater at 0x107bcdbf8>

調用生成器函數甚至不能運行函數解藻。它只是創(chuàng)造和返回一個生成器對象。

The code in the generator function only executes when next() is called on the generator object:

>>> generator_obj = repeater('Hey')
>>> next(generator_obj)
'Hey'

If you read the code of the repeater function again, it looks like the yield keyword in there somehow stops this generator function in midexecution and then resumes it at a later point in time:

def repeater(value):
  while True:
    yield value

And that’s quite a fitting mental model for what happens here. You see, when a return statement is invoked inside a function, it permanently passes control back to the caller of the function. When a yield is invoked, it also passes control back to the caller of the function—but it only does so temporarily.

當在函數內部調用RETURN語句時波俄,它會永久地將控制權傳遞回函數的調用方。當調用yield時解幽,它也將控制權傳遞回函數的調用方,但它只是暫時這樣做。

Whereas a return statement disposes of a function’s local state, a yield statement suspends the function and retains its local state. In practical terms, this means local variables and the execution state of the generator function are only stashed away temporarily and not thrown out completely. Execution can be resumed at any time by calling next() on the generator:

>>> iterator = repeater('Hi')
>>> next(iterator)
'Hi'
>>> next(iterator)
'Hi'
>>> next(iterator)
'Hi'

執(zhí)行可以在調用next在生成器上可以再重新開始。

This makes generators fully compatible with the iterator protocol. For this reason, I like to think of them primarily as syntactic sugar for implementing iterators.

You’ll find that for most types of iterators, writing a generator function will be easier and more readable than defining a long-winded classbased iterator.

Generators That Stop Generating

In this chapter we started out by writing an infinite generator once again. By now you’re probably wondering how to write a generator that stops producing values after a while, instead of going on and on forever.

Remember, in our class-based iterator we were able to signal the end of iteration by manually raising a StopIteration exception. Because generators are fully compatible with class-based iterators, that’s still what happens behind the scenes.

Thankfully, as programmers we get to work with a nicer interface this time around. Generators stop generating values as soon as control flow returns from the generator function by any means other than a yield statement. This means you no longer have to worry about raising StopIteration at all!

Here’s an example:

def repeat_three_times(value):
  yield value
  yield value
  yield value

Notice how this generator function doesn’t include any kind of loop. In fact it’s dead simple and only consists of three yield statements. If a yield temporarily suspends execution of the function and passes back a value to the caller, what will happen when we reach the end of this generator? Let’s find out:

>>> for x in repeat_three_times('Hey there'):
...   print(x)
'Hey there'
'Hey there'
'Hey there'

As you may have expected, this generator stopped producing new values after three iterations. We can assume that it did so by raising a StopIteration exception when execution reached the end of the function. But to be sure, let’s confirm that with another experiment:

>>> iterator = repeat_three_times('Hey there')
>>> next(iterator)
'Hey there'
>>> next(iterator)
'Hey there'
>>> next(iterator)
'Hey there'
>>> next(iterator)
StopIteration
>>> next(iterator)
StopIteration

This iterator behaved just like we expected. As soon as we reach the end of the generator function, it keeps raising StopIteration to signal that it has no more values to provide.

Let’s come back to another example from the iterators chapter. The BoundedIterator class implemented an iterator that would only repeat a value a set number of times:

class BoundedRepeater:
  def __init__(self, value, max_repeats):
    self.value = value
    self.max_repeats = max_repeats
    self.count = 0

  def __iter__(self):
    return self

  def __next__(self):
    if self.count >= self.max_repeats:
        raise StopIteration
    self.count += 1
    return self.value

Why don’t we try to re-implement this BoundedRepeater class as a generator function. Here’s my first take on it:

def bounded_repeater(value, max_repeats):
  count = 0
  while True:
    if count >= max_repeats:
      return
    count += 1
    yield value

I intentionally made the while loop in this function a little unwieldy. I wanted to demonstrate how invoking a return statement from a generator causes iteration to stop with a StopIteration exception. We’ll soon clean up and simplify this generator function some more, but first let’s try out what we’ve got so far:

>>> for x in bounded_repeater('Hi', 4):
...   print(x)
'Hi'
'Hi'
'Hi'
'Hi'

Great! Now we have a generator that stops producing values after a configurable number of repetitions. It uses the yield statement to pass back values until it finally hits the return statement and iteration stops.

Like I promised you, we can further simplify this generator. We’ll take advantage of the fact that Python adds an implicit return None statement to the end of every function. This is what our final implementation looks like:

def bounded_repeater(value, max_repeats):
  for i in range(max_repeats):
    yield value

Feel free to confirm that this simplified generator still works the same way. All things considered, we went from a 12-line implementation in the BoundedRepeater class to a three-line generator-based implementation providing the exact same functionality. That’s a 75% reduction in the number of lines of code—not too shabby!

As you just saw, generators help “abstract away” most of the boilerplate code otherwise needed when writing class-based iterators. They can make your life as a programmer much easier and allow you to write cleaner, shorter, and more maintainable iterators. Generator functions are a great feature in Python, and you shouldn’t hesitate to use them in your own programs.

Key Takeaways
  • Generator functions are syntactic sugar for writing objects that support the iterator protocol. Generators abstract away much of the boilerplate code needed when writing class-based iterators.
  • The yield statement allows you to temporarily suspend execution of a generator function and to pass back values from it.
  • Generators start raising StopIteration exceptions after control flow leaves the generator function by any means other than a yield statement.

生成器允許你暫時中止一個生成器函數的執(zhí)行唧瘾,然后將數值傳遞回去。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末别凤,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子领虹,更是在濱河造成了極大的恐慌规哪,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件塌衰,死亡現場離奇詭異诉稍,居然都是意外死亡蝠嘉,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門杯巨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蚤告,“玉大人,你說我怎么就攤上這事服爷《徘。” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵仍源,是天一觀的道長心褐。 經常有香客問我,道長笼踩,這世上最難降的妖魔是什么逗爹? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮嚎于,結果婚禮上掘而,老公的妹妹穿的比我還像新娘。我一直安慰自己于购,他們只是感情好袍睡,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著价涝,像睡著了一般女蜈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上色瘩,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天伪窖,我揣著相機與錄音,去河邊找鬼居兆。 笑死覆山,一個胖子當著我的面吹牛,可吹牛的內容都是我干的泥栖。 我是一名探鬼主播簇宽,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吧享!你這毒婦竟也來了魏割?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钢颂,失蹤者是張志新(化名)和其女友劉穎钞它,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡遭垛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年尼桶,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锯仪。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡泵督,死狀恐怖,靈堂內的尸體忽然破棺而出庶喜,到底是詐尸還是另有隱情小腊,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布溃卡,位于F島的核電站溢豆,受9級特大地震影響,放射性物質發(fā)生泄漏瘸羡。R本人自食惡果不足惜漩仙,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望犹赖。 院中可真熱鬧队他,春花似錦、人聲如沸峻村。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粘昨。三九已至垢啼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間张肾,已是汗流浹背芭析。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吞瞪,地道東北人馁启。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像芍秆,于是被迫代替她去往敵國和親惯疙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內容