python 循环迭代
Python的for
循环与其他语言中的for
循环不同。 在本文中,我们将深入探讨Python的for
循环,以了解它们在幕后如何工作以及它们为何以这种方式工作。
我们将通过了解一些“陷阱”来开始我们的旅程。 在了解了Python中循环的工作原理之后,我们将再次了解这些陷阱并解释发生了什么。
假设我们有一个数字列表和一个生成器,它将为我们提供这些数字的平方:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> squares = ( n** 2 for n in numbers )
我们可以将生成器对象传递给tuple
构造函数以从中生成一个tuple:
>>> tuple ( squares )
( 1 , 4 , 9 , 25 , 49 )
如果然后使用相同的生成器对象并将其传递给sum
函数,则可能期望得到这些数字的总和,即88。
>>> sum ( squares )
0
相反,我们得到0
。
让我们采用相同的数字列表和相同的生成器对象:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> squares = ( n** 2 for n in numbers )
如果我们问是否9
是在我们的squares
发电机,Python会告诉我们,9 是 squares
。 但是,如果我们再次问同样的问题 ,Python会告诉我们9 不是 squares
。
>>> 9 in squares
True
>>> 9 in squares
False
我们问了同样的问题两次,Python给了我们两个不同的答案。
该词典有两个键值对:
>>> counts = {'apples': 2, 'oranges': 1}
让我们使用多个分配来解开字典:
>>> x, y = counts
您可能希望在解压缩此字典时,会得到键值对,或者会出错。
但是解压缩字典不会引发错误,也不会返回键值对。 解压缩字典时,您会得到以下密钥:
>>> x
'apples'
Python没有传统的for
循环。 为了解释我的意思,让我们看一下另一种编程语言中的for
循环。
这是用JavaScript编写的传统C风格的for
循环:
let numbers = [ 1 , 2 , 3 , 5 , 7 ] ;
for ( let i = 0 ; i < numbers. length ; i += 1 ) {
print ( numbers [ i ] )
}
JavaScript,C,C ++,Java,PHP和其他所有编程语言都具有这种for
循环。 但是Python 没有 。
Python 没有传统的C风格的for
循环。 在Python中,确实有一些我们称为 for
循环的东西,但是它就像foreach循环一样工作 。
这是Python的for
循环:
numbers = [ 1 , 2 , 3 , 5 , 7 ]
for n in numbers:
print ( n )
与传统的C样式for
循环不同,Python的for
循环没有索引变量。 没有索引初始化,边界检查或索引递增。 Python的for
循环为我们完成了遍历numbers
列表的所有工作 。
因此,尽管我们在Python中确实有for
循环,但我们没有传统的C风格的for
循环。 我们称之为for循环的东西的工作方式大不相同。
既然我们已经解决了Python室中无索引的for
循环,那么让我们排除一些定义。
可迭代是您可以在Python中使用for
循环for
循环的任何内容。 可迭代对象可以循环,任何可以循环的对象都是可迭代的。
for item in some_iterable:
print ( item )
序列是一种非常常见的可迭代类型。 列表,元组和字符串都是序列。
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> coordinates = ( 4 , 5 , 7 )
>>> words = "hello there"
序列是具有一组特定功能的可迭代对象。 它们可以从0
开始索引,并以比序列长度小的1结束索引,它们具有长度,可以对其进行切片。 列表,元组,字符串和所有其他序列都以这种方式工作。
>>> numbers [ 0 ]
1
>>> coordinates [ 2 ]
7
>>> words [ 4 ]
'o'
Python中的很多东西都是可迭代的,但并非所有可迭代的都是序列。 集,字典,文件和生成器都是可迭代的,但是这些都不是序列。
>>> my_set = { 1 , 2 , 3 }
>>> my_dict = { 'k1' : 'v1' , 'k2' : 'v2' }
>>> my_file = open ( '' )
>>> squares = ( n** 2 for n in my_set )
因此,可以通过for
循环进行循环的任何事物都是可迭代的,而序列是可迭代的一种类型,但是Python也具有许多其他类型的可迭代。
您可能会认为Python的for
循环使用索引进行循环。 在这里,我们使用while
循环和索引手动遍历可迭代对象:
numbers = [ 1 , 2 , 3 , 5 , 7 ]
i = 0
while i < len ( numbers ) :
print ( numbers [ i ] )
i + = 1
这适用于列表,但不能全部使用。 这种循环方式仅适用于序列 。
如果我们尝试使用索引手动遍历集合,则会收到错误消息:
>>> fruits = { 'lemon' , 'apple' , 'orange' , 'watermelon' }
>>> i = 0
>>> while i < len ( fruits ) :
... print ( fruits [ i ] )
... i + = 1
...
Traceback ( most recent call last ) :
File "<stdin>" , line 2 , in < module >
TypeError : 'set' object does not support indexing
集不是序列,因此它们不支持索引。
我们无法使用索引手动遍历Python中的所有可迭代对象。 这对于不是序列的可迭代对象根本不起作用。
因此,我们已经看到Python的for
循环一定不能在后台使用索引。 相反,Python的for
循环使用迭代器 。
迭代器是为迭代提供动力的东西。 您可以从任何可迭代的对象中获取一个迭代器。 而且,您可以使用迭代器手动遍历它来自的可迭代对象。
让我们看看它是如何工作的。
这是三个可迭代项:集合,元组和字符串。
>>> numbers = { 1 , 2 , 3 , 5 , 7 }
>>> coordinates = ( 4 , 5 , 7 )
>>> words = "hello there"
我们可以使用Python的内置iter
函数向这些可迭代对象的每一个请求一个迭代器 。 将iterable传递给iter
函数,无论我们使用哪种类型的iterable,总是会给我们提供迭代器。
>>> iter ( numbers )
< set_iterator object at 0x7f2b9271c860 >
>>> iter ( coordinates )
< tuple_iterator object at 0x7f2b9271ce80 >
>>> iter ( words )
< str_iterator object at 0x7f2b9271c860 >
一旦有了迭代器,我们可以做的一件事就是通过将其传递给内置的next
函数来获取其下一项。
>>> numbers = [ 1 , 2 , 3 ]
>>> my_iterator = iter ( numbers )
>>> next ( my_iterator )
1
>>> next ( my_iterator )
2
迭代器是有状态的,这意味着一旦您从迭代器中消费了一个项目,它就消失了。
如果从迭代器中请求next
一项,并且没有其他项,则将收到StopIteration
异常:
>>> next ( my_iterator )
3
>>> next ( my_iterator )
Traceback ( most recent call last ) :
File "<stdin>" , line 1 , in < module >
StopIteration < /module >< /stdin >
因此,您可以从每个可迭代对象中获取一个迭代器。 您对迭代器唯一可以做的就是使用next
函数询问他们的下一项。 如果将它们传递给next
但它们没有下一个项目,则将引发StopIteration
异常。
您可以将迭代器视为无法重新加载的Pez分配器。 您可以将Pez取出,但是一旦将Pez取下就无法将其放回去,并且一旦分配器空了,它就没用了。
既然我们已经了解了迭代器以及iter
和next
函数,我们将尝试手动遍历可迭代对象而不使用for
循环。
我们将通过尝试将此for
循环转换为while
循环来做到这一点:
def funky_for_loop ( iterable , action_to_do ) :
for item in iterable:
action_to_do ( item )
为此,我们将:
for
循环的主体 StopIteration
异常,请停止循环 def funky_for_loop ( iterable , action_to_do ) :
iterator = iter ( iterable )
done_looping = False
while not done_looping:
try :
item = next ( iterator )
except StopIteration :
done_looping = True
else :
action_to_do ( item )
我们刚刚通过使用while
循环和迭代器重新构造了for
循环。
上面的代码几乎定义了Python内幕循环的工作方式。 如果您了解内置的iter
和next
函数对事物for
循环的工作方式,那么您将了解Python的for
循环的工作方式。
实际上,您将了解的不仅仅是Python中for
循环的工作原理。 遍历可迭代对象的所有形式都以这种方式工作。
迭代器协议是一种“在Python中如何遍历可迭代对象”的奇特方法。 从本质iter
,这是iter
和next
函数在Python中工作方式的定义。 Python中的所有形式的迭代都由迭代器协议提供支持。
迭代器协议由for
循环使用(如我们所见):
for n in numbers:
print ( n )
多重分配也使用迭代器协议:
x, y, z = coordinates
星号表达式使用迭代器协议:
a , b , *rest = numbers
print ( *numbers )
许多内置函数依赖于迭代器协议:
unique_numbers = set(numbers)
Python中任何可迭代的东西都可能以某种方式使用迭代器协议。 每当您在Python中遍历可迭代对象时,您都在依赖迭代器协议。
因此,您可能会想:迭代器看起来很酷,但它们似乎也像是实现细节,作为Python的用户,我们可能不需要关心它们。
我有个好消息:在Python中直接使用迭代器非常普遍。
这里的squares
对象是一个生成器:
>>> numbers = [ 1 , 2 , 3 ]
>>> squares = ( n** 2 for n in numbers )
生成器是迭代器,这意味着您可以在生成器上调用next
来获取其下一项:
>>> next ( squares )
1
>>> next ( squares )
4
但是,如果您曾经使用过生成器,则可能知道您还可以循环生成器:
>>> squares = ( n** 2 for n in numbers )
>>> for n in squares:
... print ( n )
...
1
4
9
如果您可以在Python中进行遍历 ,那么这是可迭代的 。
因此, 生成器是迭代器 ,但是生成器也是可迭代的。 这里发生了什么?
因此,当我解释迭代器的工作原理时,我跳过了有关它们的重要细节。
迭代器是可迭代的。
我再说一遍:Python中的每个迭代器也是可迭代的,这意味着您可以遍历迭代器。
由于迭代器也是可迭代的,因此您可以使用内置的iter
函数从迭代器中获取迭代器:
>>> numbers = [ 1 , 2 , 3 ]
>>> iterator1 = iter ( numbers )
>>> iterator2 = iter ( iterator1 )
请记住,iterables给我们打电话给我们迭代器iter
他们。
当我们在迭代器上调用iter
时,它总是会自动返回:
>>> iterator1 is iterator2
True
迭代器是可迭代的,所有迭代器都是它们自己的迭代器。
def is_iterator ( iterable ) :
return iter ( iterable ) is iterable
感到困惑了吗?
让我们回顾一下这些术语。
另外,在Python中,迭代器也是可迭代的,它们充当自己的迭代器。
因此,迭代器是可迭代的,但它们没有某些可迭代的功能。
迭代器没有长度,因此无法编制索引:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> iterator = iter ( numbers )
>>> len ( iterator )
TypeError : object of type 'list_iterator' has no len ( )
>>> iterator [ 0 ]
TypeError : 'list_iterator' object is not subscriptable
从我们作为Python程序员的角度来看,使用迭代器唯一有用的事情是将其传递给内置的next
函数或对其进行循环:
>>> next ( iterator )
1
>>> list ( iterator )
[ 2 , 3 , 5 , 7 ]
而且,如果我们第二次遍历迭代器,我们将一无所获:
>>> list ( iterator )
[ ]
您可以将迭代器视为一次性的 惰性迭代器,这意味着它们只能循环一次。
正如您在下面的真值表中看到的那样,可迭代的对象并不总是迭代器,而是迭代器始终是可迭代的:
目的 | 可迭代? | 迭代器? |
---|---|---|
Iterable | ✔️ | ❓ |
Iterator | ✔️ | ✔️ |
Generator | ✔️ | ✔️ |
List | ✔️ | ❌ |
让我们从Python的角度定义迭代器的工作方式。
可以将Iterables传递给iter
函数以为其获取迭代器。
迭代器:
next
函数,该函数将给出其下一个项目;如果没有更多项目,则引发StopIteration
异常 iter
函数,并将自己返回 这些陈述的反义也成立:
TypeError
可以传递给iter
任何东西都是可迭代的 TypeError
可以传递到next
一个的任何东西都是迭代器 iter
时返回的任何内容都是iterator 这就是Python中的迭代器协议。
迭代器使我们既可以工作又可以创建懒惰的可迭代对象 ,这些可迭代对象在我们要求他们提供下一个项目之前不会做任何工作。 因为我们可以创建懒惰的可迭代对象,所以我们可以制作无限长的可迭代对象。 我们可以创建对系统资源保守的可迭代对象,可以节省内存,并节省CPU时间。
您已经在Python中看到了很多迭代器。 我已经提到生成器是迭代器。 Python的许多内置类也是迭代器。 例如,Python的enumerate
和reversed
对象是迭代器。
>>> letters = [ 'a' , 'b' , 'c' ]
>>> e = enumerate ( letters )
>>> e
< enumerate object at 0x7f112b0e6510 >
>>> next ( e )
( 0 , 'a' )
在Python 3中, zip
, map
和filter
对象也是迭代器。
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> letters = [ 'a' , 'b' , 'c' ]
>>> z = zip ( numbers , letters )
>>> z
< zip object at 0x7f112cc6ce48 >
>>> next ( z )
( 1 , 'a' )
Python中的文件对象也是迭代器。
>>> next ( open ( ' ) )
'hello world n '
在标准库和第三方Python库中,Python内置了许多迭代器。 这些迭代器通过延迟工作直到您要求它们的下一个项目,都像懒惰的可迭代器一样工作。
知道您已经在使用迭代器很有用,但是我希望您也可以创建自己的迭代器和懒惰的可迭代对象。
此类使迭代器可以接受数字的迭代,并在其循环时提供每个数字的平方。
class square_all:
def __init__ ( self , numbers ) :
self . numbers = iter ( numbers )
def __next__ ( self ) :
return next ( self . numbers ) ** 2
def __iter__ ( self ) :
return self
但是,直到我们开始循环访问此类的实例之前,将无法完成任何工作。
在这里,我们有一个无限长的可迭代count
,您可以看到square_all
接受了count
而没有完全循环遍历这个无限长的可迭代count
:
>>> from itertools import count
>>> numbers = count ( 5 )
>>> squares = square_all ( numbers )
>>> next ( squares )
25
>>> next ( squares )
36
该迭代器类可以工作,但是我们通常不以这种方式进行迭代器。 通常,当我们要创建自定义迭代器时,我们会生成一个生成器函数:
def square_all ( numbers ) :
for n in numbers:
yield n** 2
这个生成器函数等效于我们上面创建的类,并且其工作方式基本上相同。
该yield
语句可能看起来很神奇,但是它非常强大: yield
允许我们将生成器函数置于next
函数的调用之间的暂停处。 yield
语句是将生成器功能与常规功能区分开的东西。
我们可以实现同一迭代器的另一种方法是使用生成器表达式。
def square_all ( numbers ) :
return ( n** 2 for n in numbers )
这和生成器函数具有相同的作用,但是它使用的语法看起来像是列表推导 。 如果需要在代码中进行延迟迭代,请考虑迭代器,并考虑制作生成器函数或生成器表达式。
一旦在代码中包含了使用懒惰的可迭代对象的想法,您就会发现发现或创建帮助函数的很多可能性,这些函数可以帮助您遍历可迭代对象和处理数据。
这是一个for
循环,总结了Django查询集中的所有可计费时间:
hours_worked = 0
for event in events:
if event. is_billable ( ) :
hours_worked + = event. duration
这是通过使用生成器表达式进行延迟评估来执行相同操作的代码:
billable_times = (
event. duration
for event in events
if event. is_billable ( )
)
hours_worked = sum ( billable_times )
注意,我们的代码形状发生了巨大变化。
将我们的可计费时间变成一个可延迟的可迭代对象,使我们能够命名以前未命名的内容( billable_times
)。 这也使我们可以使用sum
函数。 我们以前不可能使用过sum
,因为我们甚至没有传递给它的迭代器。 迭代器使您可以从根本上改变结构代码的方式。
此代码输出日志文件的前10行:
for i , line in enumerate ( log_file ) :
if i >= 10 :
break
print ( line )
这段代码做同样的事情,但是我们使用itertools.islice
函数在循环时懒惰地获取文件的前10行:
from itertools import islice
first_ten_lines = islice ( log_file , 10 )
for line in first_ten_lines:
print ( line )
我们first_ten_lines
变量是一个迭代器。 同样,使用迭代器可以使我们给以前未命名的东西( first_ten_lines
)命名。 命名事物可以使我们的代码更具描述性和可读性。
另外,由于islice
实用程序可以为我们处理break
,因此我们也无需在循环中使用break
语句。
您可以在标准库的itertools以及第三方库(例如bolton和more-itertools)中找到更多的迭代助手功能。
您可以在标准库和第三方库中找到用于循环的辅助函数,但是您也可以自己创建!
此代码列出了序列中连续值之间的差异。
current = readings [ 0 ]
for next_item in readings [ 1 : ] :
differences. append ( next_item - current )
current = next_item
请注意,此代码还有一个额外的变量,每次循环时都需要分配。 还要注意,此代码仅适用于我们可以切片的东西,例如序列。 如果readings
是一个生成器,一个zip对象或任何其他类型的迭代器,则此代码将失败。
让我们编写一个辅助函数来修复代码。
这是一个生成器函数,可为我们提供给定可迭代项中的每个项目的当前项目和其后的项目:
def with_next ( iterable ) :
"""Yield (current, next_item) tuples for each item in iterable."""
iterator = iter ( iterable )
current = next ( iterator )
for next_item in iterator:
yield current , next_item
current = next_item
我们从迭代器中手动获取一个迭代器,在其上调用next
以获取第一个项目,然后遍历迭代器以获取所有后续项,并一路跟踪我们的最后一个项目。 此函数不仅适用于序列,而且适用于任何可迭代类型。
这与以前的代码相同,但是我们使用的是helper函数,而不是手动跟踪next_item
:
differences = [ ]
for current , next_item in with_next ( readings ) :
differences. append ( next_item - current )
请注意,此代码在循环中没有为next_item
分配next_item
代码。 with_next
生成器函数为我们处理跟踪next_item
的工作。
还要注意,这段代码已经压缩得足够多了,如果我们愿意的话,我们甚至可以复制粘贴到列表推导中。
differences = [
( next_item - current )
for current , next_item in with_next ( readings )
]
现在,我们可以跳回到前面看到的那些奇怪的例子,并试图弄清楚发生了什么。
这里我们有一个生成器对象, squares
:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> squares = ( n** 2 for n in numbers )
如果我们将此生成器传递给tuple
构造函数,则将获得其项的元组:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> squares = ( n** 2 for n in numbers )
>>> tuple ( squares )
( 1 , 4 , 9 , 25 , 49 )
如果然后尝试计算此生成器中数字的sum
,则将得到0
:
>>> sum ( squares )
0
这个生成器现在是空的:我们已经用光了。 如果我们尝试再次从中创建一个元组,则会得到一个空的元组:
>>> tuple ( squares )
( )
生成器是迭代器。 迭代器是一次性的可迭代对象。 它们就像无法重新加载的Hello Kitty Pez分配器。
同样,我们有一个生成器对象, squares
:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> squares = ( n** 2 for n in numbers )
如果我们问9
是否在这个squares
生成器中,我们将得到True
:
>>> 9 in squares
True
但是,如果我们再次问同样的问题,我们将得到False
:
>>> 9 in squares
False
当我们问这个生成器中是否有9
,Python必须遍历该生成器以找到9
。 如果我们在检查9
之后一直循环遍历它,那么我们只会得到最后两个数字,因为在此之前我们已经消耗了这些数字:
>>> numbers = [ 1 , 2 , 3 , 5 , 7 ]
>>> squares = ( n** 2 for n in numbers )
>>> 9 in squares
True
>>> list ( squares )
[ 25 , 49 ]
询问迭代器中是否包含某些东西会部分消耗迭代器。 如果不开始遍历迭代器,就无法知道迭代器中是否包含某些内容。
当您遍历字典时,您会得到以下键:
>>> counts = { 'apples' : 2 , 'oranges' : 1 }
>>> for key in counts:
... print ( key )
...
apples
oranges
打开字典包装时,您还会获得密钥:
>>> x , y = counts
>>> x , y
( 'apples' , 'oranges' )
循环依赖于迭代器协议。 可迭代的拆包还依赖于迭代器协议。 打开字典的包装实际上与循环字典相同。 两者都使用迭代器协议,因此在两种情况下您将获得相同的结果。
序列是可迭代的,但并非所有可迭代的都是序列。 当有人说“可迭代”时,您只能假设它们的意思是“可以迭代的东西”。 不要以为可迭代项可以循环两次,询问其长度或建立索引。
迭代器是Python中最基本的可迭代形式。 如果您想在代码中进行延迟迭代,请考虑迭代器,并考虑制作生成器函数或生成器表达式。
最后,请记住,Python中的每种迭代类型都依赖于迭代器协议,因此,了解迭代器协议是大致了解Python中循环的关键。
以下是我推荐的相关文章和视频:
For
循环工作 ,很短的文章中,我写了一篇关于迭代器协议 本文基于作者去年在DjangoCon AU , PyGotham和North Bay Python上发表的Loop Better演讲。 有关更多内容,请参加将于2018年5月9日至17日在俄亥俄州哥伦布举行的PYCON 。
翻译自:
python 循环迭代
本文发布于:2024-02-04 22:33:22,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170717883660280.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |