iterators, iterables, generators

原文作者:Mark Mruss
中文翻译:Tony(digitalsatori)
原文出处:http://www.learningpython.com

简介

在本文中我们将讨论Python语言中三个相关的概念:iterators, iterables 和 generators。(译者注4 ) Generators的概念很容易了解。它们是创建和返回iterator的函数。而iterator和iterables就用起来容易说起来难了。iterable对象是”可以一次返回其一个成员的容器对象1 iterator 对象表示一个数据流,当重复的调用iterator 的next()方法时,其将顺序返回数据流上的数据。当没有数据时返回一个StopIteration 异常。此时iterator 对象已被穷尽,任何对next()的继续调用,都只会导致返回StopIteration 异常。2 你可以这样来理解这两个概念之间的差别:iterable 对象可以被遍历多次,而iterator 对象只能遍历一次。总之,当每次需要遍历iterable 的数据时,iterable 创建一个iterator 对象。

注: 定义了__getitem__函数的类也可以被认为是iterables,但是这个内容不在本文讨论范围之内。

本文中我们首先讨论iterators及其最基本的概念。然后是generators, 最后我们讨论3个概念中最为宽泛的话题,iterables。

Python中的遍历操作

在Python中我们使用Iterators对象来遍历对象中的数据。例如,我们都知道如何来遍历list中的数据:

my_list = [1,2,3]
for num in my_list:
  print num

上面的代码会遍历list对象my_list,然后打印出其中所有的对象,即,1,2,3。 用这种简单透明的方法对一个序列(sequences)作遍历操作是我喜欢Python的原因之一。

根据上面的定义,list是iterables,因为你可以对它作多次的遍历操作。事实上,每次对list作遍历操作时,我们实际上用到的是list创建的listiterator这个iterator对象。

你可能还不清楚什么时候应该给一个类增加遍历支持。可是,随着你更多的接触Python, 你会发现这样的设计非常有帮助,它的一个明显的好处是代码更加清晰。使用iterators 和 generators的一个好处是,只在需要时处理每个成员对象。它不是获取所有的数据并放到一个list中,然后遍历这个list,而是在你需要使用时,才去获取每个数据。乍看起来这好象没太大的区别但是请设想如果成员数据有成千上万之多呢?或者你要获取的数据来自网络呢?一次性处理所有的数据会花费很多的时间,特别当你需要的只是开头的几个数据就更划不来了。

第一个例子

为了更深入的了解遍历操作,我们来看一个可能用到iterators的简单例子。在这个例子中我们会创建一个类,这个类接受字符串然后将每个字符转换为ASCII字节码。如果我们不用iterators,这个程序会如: Listing 1 所示:

Listing 1:

class ByteValue(object):

  def __init__(self, data):
    self.data = data

  def to_bytes(self):
    bytes = []
    for char in self.data:
      bytes.append(ord(char))
    return bytes

这个代码很简单。这里有一个类的数据成员 data , 我们用它来储存用于初始化类的字符串数据。在 to_bytes 函数中我们循环遍历这个字符串,使用内置的 ord 函数转换其中的每个字符为其ASCII字节值,将每个ASCII码值存储于一个list中,当获取了所有的数值,我们将返回这个list。

运行如下:

bv = ByteValue("abcdef")
for byte in bv.to_bytes():
  print byte

返回的结果为:

97
98
99
100
101
102

创建iterator对象

我们现在将上面创建的这个类转变成一个iterator。要使一个类变成iterator需要添加两个方法,这两个方法共同组成了iterator协议。3 所需的函数是:

  1. __iter__ 函数
  2. next 函数

__inter__会返回对象本身,而next函数会返回下一个数据项。next函数正是遍历操作实现之所在。每次next函数被调用,其会返回下一个数据项直到所有数据项都已被遍历。当没有更多的数据项时,next函数应该抛出StopIteration异常以终止遍历操作。

也就是说,要让你的类变成一个iterator,你需要做:

  1. 添加一个 __iter__ 函数用来返回对象本身(self)
  2. 添加一个 next 函数用来在被调用时返回序列中的下一个数据项。当序列中的数据项都已被返回时,调用next函数抛出StopIteration异常来表示遍历的结束。

如果你还是不太清楚,下面是将ByteValue类转换为iterator对象的代码:

Listing 2:

class ByteValue(object):

  def __init__(self, data):
    self.data = data
    self.current_item = 0

  def __iter__(self):
    return self

  def next(self):
    if (self.current_item == len(self.data)):
      raise StopIteration
    else:
      byte_value = ord(self.data[self.current_item])
      self.current_item += 1
      return byte_value

