09月17, 2020

49. Python 为什么需要 iterator?

有关 iterable 和 iterator 的定义介绍已经很多了:

  • 实现 iter 或者 getitem 方法的对象是 iterable 对象;
  • 实现 iternext 方法的对象是 iterator 对象;

那为什么需要 iterator 呢?iterator 实际上是 Python 的一种 interface,它是一种遍历序列(Sequeence) 的约定,通过调用 iterator 的 next 方法实现。所谓序列:List、Tuple、Dict、Set 等等,它们的底层实现差异很大,对于某些类型的底层数据甚至是不连续的,期间参杂着一些无效值(NULL、dummy 之类)。使用 iterator 这一接口,则将序列的遍历实现推给类型自己完成,调用者只需要根据 interface 的约定,调用 next 方法即可。

List 的 next 实现

static PyObject *
listiter_next(listiterobject *it)
{
    // ··· ···

    // it_seq 就是迭代器的目标 list
    seq = it->it_seq;

    // 还有 list item 可读
    if (it->it_index < PyList_GET_SIZE(seq)) {
        item = PyList_GET_ITEM(seq, it->it_index);
        ++it->it_index;
        Py_INCREF(item);
        return item;
    }

    // list 所有成员已遍历完毕,结束
    it->it_seq = NULL;
    Py_DECREF(seq);
    return NULL;
}

对于 List 而言,它的 items 是连续的,在 next 中通过递增 it->it_index,然后通过 PyList_GET_ITEM 读取数据。其实 List 的 next 方法是将经典的 C 语言 for 循环的计数变量放到 iterator 内部了,其实际效果与 C 下用 for 循环遍历数组差别不大。 与 Python 遍历 List 等效的 C 语言代码:

for(it->it_index = 0; it->it_index < PyList_GET_SIZE(seq), it->it_index ++){
     // visit list[it->it_index]
}

Set

Dict、Set 之类容器高度依赖哈希表,其底层数据之间存在一些空缺、无效数据等,对于上层代码,如果直接遍历这些底层数据,就需要妥善处理这些特殊情况。

static int
set_next(PySetObject *so, Py_ssize_t *pos_ptr, setentry **entry_ptr)
{
    // ··· ···

    // 忽略那些无效值
    while (i <= mask && (entry->key == NULL || entry->key == dummy)) {
        i++;
        entry++;
    }

    // ··· ···
}

在 iterator 的 next 中,自动忽略了那些无效值,上层代码可以无差别的对待 Set 之类比较特殊的数据。与 Python 遍历 Set 的等效 C 代码:

for(i = 0; i <= mask; i++, entry ++){
   if(entry->key == NULL || entry->key == dummy){
      continue;
   }

   // visit entry here
}

总结

Python 的遍历实际上与 C、Java 之类的循环方案,在形式和思想上采用了完全不同的策略,不同于 C 之类的 for 循环直接访问具体的元素,Python 的 for 循环更接近 js 的 foreach 实现,它通过约定的接口,由类型自行返回 next item。这种设计的主要目的是为了隔离不同类型底层实现的差异,否则就会出现如同上面提到的等效代码之间的差异,破坏了 Python 的动态性。

底层的 C 语言并不原生支持动态类型,Python 语言的优雅是通过大量标准接口实现的,将类型的差异约束在类型内部,对外实现标准的接口,这一点从 Python 的 PyObject 结构就可以看出来。

到此为止,我们已经将 Python 的主要类型都已涉及,明天开始我们终于可以深入 Python 的 VM 机制。需要注意的是,Python 的类型系统、内存管理同时提供了 C 和 Python 两种调用方式,在 Python VM 中我们会看到大量熟悉的类型被应用于 VM 的内部数据。Python 在整个系统的设计中,实现了相当程度的自举,Python 自身的实现也高度依赖 Python 特性,这一点非常有意思,让我们拭目以待。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_49.html

-- EOF --