【python小技巧4】for循环原理与迭代器的实现
小倪同学0v0
2023年01月03日 23:02
收录于文集
共6篇

对于新手,for 循环无非 for i in range(10):  for i in lst: 等,那你知道 for 循环真正的原理吗?

目录

  1. 你需要知道的...

  2. for 循环的工作原理(2种情况)

  3. 如何写一个可迭代对象和迭代器


你需要知道的...

  1. for 循环的语法永远是 for xxx in xxx,for 循环又称遍历循环,本质上一切 for 循环,都是在遍历一个对象,无论是 range 还是 list。

  2. 内置函数 与 类的魔术方法 的关系python 里有很多“魔术方法”,它们都有一个共同点:以双下划线开头,以双下划线结尾,如 __str__,__int__,__iter__,__next__ 等。(这也是为什么不推荐新手用 __xxx__ 这种当变量名的原因)其中有很多魔术方法可被对应的内置函数调用,如 str(), int(), iter(), next() 等。

  3. 本文还涉及了一个魔术方法 __getitem__obj.__getitem__(item)  <==>  obj[item],就是列表的取元素操作

  4. 可迭代对象迭代器

    1. 可迭代对象指实现 __iter__ 方法,一般保存了数据,如 list 等。

    2. 迭代器指实现了 __next__ 方法,一般保存了迭代的状态(如迭代到哪一个),如 list_iterator 等。

    3. 严格定义如上,但有一条规定:迭代器应该也是可迭代的。换句话说:如果一个对象实现了 __next__ 方法,那它也应该实现 __iter__ 方法,大部分就是返回自己。可迭代对象 与 迭代器的概念一定要记住,很重要!!!

    4. 如何检测一个对象是否是迭代器或可迭代?使用内置模块 collections(新版建议用collections.abc 模块下的 Iterable 和 Iterator 就可以,具体用法:

代码块
Python
自动换行
复制代码
# 不推荐使用 from collections import Iterable, Iterator
from collections.abc import Iterable, Iterator

isinstance(obj, Iterable)
isinstance(obj, Iterator)
复制成功

        注意:字符串、列表、元组、字典、集合等是可迭代的,但不是迭代器!


假设有一个循环:

代码块
Python
自动换行
复制代码
for i in obj:
  print(i)
复制成功

以下将在这个循环的基础上讲解

第一种情况:

如果 obj 是可迭代的,即实现了 __iter__ 方法,尝试调用 iter(obj) 得到 obj 的迭代器,假设迭代器是 iterator。

然后不断调用 next(iterator),返回值就是迭代出来的值,直到遇到 StopIteration,停止循环。

这种情况下上面的代码与下面是等价的:

代码块
Python
自动换行
复制代码
iterator = iter(obj)
while True:
  try:
    item = next(iterator)
    print(item)
  except StopIteration:
    break
复制成功

第二种情况:

如果 obj 没有实现 __iter__ 方法,则康康有没有实现 __getitem__ 方法。

如果有,则从 0 开始,依次递增 1 来调用 __getitem__ 方法,返回值就是迭代出来的值,直到遇到 StopIteration,停止调用。

这种情况下上面的代码与下面是等价的:

代码块
Python
自动换行
复制代码
index = 0
while True:
  try:
    item = obj[index]
    print(item)
    index += 1
  except StopIteration:
    break
复制成功

如果以上两种方法都行不通,那就不能用 for 循环(会报TypeError: 'xxx&#​39; object is not iterable)


下面再来讲一讲如何实现可迭代对象和迭代器

实现可迭代对象和迭代器

我们试着模仿 range 函数写一个生成等差数列的可迭代对象

代码块
Python
自动换行
复制代码
class Range:
  def __init__(self, start, end, step=1):
    assert step > 0   # 简单起见,我们只处理步长 > 0 的情况
    self.start = start
    self.end   = end
    self.step  = step
复制成功

现在它既不是迭代器,也不可迭代

我们试着让它的迭代器就是它自己(实现 __iter__ 方法):

代码块
Python
自动换行
复制代码
class Range:
  def __init__(self, start, end, step=1):
    assert step > 0   # 简单起见,我们只处理步长 > 0 的情况
    self.start = start
    self.end   = end
    self.step  = step
  def __iter__(self):
    return self
复制成功

已经可迭代了,接下来让它成为迭代器

代码块
Python
自动换行
复制代码
class Range:
  def __init__(self, start, end, step=1):
    assert step > 0   # 简单起见,我们只处理步长 > 0 的情况
    self.start = start
    self.end   = end
    self.step  = step
    self.num   = start  # 保存迭代到哪一个数了,刚开始就是初始值
  def __iter__(self):
    return self
  def __next__(self):
    if self.num < self.end:              # 还可以继续迭代
      output = self.num
      self.num += self.step              # 增长步长
      return output                      # 返回值
    else:                                # 迭代到头了就触发 StopIteration
      raise StopIteration
复制成功

我们可以对它用 for 循环了:

代码块
Python
自动换行
复制代码
r = Range(2, 11, 2)
for i in r:
    print(i, end=' ')

# 输出:2 4 6 8 10
复制成功

但是,这种写法有问题,慢慢看来:

我们要输出一个 [1, 2, 3, 4, 5] 与它自己的笛卡尔积(不知道啥是笛卡尔积的看这里https://baike.baidu.com/item/%E7%AC%9B%E5%8D%A1%E5%B0%94%E4%B9%98%E7%A7%AF/6323173),即

代码块
Python
自动换行
复制代码
(1, 1) (1, 2) (1, 3) (1, 4) (1, 5) 
(2, 1) (2, 2) (2, 3) (2, 4) (2, 5) 
(3, 1) (3, 2) (3, 3) (3, 4) (3, 5) 
(4, 1) (4, 2) (4, 3) (4, 4) (4, 5) 
(5, 1) (5, 2) (5, 3) (5, 4) (5, 5) 
复制成功

传统写法是

代码块
Python
自动换行
复制代码
r = range(1, 6)                 #先定义一个 range 对象
for i in r:
    for j in r:                 # 双重循环
        print((i, j), end=' ')  # end=' ' 是不换行的意思
    print()                     # 只打印一个换行
复制成功

这很简单。但如果用咱们编的这个 Range,问题就出来了,看输出:

代码块
Python
自动换行
复制代码
(1, 2) (1, 3) (1, 4) (1, 5) 
复制成功

问题就出在:因为有 return self 的存在,两个 for 循环共用了一个迭代器(换而言之,这个 Range 对象只能用一次)!让这个 Range 对象既保存数据又保存状态是不行哒!

range 对象的 iter() 原理图

自定义的 Range 对象的 iter() 原理图

所以我们需要这里给出两种解决方案:

(一)自定义专门的迭代器

最本质的问题就是 __iter__ 方法里的 return self,我们再定义一个迭代器:

代码块
Python
自动换行
复制代码
class Range:
    def __init__(self, start, end, step=1):
        assert step > 0  # 简单起见,我们只处理步长 > 0 的情况
        self.start = start
        self.end = end
        self.step = step

    def __iter__(self):
        return RangeIterator(self.start, self.end, self.step)  # 返回迭代器


class RangeIterator:
    def __init__(self, num, end, step):
        self.num = num
        self.end = end
        self.step = step

    def __next__(self):
        if self.num < self.end:    # 还可以继续迭代
            output = self.num
            self.num += self.step  # 增长步长
            return output          # 返回值
        else:                      # 迭代到头了就触发 StopIteration
            raise StopIteration
复制成功

为了要满足第三条规则,即迭代器应该也是可迭代的,上面也说了,大部分就是返回自己,所以再给 RangeIterator 加上 __iter__ 方法:

代码块
Python
自动换行
复制代码
class Range:
    def __init__(self, start, end, step=1):
        assert step > 0  # 简单起见,我们只处理步长 > 0 的情况
        self.start = start
        self.end = end
        self.step = step

    def __iter__(self):
        return RangeIterator(self.start, self.end, self.step)  # 返回迭代器


class RangeIterator:
    def __init__(self, num, end, step):
        self.num = num
        self.end = end
        self.step = step

    def __next__(self):
        if self.num < self.end:    # 还可以继续迭代
            output = self.num
            self.num += self.step  # 增长步长
            return output          # 返回值
        else:                      # 迭代到头了就触发 StopIteration
            raise StopIteration
    def __iter__(self):
        return self
复制成功

这样,Range 对象就可以多次使用了,但 RangeIterator 作为迭代器本来就只能使用一次。

至此,一个迭代器已经实现了。

(二)使用生成器

我们下一章详细讨论生成器,这里不理解没关系(挖坑*1)

我们将 __iter__ 方法变成一个生成器函数:

代码块
JavaScript
自动换行
复制代码
class Range:
    def __init__(self, start, end, step=1):
        assert step > 0  # 简单起见,我们只处理步长 > 0 的情况
        self.start = start
        self.end = end
        self.step = step

    def __iter__(self):
        n = self.start
        while n < self.end:
            yield n
            n += self.step
复制成功

也能达到效果,且更加简洁,推荐使用这种方法。

至于我们刚才讨论的第二种情况,实际使用中很少使用,我们就不再赘述。

END


这算是给之前的一章填坑了...

参考资料:

https://docs.python.org/3.8/tutorial/classes.html

https://docs.python.org/3.8/library/stdtypes.html

https://baike.baidu.com/item/%E7%AC%9B%E5%8D%A1%E5%B0%94%E4%B9%98%E7%A7%AF/6323173

以上内容如有错误,欢迎指出!