我们来比较一下 Listing 1Listing 2 的代码,重点考察 Listing 2 中的iterator。第一处不同是在 __init__ 函数中多了类的数据成员 current_item 。current_item 作为一个计数器在我们遍历字符串中的字符时跟踪当前的字符的位置。该计数器必须有类的作用域,iterator要工作于对next函数的持续调用。如果current_item是next函数的本地变量,它的值在每次后继的调用中就被重置了,这样就起不到计数器的作用了。

第二个不同是多了返回 self__iter__ 函数。

最后一个不同之处就是多了函数 next 。该函数首先判断current_item是否与字符串的长度相等,如果current_item与字符串的长度相等就抛出 StopIteration 异常以表示遍历操作结束,因为已经没有更多的字符可以用来遍历了。如果还有更多的字符可以遍历,计算当前字符的字节值(即ASCII码),current_item计数器加一,并返回当前的的字节值。

注: current_item只在ByteValue对象创建时被初始化。这样设计是因为根据iterator协议,“一旦iterator的next函数抛出StopIteration异常,对于以后的函数调用它会抛出同样的异常。”[2]_ 如果我们能重新初始化current_item的值,iterator就可以遍历多次,这也就违反了iterator协议。

我们现在来看看转变为iterator后的类的工作情况:

for byte in ByteValue("abcdef"):
  print byte

返回的结果为:

97
98
99
100
101
102

请注意这里我们并没有将ByteValue类的实例保存在一个变量中。那样做的话是完全多余的,因为ByteValue是一个iterator,只能从它获取一次数据。如果ByteValue是一个iterable(当调用__iter__时返回一个iterator),那么保存这个实例是有意义的,因为这个实例可以被遍历多次。我们稍后会了解使用iterables的情况。

深入了解Iterator

让我们来看看在遍历过程到底发生了什么。为了说明for循环的背后到底发生了什么,我会展示给你其背后函数实际调用的顺序。

遍历操作的第一步就是调用__iter__函数,目的是获得iterator对象,其将被用于遍历操作。请注意这一步同时适用于iterator和iterable对象,因为iterators 会返回它们自己而iterables 会返回一个iterator。

bv = ByteValue("abcdef")
iterator = bv.__iter__()

这样我们就已经有了interator 对象,接下来我们可以通过调用next 函数来遍历获得它的下一个值:

print iterator.next()

这样就执行了next 函数,其将返回字符串中当前字符的字节码。因为这是第一次调用next 函数,current_item 值为0,所以会计算字符串中第一个字符('a')的字节码:

97

如果我们继续调用next 函数六次,就获得如 Listing 3 所示的结果。注意这里我们总共调用了七次next函数。比字符串中的字符个数多了一个。以下的结果是运行python文件(iter.py- Listing 4 )获得的。因为运行方式的不同,结果可能会稍有不同。不过,最重要的是要看到例子中的所抛出的异常。

Listing 3:

97
98
99
100
101
102
Traceback (most recent call last):
File "Listing4.py", line 34, in ?
main()
File "Listing4.py", line 30, in main
print iterator.next()
File "Listing4.py", line 13, in next
raise StopIteration
StopIteration

Listing 4:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  #!/usr/bin/env python

  class ByteValue(object):

    def __init__(self, data):
      self.data = data
      self.current_item = 0
    def __iter__(self):
      return self

  def next(self):
    if (self.current_item == len(self.data)):
      raise StopIteration
    else:
      byte_value = ord(self.data[self.current_item])
      self.current_item += 1
      return byte_value
    return self.data

  def main():
    for v in ByteValue("abc"):
      if v in ByteValue("abc"):
        print "We have a %d" % v

  bv = ByteValue("abcdef")
  iterator = bv.__iter__()
  print iterator.next()
  print iterator.next()
  print iterator.next()
  print iterator.next()
  print iterator.next()
  print iterator.next()
  print iterator.next()

  if __name__ == "__main__":
  # Someone is launching this directly
    main()

多数情况下你不会去手工调用iterator 对象的__iter__或next 函数,而是让for循环来帮你完成这些操作。

Iterators 的长处与短处

既然我们定义的类是iterator 对象,我们就可以使用那些工作于iterators 和 iterables 的内置函数和方法,比如:sum,tuple,sorted,list等。

在第一个例子中,bytes 函数返回了一个list对象。如果某种情况下需要list而不是iterator 的话,可以很简单的使用list函数获得,iterator可以作为list函数的实参,函数会返回一个list:

