非常简单的 Python:循环和迭代器
循环概述
一些容器
拆开容器
怪in
形
迭代器
你自己迭代器
期待
审查
喜欢这些文章吗?那就买本书吧! Jason C. McDonald 的《Dead Simple Python》现已由 No Starch Press 出版。
还记得上次丢失东西的情况吗?
你可能为了找它把家里翻了个底朝天。你一个房间一个房间地翻找,周围的人却总是问些毫无意义的问题,比如“你上次把它们放哪儿了?”(说真的,要是我知道的话,我肯定不会去找它们!)优化一下搜索方式当然很好,但你的房子并没有整理好……或者说,如果你和我一样,房子的布局不是很合理。你只能用线性搜索的方式。
在编程中,就像在现实生活中一样,我们通常不会以任何有意义的顺序收到数据。我们一开始会面对一堆乱七八糟的数据,然后需要对它们执行一些任务。搜索无序数据可能是你首先想到的例子,但你可能还想做数百件其他的事情:将所有华氏温度记录转换为摄氏度,求所有数据点的平均值,等等。
“是的,是的,这就是循环的用途!”
但这是 Python。这里的循环完全是另一个层次。它们好得简直是犯罪。
循环概述
让我们把那些无聊的东西解决掉,好吗?
在 Python 中,与大多数语言一样,我们有两个基本循环:while
和for
。
while
循环while
是非常基本的。
clue = None
while clue is None:
clue = searchLocation()
只要循环条件(clue is None
在本例中为)计算结果为True
,循环的代码就会被执行。
在 Python 中,我们还有几个有用的关键字:break
立即停止循环,而continue
跳到循环的下一次迭代。
最有用的方面之一break
是,如果我们想运行相同的代码,直到用户提供有效输入。
while True:
try:
age = int(input("Enter your age: "))
except ValueError:
print(f"Please enter a valid integer!")
else:
if age > 0:
break
一旦遇到该break
语句,我们就退出循环。诚然,这是一个相当复杂的例子,但它说明了这一点。你也经常while True:
在游戏循环中看到它。
陷阱提醒:如果你曾经使用过任何语言的循环,那么你已经熟悉无限循环了。这通常是由于循环中while
始终求值为条件True
,且没有语句而导致的。break
for
如果你之前用过 Java、C++ 或许多类似的 ALGOL 风格语言,你可能对三部分for
循环很熟悉:for i := 1; i < 100; i := i + 1
。我不知道你是怎么想的,但我第一次遇到它的时候,吓得魂飞魄散。现在我习惯了,但它就是不具备 Python 那种优雅简洁的特性,不是吗?
Python 的for
循环看起来截然不同。与上述伪代码等效的 Python 代码是……
for i in range(1,100):
print(i)
range()
是 Python 中一个特殊的“函数”,用于返回一个序列。(严格来说,它根本就不是一个函数,不过这么说有点儿太迂腐了。)
这是 Python 令人印象深刻的地方——它迭代一种特殊类型的序列,称为可迭代序列,我们稍后会讨论。
现在,最容易理解的是,我们可以迭代顺序数据结构,比如数组(在 Python 中称为“列表”)。
因此,我们可以这样做……
places = ['Nashville', 'Norway', 'Bonaire', 'Zimbabwe', 'Chicago', 'Czechoslovakia']
for place in places:
print(place)
print("...and back!")
...我们得到了这个...
Nashville
Norway
Bonaire
Zimbabwe
Chicago
Czechoslovakia
...and back!
for...else
Python 的循环中还有一个独特的小技巧:else
子句!循环完成后,如果未遇到break
语句,它将运行 中的代码else
。但是,如果手动跳出 中的循环,它将else
完全跳过 中的代码。
places = ['Nashville', 'Norway', 'Bonaire', 'Zimbabwe', 'Chicago', 'Czechoslovakia']
villain_at = 'Mali'
for place in places:
if place == villain_at:
print("Villain captured!")
break
else:
print("The villain got away again.")
由于“马里”不在列表中,我们会看到消息“恶棍再次逃脱。”但是,如果我们将的值更改为villain_at
,我们将看到“恶棍Norway
被捕获!” 。
在哪里do
?
Python 没有do...while
循环。如果你想找一个,典型的 Python 惯例是使用while True:
带有内部break
条件的循环,就像我们之前演示的那样。
一些容器
Python 有许多容器(或数据结构)来保存数据。我们不会深入探讨这些容器,但我想快速浏览一下最重要的几个:
list
Alist
是一个可变序列(基本上是一个数组)。
它用方括号定义[ ]
,您可以通过索引访问其元素。
foo = [2, 4, 2, 3]
print(foo[1])
>>> 4
foo[1] = 42
print(foo)
>>> [2, 42, 2, 3]
虽然没有严格的技术要求,但典型的惯例是列表仅包含同一类型(“同质”)的项目。
tuple
Atuple
是一个不可变序列。一旦定义了它,从技术上讲就无法更改它(回想一下之前不可变性的含义)。这意味着在元组定义之后,你就无法添加或删除元素。
元组在括号内定义( )
,可以通过索引访问其元素。
foo = (2, 4, 2, 3)
print(foo[1])
>>> 4
foo[1] = 42
>>> TypeError: 'tuple' object does not support item assignment
与列表不同,标准约定允许元组包含不同类型的元素(“异构”)。
set
Aset
是一个无序可变集合,保证不包含重复项。“无序”这一点需要牢记:单个元素的顺序无法保证!
集合的定义在花括号内{ }
,但如果你想要一个空集,则必须使用foo = set()
,就像foo = {}
创建一个 一样dict
。由于它是无序的,因此无法通过索引访问其元素。
foo = {2, 4, 2, 3}
print(foo)
>>> {2, 3, 4}
print(foo[1])
>>> TypeError: 'set' object does not support indexing
要将对象添加到集合中,它还必须是可哈希的。如果满足以下条件,则对象是可哈希的:
-
它定义了一个方法
__hash__()
,该方法返回一个整数形式的哈希值。(见下文) -
__eq__()
它定义了比较两个对象的方法。
对于同一个对象(值),有效的哈希值应该始终相同,并且应该合理地保持唯一性,这样其他对象返回相同哈希值的情况就比较少见了。(两个或多个对象具有相同的哈希值称为哈希冲突,这种情况仍然会发生。)
字典 ( dict
)
Adict
是一个键值数据结构。
它定义在花括号内{ }
,用于分隔键和值。它是无序的,因此无法通过索引访问其元素;但是,可以用方括号以类似的方式:
指示键。[ ]
foo = {'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4}
print(foo['b'])
>>> 2
foo['b'] = 42
print(foo)
>>> {'a': 1, 'b': 42, 'c': 3, 'd': 4}
只有可哈希对象才可以用作字典键。(set
有关可哈希性的更多信息,请参阅上文部分。)
其他数据结构
collections
除了基本容器之外,Python 还提供了其他容器。您可以在内置模块中找到它们。
拆开容器
有一个重要的 Python 语法我们还没讲到,但很快就会用到。我们可以把容器里的每个元素赋值给一个变量!这叫做解包。
当然,我们需要确切地知道我们要拆包多少件物品才能使其正常工作,否则我们会遇到ValueError
异常。
让我们看一个使用元组的基本示例。
fullname = ('Carmen', 'Sandiego')
first, last = fullname
print(first)
>>> Carmen
print(last)
>>> Sandiego
秘诀就在第二行。我们可以列出多个要赋值的变量,用逗号分隔。Python 会在等号右侧解压容器,并按从左到右的顺序将每个值赋给相应的变量。
陷阱警报:记住,set
是无序的!虽然理论上你可以用集合来实现这一点,但你无法确定赋值给哪个变量的值。它不能保证任何顺序;集合通常按排序顺序解包其值,这只是偶然的,并不能保证!
怪in
形
Python 提供了一个漂亮的关键字,in
用于检查是否在容器内找到特定元素。
places = ['Nashville', 'Norway', 'Bonaire', 'Zimbabwe', 'Chicago', 'Czechoslovakia']
if 'Nashville' in places:
print("Music city!")
这适用于许多容器,包括列表、元组、集合,甚至字典键(但不是字典值)。
如果您希望您的某个自定义类支持该in
运算符,则只需要定义__contains__(self, item)
返回True
或 的方法False
。(请参阅文档)。
迭代器
Python 的循环设计用于处理我之前提到的可迭代对象。这些对象可以使用迭代器进行迭代。
蟋蟀的声音。
好的,让我们从头开始。Python 容器对象(例如 a list
)也是一个可迭代对象,因为它__iter__()
定义了一个方法,该方法返回一个迭代器对象。
迭代器定义为一个__next__()
方法,对于容器迭代器,该方法返回下一个项。即使是无序容器(例如set()
),也可以使用迭代器进行遍历。
当没有其他任何内容可以返回时__next__()
,它会抛出一个称为 的特殊异常StopIteration
。可以使用典型的 来捕获和处理该异常try...except
。
让我们再次看一下for
遍历的循环list
,例如......
dossiers = ['The Contessa', 'Double Trouble', 'Eartha Brute', 'Kneemoi', 'Patty Larceny', 'RoboCrook', 'Sarah Nade', 'Top Grunge', 'Vic the Slick', 'Wonder Rat']
for crook in dossiers:
print(crook)
dossiers
是一个list
对象,它是一个可迭代对象。当 Python 到达for
循环时,它会做三件事:
-
调用
iter(dossiers)
,进而执行dossiers.__iter__()
。这将返回一个迭代器对象,我们将它称为list_iter
。此迭代器对象将由循环使用。 -
对于循环的每次迭代,它都会调用
next(list_iter)
,从而执行list_iter.__next__()
,并将返回值分配给crook
。 -
如果迭代器抛出特殊异常
StopIteration
,则循环结束,我们退出。
如果我循环重写该逻辑,可能会更容易理解这一点while True:
......
list_iter = iter(dossiers)
while True:
try:
crook = next(list_iter)
print(crook)
except StopIteration:
break
如果您尝试这两个循环,您会发现它们执行完全相同的操作!
了解了、和异常的工作原理__iter__()
,__next__()
您StopIteration
现在可以使自己的类可迭代!
黑客警报:虽然将迭代器类与可迭代类分开定义很常见,但并非必须如此!只要两个方法都在类中定义,并且__next__()
行为正常,就可以直接定义__iter__()
到return self
。
值得注意的是,迭代器本身是可迭代的:它们有一个__iter__()
返回的方法self
。
《词典奇案》
假设我们有一本想要使用的字典......
locations = {
'Parade Ground': None,
'Ste.-Catherine Street': None,
'Pont Victoria': None,
'Underground City': None,
'Mont Royal Park': None,
'Fine Arts Museum': None,
'Humor Hall of Fame': 'The Warrant',
'Lachine Canal': 'The Loot',
'Montreal Jazz Festival': None,
'Olympic Stadium': None,
'St. Lawrence River': 'The Crook',
'Old Montréal': None,
'McGill University': None,
'Chalet Lookout': None,
'Île Notre-Dame': None
}
如果我们只想查看其中的每个项目,那么使用for
循环就足够了。这样应该可以了,对吧?
for location in locations:
print(location)
哎呀!这只显示了键,没有显示值。这绝对不是我们想要的,对吧?这到底是怎么回事?
dict.__iter__()
返回一个dict_keyiterator
对象,该对象按照其类名所示执行操作:它迭代键,但不迭代值。
要获取键和值,我们需要调用locations.items()
,它返回dict_items
对象。dict_items.iter()
返回一个dict_itemiterator
,它将以元组的形式返回字典中的每个键值对。
旧注意事项:如果您使用的是 Python 2,则应locations.iteritems()
改为调用。
还记得之前我们讨论过解包吗?我们将每对元素作为一个元组来处理,这意味着我们可以将它们解包成两个变量。
for key, value in locations.items():
print(f'{key} => {value}')
打印出以下内容:
Parade Ground => None
Ste.-Catherine Street => None
Pont Victoria => None
Underground City => None
Mont Royal Park => None
Fine Arts Museum => None
Humor Hall of Fame => The Warrant
Lachine Canal => The Loot
Montreal Jazz Festival => None
Olympic Stadium => None
St. Lawrence River => The Crook
Old Montréal => None
McGill University => None
Chalet Lookout => None
Île Notre-Dame => None
啊,差不多了!现在我们可以处理数据了。比如,我可能想把重要的信息记录到另一本词典里。
information = {}
for location, result in locations.items():
if result is not None:
information[result] = location
# Win the game!
print(information['The Loot'])
print(information['The Warrant'])
print(information['The Crook'])
print("Vic the Slick....in jaaaaaaaaail!")
这将找到 Loot、Warrant 和 Crook,并按正确的顺序列出它们:
Lachine Canal
Humor Hall of Fame
St. Lawrence River
Vic the Slick....in jaaaaaaaaail!
瞧,循环和迭代器的打击犯罪的力量!
你自己迭代器
我之前已经提到过,您可以创建自己的可迭代对象和迭代器,但展示比讲述更好!
假设我们想要保存一份特工列表,以便随时通过他们的特工编号识别他们。但是,有些特工我们无法提及。我们可以很容易地通过将特工 ID 和姓名存储在字典中,然后维护一个分类特工列表来实现这一点。
提示:记住,根据我们关于类的讨论,Python 中实际上并不存在私有变量。如果您真的想保守秘密,请使用行业标准的加密和安全措施,或者至少不要将您的 API 暴露给任何恶意人员。;)
首先,这是该类的基本结构:
class AgentRoster:
def __init__(self):
self._agents = {}
self._classified = []
def add_agent(self, name, number, classified=False):
self._agents[number] = name
if classified:
self._classified.append(name)
def validate_number(self, number):
try:
name = self._agents[number]
except KeyError:
return False
else:
return True
def lookup_agent(self, number):
try:
name = self._agents[number]
except KeyError:
name = "<NO KNOWN AGENT>"
else:
if name in self._classified:
name = "<CLASSIFIED>"
return name
我们可以继续进行测试,只是为了以后使用:
roster = AgentRoster()
roster.add_agent("Ann Tickwitee", 2539634)
roster.add_agent("Ivan Idea", 1324595)
roster.add_agent("Rock Solid", 1385723)
roster.add_agent("Chase Devineaux", 1495263, True)
print(roster.validate_number(2539634))
>>> True
print(roster.validate_number(9583253))
>>> False
print(roster.lookup_agent(1324595))
>>> Ivan Idea
print(roster.lookup_agent(9583253))
>>> <NO KNOWN AGENT>
print(roster.lookup_agent(1495263))
>>> <CLASSIFIED>
太棒了,一切正如预期!现在,如果我们想要循环遍历整个字典,比如想在某个很棒的代码里,在漂亮的全球地图上显示他们的名字和当前位置,那该怎么办呢?
但是,我们不想roster._agents
直接访问字典,因为那样会忽略这个类的“分类”属性。该如何处理呢?
正如我之前提到的,我们可以让这个类也充当它自己的迭代器,这意味着它有一个__next__()
方法。在这种情况下,我们只需要返回self
。但是,这已经是极简 Python 了,所以我们还是跳过这些烦人的简化操作,直接创建一个单独的迭代器类吧。
在这个例子中,我实际上会把这个字典转换成一个元组列表,这样我就可以使用索引了。(记住,字典是无序的。)我还会找出有多少代理未__init__()
分类。当然,所有这些逻辑都包含在方法中:
class AgentRoster_Iterator:
def __init__(self, container):
self._roster = list(container._agents.items())
self._classified = container._classified
self._max = len(self._roster) - len(self._classified)
self._index = 0
要成为迭代器,该类必须有一个__next__()
方法;这是唯一的要求!记住,StopException
一旦没有更多数据可返回,该方法就需要抛出。
我将定义AgentRoster_Iterator
的__next__()
方法如下:
class AgentRoster_Iterator:
# ...snip...
def __next__(self):
if self._index == self._max:
raise StopIteration
else:
r = self._roster[self._index]
self._index += 1
return r
现在我们回到AgentRoster
类,我们需要添加一个__iter__()
返回适当迭代器对象的方法。
class AgentRoster:
# ...snip...
def __iter__(self):
return AgentRoster_Iterator(self)
只需要一点点魔法,现在我们的AgentRoster
类就能按照预期循环运行了!代码如下……
roster = AgentRoster()
roster.add_agent("Ann Tickwitee", 2539634)
roster.add_agent("Ivan Idea", 1324595)
roster.add_agent("Rock Solid", 1385723)
roster.add_agent("Chase Devineaux", 1495263, True)
for number, name in roster:
print(f'{name}, id #{number}')
...产生...
Ann Tickwitee, id #2539634
Ivan Idea, id #1324595
Rock Solid, id #1385723
期待
我听到后面的 Pythonista 说:“等等,等等,我们还没完呢!你甚至还没有触及列表推导呢!”
Python 确实在循环和迭代器之上添加了一层额外的魔法,它使用了一种名为生成器的特殊工具。这种类型的类提供了另一个令人难以置信的工具,称为推导式,它就像一个用于创建数据结构的紧凑循环。
我还特意省略了诸如zip()
和 之类的优点enumerate()
,它们使循环和迭代更加强大。我本来想在这里包含它们,但我不想让文章太长。(这已经够长了。)我稍后也会谈到这些。
我看到你们中的一些人已经兴奋不已,但遗憾的是,你们必须等到下一篇文章才能了解更多信息。
审查
让我们回顾一下本节中最重要的概念:
while
只要循环条件为 ,循环就会运行True
。- 您可以使用关键字跳出循环
break
,或者使用关键字跳到下一次迭代continue
。 - 循环
for
对可迭代对象(可以迭代的对象)进行迭代,例如列表。 - 该
range()
函数返回一个可迭代的数字序列,可以在for
循环中使用,例如for i in range(1, 100)
。 - Python 没有
do...while
循环。请使用while True:
带有明确 break 语句的循环。 - Python 有四种基本数据结构或容器:
- 列表是可变的、有序的、连续的结构……基本上就是数组。
- 元组是不可变的、有序的、序列化的结构。它类似于列表,但你无法修改其内容。
- 集合是可变的、无序的结构,保证不会包含任何重复元素。它们只能存储可哈希对象。
- 字典是可变的、无序的结构,用于存储键值对。您可以按键查找项,而不是通过索引。只有可哈希对象可以用作键。
- 您可以使用约定将容器的值解包
a, b, c = someContainer
为多个变量。左侧的变量数量和右侧容器中的元素数量必须相同! - 你可以使用关键字快速检查元素是否位于容器中
in
。如果你希望你的类支持此功能,请定义该contains()
方法。 - Python 的容器就是可迭代对象的例子:它们返回可以遍历其内容的迭代器。可迭代对象总是通过其
iter()
方法返回一个迭代器对象。 - 迭代器对象总是有一个
next()
返回值的方法。容器迭代器的next()
方法将返回容器中的下一个元素。当没有更多可返回的内容时,迭代器将引发StopIteration
异常。
Ned Batchelder 有一场关于迭代器和循环的精彩演讲,题为“像本地人一样循环”。强烈推荐大家去听一听!
另外,像往常一样,请务必阅读文档。循环、容器和迭代器还有很多其他用途。
感谢deniska
、、grym
和ikanobori
(Freenode IRC #python
)提出的修改建议。