Python for 循环的一些细节

露娜要拿蓝啊 · 2020年07月16日 · 1127 次阅读

天天在用的 for 循环,还老是遇上坑。所以晚上有时间,写了一篇对 for 循环的解释,做个记录。同时对循环有一个更深入的理解吧。

下面是一个 Python 中 for 循环最基本的使用:

nums = [1,2,3,4,5,6]
for num in nums:
        print(num)

来对比一下 Java 中的 for 循环,先上代码

int[] nums = [1,2,3,4,5,6,7];
for(int i =0;i<nums.length();i++){
    System.out.print(i);
}

这两种方式除了语法之外还有什么区别呢?先来研究下 Python 中的 for 循环:

Python 中的 for 语句与你在 C 或 Pascal 中所用到的有所不同。 Python 中的 for 语句并不总是对算术递增的数值进行迭代(如同 Pascal),或是给予用户定义迭代步骤和暂停条件的能力(如同 C),而是对任意序列进行迭代(例如列表或字符串),条目的迭代顺序与它们在序列中出现的顺序一致。 例如(此处英文为双关语):

这是 Python 3.8 文档中对于 Python for 语句的解释。这里来看一下 对任意序列进行迭代(例如列表或字符串)这句话:

先来解释一下几个名词。任意序列。

所谓序列,指的是一块可存放多个值的连续内存空间,这些值都按一定顺序排列,可通过每个值所在的编号(称为索引)来访问他们

学过数据结构课的同学可以知道,数组就是一种序列,他在内存中就是一块连续的内存空间,具有快速随机访问的特性。对于 Python 中来说,这个任意序列则可以指listdicttuple等,因为这些类型,Python 在底层都对他们做了不同的实现,比如说list 本质上是一个长度可变的连续数组,有需要的同学可以更深入的去了解一下具体的实现。

再来说一下迭代的概念:

迭代 一般指更新换代,迭代操作对应 Python 中的 for 循环等内置工具。这里有两个重要的概念,可迭代对象迭代器,放一下 Python 官方文档中对于迭代器的描述:

Python 支持在容器中进行迭代的概念。 这是通过使用两个单独方法来实现的;它们被用于允许用户自定义类对迭代的支持。 将在下文中详细描述的序列总是支持迭代方法。
容器对象要提供迭代支持,必须定义一个方法:
container.**iter**()
返回一个迭代器对象。 该对象需要支持下文所述的迭代器协议。 如果容器支持不同的迭代类型,则可以提供额外的方法来专门地请求不同迭代类型的迭代器。(支持多种迭代形式的对象的例子有同时支持广度优先和深度优先遍历的树结构。)此方法对应于 Python/C API 中 Python 对象类型结构体的 tp_iter 槽位。
迭代器对象自身需要支持以下两个方法,它们共同组成了 迭代器协议:
iterator.**iter**()
返回迭代器对象本身。 这是同时允许容器和迭代器配合 for 和 in 语句使用所必须的。 此方法对应于 Python/C API 中 Python 对象类型结构体的 tp_iter 槽位。
iterator.**next**()
从容器中返回下一项。 如果已经没有项可返回,则会引发 StopIteration 异常。 此方法对应于 Python/C API 中 Python 对象类型结构体的 tp_iternext 槽位。
Python 定义了几种迭代器对象以支持对一般和特定序列类型、字典和其他更特别的形式进行迭代。 除了迭代器协议的实现,特定类型的其他性质对迭代操作来说都不重要。
一旦迭代器的 next() 方法引发了 StopIteration,它必须一直对后续调用引发同样的异常。 不遵循此行为特性的实现将无法正常使用。

翻译一下,首先这个容器也就是前面所说的任意序列,需要实现一个iter() 的方法,这样子可以返回一个迭代器对象,然后这个对象呢自己要遵守这个协议,也就是迭代器协议,满足了以上条件,那么你就是一个合格的迭代器了,可以被 for 循环进行迭代了。

迭代器的协议呢就是你要实现这两个方法,一个是返回对象本身的iter(),一个呢是返回下一项的next() ,同时呢如果没有下一项,你要抛出一个异常情况,即StopIteration 异常。字面理解就是停止迭代的意思。

思考一下,为什么需要实现这两个方法呢?

首先iter()方法返回是对象本身,通过调用next()的方法不断迭代更新。这个就是 for 循环的本质。

那么为什么不一次性的加载到内存呢?这就是迭代器的优势,它不像列表定义的时候那样,如果有一千万个数字构成的列表,那么列表会一次性的加载到内存中,这会占用超过 400M 的内存,而使用迭代器的话,它只会占用几十个字节的空间,因为他没有加载所有元素到内存,而是只有在迭代过程中不断调用next()方法才返回该元素。(这与懒加载的方式有点相似)

再来看看 Java 中的 for 循环,Java 中目前有三种形式的 for 循环实现:

for (int i = 0; i < list.size(); i++) {
    System.out.print(list.get(i) + ",");
}

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.print(iterator.next() + ",");
}

for (Integer i : list) {
    System.out.print(i + ",");
}

第一种是最普通的 for 循环,通过循环数字的方式来控制流程,就不细说了。

第二种则是类似于 Python 中的迭代器,也是调用了next()方法来返回下一项。

第三种一般称为增强 for,也就是 foreach,是 Java 提供的一种语法糖,反编译后也可以看到他的底层实现也是迭代器模式,但是不需要自己生成一个迭代器对象。

对比一下,Python 中和 Java 中的 for 循环,在迭代器方面的使用基本是一致的,都遵守了迭代器的协议。

在使用 for 循环过程中,还有一个很容易遇到的坑:

Python 官方文档中有一段话:

在遍历同一个集合时修改该集合的代码可能很难获得正确的结果。通常,更直接的做法是循环遍历该集合的副本或创建新集合:

# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]

# Strategy:  Create a new collection
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

这是因为在你循环遍历时,当原来的对象数量发生变化时,比如进行了删除的操作,那么这个迭代器的索引内容不会同步改变,在 Python 中会发现输出内容中会有重复项,在 Java 中则会抛出异常。

针对于这个问题,有多种处理方法,一种就是官方所说,创建副本或新集合来进行删除操作,这样就不会引起迭代器中的内容了。还有一种就是在 Java 中可以调用迭代器本身的remove()方法,而不是 list 的remove()方法,这样就会维护索引的一致了。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册