list(ByteValue("abcdef"))

如果需要得到排序的list:

sorted(ByteValue("abcdef"))

如果需要得到所有字节码的和:

sum(ByteValue("abcdef"))

虽然iterators 很有用,但是它们也确实有其短处。最明显的莫过于只能从其获取一次数据。它们也要求你在类中添加用于跟踪当前遍历位置的数据成员。根据遍历对象的不同,这个操作可能会比较复杂。Iterator也只允许你作一种类型的遍历操作,也就是说只能在一个方向上进行或在一段内部数据上进行。你当然可以在类中添加标志来解决这类问题,但这会使我们的类定义变得混乱,降低其可读性。那么怎么才能在类中获得多种类型的遍历操作呢?这就要用到generator了!

Generators

generator是创建,生成iterator的函数。使用关键词yield来返回一个值的函数我们称其为generator。

Generators很有趣。他们本身是函数,可是又不象普通函数那样在执行时一次运行完毕。当第一次执行一个generator函数时,从函数的起始处开始运行至yield关键词。当遍历操作继续时,将会从generator函数的紧跟yield关键词之后的语句处继续执行。函数中的所有本地变量都保持不变。如果yeild语句处于一个循环中,执行将在循环中继续而不中断。

继续ByteValue的例子,我们来添加一个名叫reverse的generator函数, 将用其来反向遍历字符串中的字符的字节码:

def reverse(self):
  current_item = len(self.data)
  while (current_item > 0):
    current_item -= 1
    yield ord(self.data[current_item])

这里有什么不同呢?首先,我们没有添加任何类数据成员,这个generator是一个独立的单位。第二,因为这是一个generator,所以计数器current_item也可以是本地变量。

在reverse函数中,第一步就是初始化用以表示字符串中当前字符位置的current_item变量,使其值与字符串的长度相等。我们使用字符串的长度而不是0因为我们要反向遍历这个字符串。接下来,我们用一个while循环,当current_item大于0时执行这个循环。在这个循环中计数器每次减一来获取当前要处理的字符。最后我们yield当前字符的字节码。

注: 在循环中从计数器中先减一是因为Python是以0为起始的,一个list的长度减一得到list中的最后一项。在我们的例子中使用了以下的字符串:

abcdef
012345

字符串的长度为6,将这个数字减一得到5,这就是字符串最后一个字符的索引值。

我们来如下运行新建的generator函数:

bv = ByteValue("abcdef")
  for byte in bv.reverse():
    print byte

结果是:

102
101
100
99
98
97

深入了解Generator

让我们用前面分析iterator对象的方法来详细了解generator背后的情况。当调用generator函数时,首先发生的并不是对函数本身的执行,而是创建了一个generator对象。比如:

bv = ByteValue("abcdef")
gen = bv.reverse()
print gen

输出结果为:

<generator object at 0xb7d8e04c>

正如我们前面所述,从以上结果可以看出第一次调用generator函数时并没有返回字符串中最后一个字符的字节码,而是创建了一个generator对象。事实上第一次调用generator, 函数本身完全没有被执行。“generator对象是Python用来创建iterators的,这些iterators是由对能yield值的函数进行遍历操作时产生2

有了generator 对象后,我们就可以调用它的next函数(怎么样,听起来很熟悉吧?)来执行遍历操作了。 Listing 5 是这个代码, Listing 6 是执行结果。执行结果应该你非常熟悉了,尤其是那个 StopIteration 异常。

Listing 5:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    #!/usr/bin/env python

    class ByteValue(object):

      def __init__(self, data):
        self.data = data
        self.current_item = 0
      def __iter__(self):
        return self

      def next(self):
        if (self.current_item == len(self.data)):
          raise StopIteration
        else:
          byte_value = ord(self.data[self.current_item])
          self.current_item += 1
          return byte_value

      def reverse(self):
        current_item = len(self.data)
        while (current_item > 0):
          current_item -= 1
          yield ord(self.data[current_item])

    def main():

    bv = ByteValue("abcdef")
    gen = bv.reverse()
    print gen.next()
    print gen.next()
    print gen.next()
    print gen.next()
    print gen.next()
    print gen.next()
    print gen.next()

    if __name__ == "__main__":
      # Someone is launching this directly
      main()

Listing 6:

102
101
100
99
98
97
Traceback (most recent call last):
File "Listing5.py", line 40, in ?
main()
File "Listing5.py", line 36, in main
print gen.next()
StopIteration

如果希望了解generator对象的结构内容,我们可以执行下面的代码:

print dir(gen)

得到以下结果:

