【问题标题】:Python for loop and iterator behaviorPython for 循环和迭代器行为
【发布时间】:2015-06-06 20:15:53
【问题描述】:

我想进一步了解iterators,所以如果我错了,请纠正我。

迭代器是一个对象,它有一个指向下一个对象的指针,并被读取为缓冲区或流(即链表)。它们特别有效,因为它们所做的只是通过引用而不是使用索引来告诉您下一步是什么。

但是我仍然不明白为什么会发生以下行为:

In [1]: iter = (i for i in range(5))

In [2]: for _ in iter:
   ....:     print _
   ....:     
0
1
2
3
4

In [3]: for _ in iter:
   ....:     print _
   ....:     

In [4]: 

在迭代器 (In [2]) 的第一个循环之后,就好像它被消耗并留空,所以第二个循环 (In [3]) 什么也不打印。

但是我从未为 iter 变量分配新值。

for 循环背后到底发生了什么?

【问题讨论】:

    标签: python iterator


    【解决方案1】:

    您的怀疑是正确的:迭代器已被消耗。

    实际上,您的迭代器是一个generator,它是一个能够被迭代一次的对象。

    type((i for i in range(5))) # says it's type generator 
    
    def another_generator():
        yield 1 # the yield expression makes it a generator, not a function
    
    type(another_generator()) # also a generator
    

    它们高效的原因与“通过引用”告诉您下一步是什么无关。它们是高效的,因为它们只根据请求生成下一个项目;所有项目都不是一次生成的。事实上,你可以拥有一个无限的生成器:

    def my_gen():
        while True:
            yield 1 # again: yield means it is a generator, not a function
    
    for _ in my_gen(): print(_) # hit ctl+c to stop this infinite loop!
    

    其他一些有助于提高理解的更正:

    • 生成器不是指针,其行为方式与您在其他语言中可能熟悉的指针不同。
    • 与其他语言的区别之一:如上所述,生成器的每个结果都是动态生成的。在请求之前不会生成下一个结果。
    • 关键字组合 for in 接受一个可迭代对象作为其第二个参数。
    • 可迭代对象可以是生成器,如您的示例情况,但它也可以是任何其他可迭代对象,例如list,或dict,或str 对象(字符串),或提供所需功能的用户定义类型。
    • iter function 应用于对象以获取迭代器(顺便说一句:不要像您所做的那样在 Python 中使用 iter 作为变量名 - 它是关键字之一)。实际上,更准确地说,对象的__iter__ method 被调用(也就是说,在大多数情况下,所有iter 函数无论如何都会做;__iter__ 是Python 所谓的“魔术方法”之一)。李>
    • 如果对__iter__ 的调用成功,函数next() 在循环中一遍又一遍地应用于可迭代对象,并将提供给for 的第一个变量in 分配给结果next() 函数。 (记住:可迭代对象可以是生成器,或者容器对象的迭代器,或者任何其他可迭代对象。)实际上,更准确地说:它调用迭代器对象的__next__ 方法,这是另一种“魔术方法”。
    • for 循环在 next() 引发 StopIteration 异常时结束(这通常发生在当调用 next() 时迭代没有另一个对象可以产生时)。

    您可以通过这种方式在 python 中“手动”实现for 循环(可能不完美,但足够接近):

    try:
        temp = iterable.__iter__()
    except AttributeError():
        raise TypeError("'{}' object is not iterable".format(type(iterable).__name__))
    else:
        while True:
            try:
                _ = temp.__next__()
            except StopIteration:
                break
            except AttributeError:
                raise TypeError("iter() returned non-iterator of type '{}'".format(type(temp).__name__))
            # this is the "body" of the for loop
            continue
    

    上面的代码和你的示例代码几乎没有区别。

    实际上,for 循环中更有趣的部分不是for,而是in。单独使用in 会产生与for in 不同的效果,但了解in 对其参数的作用非常有用,因为for in 实现了非常相似的行为。

    • 单独使用in关键字时,首先调用对象的__contains__ method,这又是一种“神奇的方法”(注意使用forin时会跳过这一步)。在容器上单独使用in,您可以执行以下操作:

      1 in [1, 2, 3] # True
      'He' in 'Hello' # True
      3 in range(10) # True
      'eH' in 'Hello'[::-1] # True
      
    • 如果可迭代对象不是容器(即它没有__contains__ 方法),in 接下来会尝试调用对象的__iter__ 方法。如前所述:__iter__ 方法返回 Python 中已知的 iterator。基本上,迭代器是一个可以使用内置泛型函数next() on1 的对象。生成器只是迭代器的一种。

    • 如果对__iter__ 的调用成功,in 关键字将函数next() 一遍又一遍地应用于可迭代对象。 (记住:可迭代对象可以是生成器,或者容器对象的迭代器,或者任何其他可迭代对象。)实际上,更准确地说:它调用迭代器对象的__next__ 方法)。
    • 如果对象没有返回迭代器的__iter__ 方法,in 然后使用对象的__getitem__ 方法返回旧式迭代协议2
    • 如果上述所有尝试均失败,您将收到TypeError exception

    如果您希望创建自己的对象类型以进行迭代(即,您可以使用forin,或者只使用in),了解yield 关键字很有用,它用于generators(如上所述)。

    class MyIterable():
        def __iter__(self):
            yield 1
    
    m = MyIterable()
    for _ in m: print(_) # 1
    1 in m # True    
    

    yield 的存在将函数或方法变成了生成器,而不是常规的函数/方法。如果您使用生成器,则不需要__next__ 方法(它会自动带来__next__)。

    如果您希望创建自己的容器对象类型(即,您可以在其上单独使用in,但不能使用for in),您只需要__contains__ 方法。

    class MyUselessContainer():
        def __contains__(self, obj):
            return True
    
    m = MyUselessContainer()
    1 in m # True
    'Foo' in m # True
    TypeError in m # True
    None in m # True
    

    1 请注意,要成为迭代器,对象必须实现the iterator protocol。这仅意味着__next____iter__ 方法都必须正确实现(生成器“免费”提供此功能,因此在使用它们时无需担心) .还要注意___next__ 方法is actually next (no underscores) in Python 2

    2请参阅this answer,了解创建可迭代类的不同方法。

    【讨论】:

    • 更多关于herehere的信息。
    • @Matteo:Marcin 是正确的。是的,您将其视为指针,但不是指针。您的代码 - 括号中的内容 - 它是生成器理解。一旦生成器引发StopIteration,它就完成了。不,0,1,2,3,4 没有存储在任何地方。 range(5) 一次生成一个值。它不会一下子全部生产出来。每次调用next() 时,生成器都会生成下一个值。查找有关函数式编程的一些信息,例如在 Haskel 中,这种想法很常见,与 Java 和 c++ 等语言相比。
    • 您的“手动”循环是 sus。您只需分配给iterable.__next__(对于可迭代对象可能存在也可能不存在!)并且永远不要调用它
    • 创建temp = iter(iterable) 然后在try 块中调用next(temp) 会更正确。例如,listiterator 没有 __next__ 方法
    • 此答案将in 等代码中使用的1 in [1, 2, 3] 运算符与for 循环中的关键字用法混为一谈。 in 运算符只需调用 __contains__ method,如果方法不存在,则返回到迭代对象。
    【解决方案2】:

    For 循环基本上调用应用到的对象的next 方法(Python 3 中的__next__)。

    你可以简单地模拟这个:

    iter = (i for i in range(5))
    
    print(next(iter))
    print(next(iter))  
    print(next(iter))  
    print(next(iter))  
    print(next(iter)) 
    
    # this prints 1 2 3 4 
    

    此时输入对象中没有下一个元素。这样做:

    print(next(iter))  
    

    将导致StopIteration 抛出异常。此时for 将停止。并且迭代器可以是any object,它将响应next() 函数并在没有更多元素时抛出异常。它不必是任何指针或引用(无论如何,在 C/C++ 意义上,python 中没有这样的东西)、链表等。

    【讨论】:

      【解决方案3】:

      python 中有一个迭代器协议,它定义了for 语句如何处理列表和字典,以及其他可以循环的东西。

      在 python 文档 herehere 中。

      迭代器协议的工作方式通常是 python 生成器的形式。我们yield一个值,只要我们有一个值,直到我们到达终点,然后我们提出StopIteration

      让我们编写自己的迭代器:

      def my_iter():
          yield 1
          yield 2
          yield 3
          raise StopIteration()
      
      for i in my_iter():
          print i
      

      结果是:

      1
      2
      3
      

      对此有几点需要注意。 my_iter 是一个函数。 my_iter() 返回一个迭代器。

      如果我改用这样的迭代器:

      j = my_iter()    #j is the iterator that my_iter() returns
      for i in j:
          print i  #this loop runs until the iterator is exhausted
      
      for i in j:
          print i  #the iterator is exhausted so we never reach this line
      

      结果和上面一样。当我们进入第二个 for 循环时,迭代器已经耗尽。

      但那是相当简单的东西,更复杂的呢?也许在一个循环中为什么不呢?

      def capital_iter(name):
          for x in name:
              yield x.upper()
          raise StopIteration()
      
      for y in capital_iter('bobert'):
          print y
      

      当它运行时,我们在字符串类型上使用迭代器(内置在 iter 中)。这反过来又允许我们在它上面运行一个 for 循环,并在我们完成之前产生结果。

      B
      O
      B
      E
      R
      T
      

      所以现在这引出了一个问题,那么迭代器中的产量之间会发生什么?

      j = capital_iter("bobert")
      print i.next()
      print i.next()
      print i.next()
      
      print("Hey there!")
      
      print i.next()
      print i.next()
      print i.next()
      
      print i.next()  #Raises StopIteration
      

      答案是函数在 yield 处暂停,等待下一次调用 next()。

      B
      O
      B
      Hey There!
      E
      R
      T
      Traceback (most recent call last):
        File "", line 13, in 
          StopIteration
      

      【讨论】:

      • 没有必要明确提出StopIteration。无论如何,生成器函数都会在省略该行的情况下执行此操作
      【解决方案4】:

      概念 1

      所有生成器都是迭代器,但所有迭代器都不是生成器

      概念 2

      迭代器是一个带有 next (Python 2) 或 next (Python 3) 的对象 方法。

      概念 3

      引用维基 Generators 发电机 函数允许您声明一个行为类似于 迭代器,即它可以在 for 循环中使用。

      你的情况

      >>> it = (i for i in range(5))
      >>> type(it)
      <type 'generator'>
      >>> callable(getattr(it, 'iter', None))
      False
      >>> callable(getattr(it, 'next', None))
      True
      

      【讨论】:

        【解决方案5】:

        关于 iter() 的行为的一些额外细节与缺少自己的 __iter__ 方法的 __getitem__ 类。


        __iter__ 之前有__getitem__。如果__getitem__ints 从0 - len(obj)-1 一起使用,则iter() 支持这些对象。它将构造一个新的迭代器,用012... 重复调用__getitem__,直到它得到一个IndexError,然后将其转换为StopIteration

        有关创建迭代器的不同方法的更多详细信息,请参阅this answer

        【讨论】:

          【解决方案6】:

          摘自the Python Practice book


          5。迭代器和生成器

          5.1。迭代器

          我们使用 for 语句循环列表。

          >>> for i in [1, 2, 3, 4]:
          ...     print i,
          ...
          1
          2
          3
          4
          

          如果我们将它与字符串一起使用,它会遍历其字符。

          >>> for c in "python":
          ...     print c
          ...
          p
          y
          t
          h
          o
          n
          

          如果我们将它与字典一起使用,它会遍历其键。

          >>> for k in {"x": 1, "y": 2}:
          ...     print k
          ...
          y
          x
          

          如果我们将它与文件一起使用,它会遍历文件的行。

          >>> for line in open("a.txt"):
          ...     print line,
          ...
          first line
          second line
          

          因此,有许多类型的对象可以与 for 循环一起使用。这些被称为可迭代对象。

          有许多函数使用这些可迭代对象。

          >>> ",".join(["a", "b", "c"])
          'a,b,c'
          >>> ",".join({"x": 1, "y": 2})
          'y,x'
          >>> list("python")
          ['p', 'y', 't', 'h', 'o', 'n']
          >>> list({"x": 1, "y": 2})
          ['y', 'x']
          

          5.1.1。迭代协议

          内置函数 iter 接受一个可迭代对象并返回一个迭代器。

              >>> x = iter([1, 2, 3])
          >>> x
          <listiterator object at 0x1004ca850>
          >>> x.next()
          1
          >>> x.next()
          2
          >>> x.next()
          3
          >>> x.next()
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
          

          停止迭代

          每次我们调用迭代器的下一个方法时,都会给我们下一个元素。如果没有更多元素,则会引发 StopIteration。

          迭代器被实现为类。这是一个类似于内置 xrange 函数的迭代器。

          class yrange:
              def __init__(self, n):
                  self.i = 0
                  self.n = n
          
              def __iter__(self):
                  return self
          
              def next(self):
                  if self.i < self.n:
                      i = self.i
                      self.i += 1
                      return i
                  else:
                      raise StopIteration()
          

          iter 方法使对象可迭代。在幕后,iter 函数在给定对象上调用 iter 方法。

          iter 的返回值是一个迭代器。它应该有一个 next 方法并在没有更多元素时引发 StopIteration。

          让我们试试吧:

          >>> y = yrange(3)
          >>> y.next()
          0
          >>> y.next()
          1
          >>> y.next()
          2
          >>> y.next()
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
            File "<stdin>", line 14, in next
          

          停止迭代

          许多内置函数接受迭代器作为参数。

          >>> list(yrange(5))
          [0, 1, 2, 3, 4]
          >>> sum(yrange(5))
          10
          

          在上面的例子中,iterable 和 iterator 都是同一个对象。请注意,iter 方法返回了 self。不一定总是这样。

          class zrange:
              def __init__(self, n):
                  self.n = n
          
              def __iter__(self):
                  return zrange_iter(self.n)
          
          class zrange_iter:
              def __init__(self, n):
                  self.i = 0
                  self.n = n
          
              def __iter__(self):
                  # Iterators are iterables too.
                  # Adding this functions to make them so.
                  return self
          
              def next(self):
                  if self.i < self.n:
                      i = self.i
                      self.i += 1
                      return i
                  else:
                      raise StopIteration()
          

          如果可迭代和迭代器都是同一个对象,则在一次迭代中消耗它。

          >>> y = yrange(5)
          >>> list(y)
          [0, 1, 2, 3, 4]
          >>> list(y)
          []
          >>> z = zrange(5)
          >>> list(z)
          [0, 1, 2, 3, 4]
          >>> list(z)
          [0, 1, 2, 3, 4]
          

          5.2。发电机

          生成器简化了迭代器的创建。生成器是一个生成一系列结果而不是单个值的函数。

          def yrange(n):
             i = 0
              while i < n:
                  yield i
                  i += 1
          

          每次执行 yield 语句时,函数都会生成一个新值。

          >>> y = yrange(3)
          >>> y
          <generator object yrange at 0x401f30>
          >>> y.next()
          0
          >>> y.next()
          1
          >>> y.next()
          2
          >>> y.next()
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
          

          停止迭代

          所以生成器也是迭代器。您不必担心迭代器协议。

          “生成器”这个词被混淆地用来表示生成的函数和它生成的内容。在本章中,我将使用“生成器”一词来表示生成的对象,而“生成器函数”则表示生成它的函数。

          你能想一想它在内部是如何运作的吗?

          当调用生成器函数时,它甚至没有开始执行该函数就返回一个生成器对象。当第一次调用 next 方法时,函数开始执行直到它到达 yield 语句。产生的值由下一次调用返回。

          以下示例演示了 yield 和对生成器对象的 next 方法的调用之间的相互作用。

          >>> def foo():
          ...     print "begin"
          ...     for i in range(3):
          ...         print "before yield", i
          ...         yield i
          ...         print "after yield", i
          ...     print "end"
          ...
          >>> f = foo()
          >>> f.next()
          begin
          before yield 0
          0
          >>> f.next()
          after yield 0
          before yield 1
          1
          >>> f.next()
          after yield 1
          before yield 2
          2
          >>> f.next()
          after yield 2
          end
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
          

          停止迭代

          让我们看一个例子:

          def integers():
              """Infinite sequence of integers."""
              i = 1
              while True:
                  yield i
                  i = i + 1
          
          def squares():
              for i in integers():
                  yield i * i
          
          def take(n, seq):
              """Returns first n values from the given sequence."""
              seq = iter(seq)
              result = []
              try:
                  for i in range(n):
                      result.append(seq.next())
              except StopIteration:
                  pass
              return result
          
          print take(5, squares()) # prints [1, 4, 9, 16, 25]
          

          【讨论】:

          • 这个答案大部分与问题无关。
          猜你喜欢
          • 2012-06-27
          • 2023-03-05
          • 1970-01-01
          • 2015-02-08
          • 2016-01-18
          • 1970-01-01
          • 2022-10-14
          • 2017-07-29
          • 1970-01-01
          相关资源
          最近更新 更多