Python 编写有Python特色的循环,对于有C语言风格背景的开发人员,若想知道他们是不是最近才使用Python,最简单的办法是观察他们如何编写循环。
例如,每当我看到类似下面的代码片段时,就知道有人试图用C或Java的风格编写Python代码:
my_items = ['a', 'b', 'c']
i = 0
while i < len(my_items):
print(my_items[i])
i += 1
这段代码看上去非常没有Python特色,有以下两点原因。
首先,代码中手动跟踪了索引i
,先初始化将其置为零,然后在每次循环迭代时仔细递增索引。
其次,为了确定迭代次数,使用len()
获取my_items
容器的大小。
在Python中编写的循环会自动处理这两个问题,最好善加利用这一点。例如,如果代码不必跟踪正在运行的索引,那么就很难写出意外的无限循环,同时代码也更简洁、更可读。
下面来重构第一个代码示例,首先将删除用于手动更新索引的代码。在Python中可以用for
循环来很好地做到这一点,做法是利用内置的range()
自动生成索引:
>>> range(len(my_items))
range(0, 3)
>>> list(range(0, 3))
[0, 1, 2]
range
类型表示不可变的数列,内存占用比普通列表少。range
对象实际上并不存储数列的每个值,而是充当迭代器实时计算数列的值。
所以可以利用range()
函数编写如下所示的内容,不用在每次循环迭代时手动递增i:
for i in range(len(my_items)):
print(my_items[i])
比之前好一点,但仍然不是很有Python特色,感觉依然像是一个Java风格的迭代结构,而不是正常的Python循环。像range(len(...))
这样的容器遍历方式通常可以进一步简化和改进它。
正如前面所提到的,在Python中,for
循环实际上是for-each
循环,可以直接在容器或序列中迭代元素,无须通过索引查找。因此可以用这一点来进一步简化:
for item in my_items:
print(item)
这个解决方案很有Python特色,其中使用了几种Python高级特性,不过仍然非常整洁,看上去就像是在阅读编程教科书中的伪代码一样。注意循环中不再跟踪容器的大小,也不使用运行时索引来访问元素。
容器本身现在负责分发将要处理的元素。如果容器是有序的,那么所得到的元素序列也是有序的。如果容器是无序的,那么将以随机顺序返回其元素,但循环仍然会遍历所有元素。
当然,并不是所有情况下都能以这种方式重写循环。如果需要用到项的索引,该怎么办呢?
有一种方法既能让循环持有当前运行的索引,又能避免前面提到的range(len(...))
模式。那就是使用内置的enumerate()
改进这种循环,让其变得具有Python特色:
>>> for i, item in enumerate(my_items):
... print(f'{i}: {item}')
0: a
1: b
2: c
看到没,Python中的迭代器可以连续返回多个值。迭代器可以返回含有任意个元素的元组,然后在for
语句内解包。
这个功能非常强大,比如可以使用相同的技术同时迭代字典的键和值:
>>> emails = {
... 'Bob': 'bob@example.com',
... 'Alice': 'alice@example.com',
... }
>>> for name, email in emails.items():
... print(f'{name} -> {email}')
'Bob -> bob@example.com'
'Alice -> alice@example.com'
还有一个例子,如果一定要编写一个C风格的循环,比如必须控制索引的步长,该怎么办呢?假如有下面这样的Java循环:
for (int i = a; i < n; i += s) {
// ...
}
如何将这种模式转到Python中?这里要再次用到range()
函数,该函数接受可选参数来控制循环的起始值(a
)、终止值(n
)和步长(s
)。因此前面的Java循环示例可转换成下面这种Python形式:
for i in range(a, n, s):
# ...
关键要点
-
在Python中编写C风格的循环非常没有Python特色。要尽可能地避免手动管理循环索引和终止条件。
-
Python的
for
循环实际上是for-each
循环,可直接在容器或序列中的元素上迭代。