['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__',
'__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__str__', 'gi_frame', 'gi_running', 'next']

请注意generator对象包含__iter__和next函数,这使得generator对象本身又是一个iterator对象。因为generator本身就是一个iterator对象,所以我之前介绍的那些作用于iterator的内置函数也同样适用于generator。

print sum(bv.reverse())

要了解的是,generator或iterator并非只能作简单的遍历操作,它们完全可以同其他代码一样执行一些复杂的判断操作。

比如说我们用ASCII码值99作为reverse generator的终止执行的条件,我们的修改如 Listing 7 所示:

Listing 7:

def reverse(self):
  current_item = len(self.data)
  while (current_item > 0):
    current_item -= 1
    value = ord(self.data[current_item])
    if (value == 99):
      return
    yield value

Listing 7 中如果字节码值为99时,将使用 return 关键词从generator函数返回。因为没有yield任何东西,会导致抛出 StopIteration 从而终止遍历操作。

不要尝试让遍历操作做太过'聪明'的事情,因为你不可以从generator返回值,而只能yield值。所以如果执行 Listing 8 的代码,试图在终止前返回最后一个值,在执行到返回值的那行代码时你会获得下面的报错:

SyntaxError: 'return' with argument inside generator

Listing 8:

def reverse(self):
  current_item = len(self.data)
  while (current_item > 0):
    current_item -= 1
    value = ord(self.data[current_item])
    if (value == 99):
      return value
    yield value

iterables又怎样呢?

你现在肯定在琢磨我在本文开始提到的iterable对象了。正如你猜到的,其实除非执行的是象我们的ByeteValue例子这样的一次性的任务,否则创建一个iterator类并不是很有用处。如果你的类是要处理多个任务的,你最好还是创建iterable对象的类,而非iterator对象。再重复一次,iterable对象在其 __iter__ 函数被调用时,返回一个iterator对象,这样其数据就可以被遍历任意多次。

创建Iterable对象

按照iterable对象的定义,只要符合在调用 __iter__ 函数时返回一个iterator对象即可,创建iterable对象因此有很多方法。其中的两个是:

  1. 创建一个iterator辅助类(helper class)来执行遍历操作
  2. 使用一个generator函数

我倾向于使用generator函数,因为这样可以让所有的功能都保持在主类(main class)中。

参见 Listing 9 ,这是一个使用generator来创建iterable对象的例子。在代码中我们将next函数替换为forward函数。forward函数是一个generator用于正向遍历所有的数据。在 __iter__ 函数中,我们返回调用forward函数所产生的generator对象。因为generator对象实际上就是iterator对象,这样我们就成功构造了一个iterable。

Listing 9:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 class ByteValue(object):

   def __init__(self, data):
     self.data = data

   def __iter__(self):
     #We are an iterable, so return our iterator
     return self.forward()

   def forward(self):
     #The forward generator
     current_item = 0
     while (current_item < len(self.data)):
       byte_value = ord(self.data[current_item])
       current_item += 1
       yield byte_value

   def reverse(self):
     #The reverse generator
     current_item = len(self.data)
     while (current_item > 0):
       current_item -= 1
       yield ord(self.data[current_item])

 def main():
   bv = ByteValue("abc")
   for v in bv:
     if v in bv:
       print "We have a %d" % v

 if __name__ == "__main__":
   main()

我们创建了这个iterable对象,这样就可以对其遍历任意多次了。甚至还可以如下这样进行嵌套多次遍历:

bv = ByteValue("abcdef")
for value in bv:
  print value
  for second_value in bv:
    print second_value

总结

到此我们就结束了对iterators, iterables, 和generators的介绍。希望我已经将他们各自的强大,灵活的特性介绍清楚了。总之,创建iterable对象和使用generators要比简单的创建iterator对象更加灵活些。因为对于大多数复杂的类或者复杂的数据,多次遍历是必须的。

虽然我们对iterators, iterables,以及generators赞誉有加,但是要知道它们不是万能药,不能用在所有的获取序列对象的方法中。在很多情况下返回一个List是必须的。而iterators, iterables, 和generators 只是提供了一个非常好的方法来循环遍历数据项。


[1]http://docs.python.org/tut/node18.html
[2](1, 2) http://docs.python.org/lib/typeiter.html
[3]http://docs.python.org/api/gen-objects.html
[4]在本文中iterator, iterable, generator这三个概念为保证其含义的准确,未作翻译。将iteration操作翻译成 遍历 ,不知是否妥当。网上也有翻译成“迭代”的,但好象在这里不是很合适。

Leave a Response