爬虫 (十八) 如何经过反编译理解 for 循环 (十)

请继续关注我 函数


点击上方蓝字关注咱们oop

欢迎关注个人公众号,志学Python学习

前面的基本运算符加减乘除等运算符内容,咱们就不讲了,我以为最应该讲讲就是 for 循环运算符这东西,真的是须要咱们去好好探讨一下的,记得关注点赞哦,谢谢 spa

在本篇博客中,咱们将讨论 Python 中 for 循环的原理3d

咱们将从一组基本例子和它的语法开始,还将讨论与 for 循环关联的 else 代码块的用处对象

而后咱们将介绍迭代对象、迭代器和迭代器协议,还会学习如何建立本身的迭代对象和迭代器blog

以后,咱们将讨论如何使用迭代对象和迭代器实现 for 循环,以及利用 while 循环经过迭代器协议实现 for 循环逻辑rem

最后,咱们将反编译一个简单的 for 循环,并逐步介绍 Python 解释器在执行 for 循环时执行的指令,以知足你们的好奇心。这些有助于理解 for 循环运行时的内部工做原理字符串

Python的for循环博客

for 语句是 Python 中执行迭代的两个语句之一,另外一个语句是 while。若是你对 Python 的迭代并非很熟悉的话,Python中的迭代:for、while、break、以及continue语句是一个不错的切入点

Python 中,for 循环用于遍历一个迭代对象的全部元素。循环内的语句段会针对迭代对象的每个元素项目都执行一次。暂且能够将迭代对象想象成一个对象集合,咱们能够一个个遍历里面的元素。咱们将在下一节对迭代器和迭代对象做详细说明

一个简单的 for 循环

咱们先从一个简单 for 循环开始,它遍历一个字符串列表并打印每个字符串

如你所见,这个循环实际上遍历了列表中的每个单词并打印它们。也就是说,在循环的每一次遍历中,变量 word 都被指定为列表中的一个元素,而后执行 for 语句中的代码块。因为列表是一个有序的元素序列,因此循环也是以相同的顺序遍历这些元素

带有 else 子句的 for 循环

Python 中的 for 循环能够选择是否关联一个 else 子句。else 子句中的代码块是在 for 循环完成后才开始执行的,即在迭代对象中的全部元素都遍历完毕以后。如今咱们看一下如何扩展前面的示例以包含一个 else 条件(子句)

else 子句适用于什么时候?

你已经注意到,else 子句是在 for 循环完成以后才执行的。那么 else 代码块的意义是什么呢?for 循环以后的语句不是也是一样会执行吗?

咱们不少时候会遇到这样一种状况,当知足某种条件时,中途结束 for 循环。且若是这个条件一直未知足,则但愿执行另外一组语句。咱们一般使用布尔类型的标记实现,下面是一个例子

调用结果:

而用 else 代码块的话,咱们能够避免使用布尔类型的标记_found_item_。咱们看看如何使用 else 子句重写上面的方法。注意若是 for 循环中的 break 语句被触发执行,那么则会跳过 else 块

因此 else 代码块适用于 for 循环中有 break 语句的状况,且咱们但愿 break 条件没有被触发的时候执行一些语句

不然,与 else 关联的语句只会在 for 循环结束时才执行。本文的最后一节查看反编译的字节码时你会看到这一点

for 循环语法

咱们已经看到了一些简单的例子,接下来以 for 循环的语法结束本节

基本上,对于 iterable 中的每个元素,都会执行 set_of_statements_1,一旦全部的元素都迭代一遍,控制器将跳转到 else 代码块中执行 set_of_statements_2

注意,else 子句是可选的。若是没有发现 else 子句,循环会在全部元素都遍历完成后结束,而且控制器会转向程序以后的语句

可迭代对象与迭代器

可迭代对象

在上一节,咱们使用术语 iterable 来表示循环中被迭代的对象。如今咱们来试着了解一下 Python 中的 iterable 对象是什么

Python 中,一个 iterable 对象指在 for 循环中能够被迭代的任意对象。这意味着,当这个对象做为参数传递给 iter()方法时应该返回一个迭代器。咱们来看一下 Python 中的一些经常使用的内置迭代的例子

如你所见,当咱们对一个 iterable 对象调用 iter() 时,它会返回一个迭代器对象

迭代器

那么什么是迭代器呢?迭代器在 Python 中被定义为一个表现为流式数据的对象。基本上,若是咱们将对象传递给内置的_next()_ 方法,它应该从与之关联的流式数据中返回下一个值。一旦全部的元素都遍历结束,它会抛出一个_*StopIteration* 异常。_next()_方法的后续调用也都会抛出*StopIteration*_ 异常。

咱们用一个列表来试一下

迭代器也是可迭代对象!可是...

有一个颇有趣的事须要记一下,_迭代器_一样支持(强制要求支持_迭代器协议_)_iter()_ 方法。这意味着咱们能够对一个迭代器调用_iter()_ 方法并获取它自身的迭代器对象

所以,咱们能够在任何指望使用迭代器的地方使用它。好比,for 循环

然而要注意一点,在像 list 这样的容器对象上调用 iter() 每次都会返回不一样的迭代器,而在迭代器上调用 iter() 仅仅返回同一个迭代器

因此若是你须要进行屡次迭代,而且用迭代器替换普通容器或可迭代对象,那么第二次你会看到一个空的容器

对一个列表迭代两次

请注意,这是按照咱们的指望运行的

对一个列表迭代器迭代两次

请注意,迭代器在第一次循环的时候就已经结束了,第二次咱们看到的是一个空容器

迭代器协议

前文咱们看到了:

1. 一个可迭代对象,做为参数传递给 iter() 方法时返回一个迭代器。

2. 一个迭代器,

1. 做为参数传递给_next()_方法时返回它的下一个元素或者在全部元素都遍历结束时抛  出_StopIteration_ 异常

2. 做为参数传递给_iter()_ 方法时返回它自身

迭代协议仅仅只是一种将对象定义为迭代器的标准方式。咱们已经在前一节看到了这种协议的实际应用。根据协议,迭代器应该定义如下两个方法:

1. __next__()

   1. 每次调用这个方法时,应该返回迭代器的下一个元素。一旦元素都遍历结束,它应该抛出_StopIteration_ 异常

2. 当咱们调动内置函数_next()_ 时,实际内部调用的是本方法

2. __iter__()

    1. 这个方法返回迭代器自身

2. 当咱们调动内置函数_iter()_ 时,实际内部调用的是本方法

本身写一个迭代器

如今咱们已经知道迭代协议的原理,能够写一个本身的迭代器了。咱们先看一个例子,下面咱们建立了一个根据给定范围和步长的 Range 类

咱们看一下它在 for 循环中是怎么工做的

注意,Range 类的实例是迭代器也是可迭代对象

本身写一个可迭代对象

咱们还能够基于 Range 迭代器另外建立一个可迭代对象。它的做用是每当调用 __iter()__ 方法是返回一个新的迭代器,在这里,它应该返回一个新的 Range 对象

在 for 循环中使用咱们这个 RangeIterable

for 循环工做原理

如今咱们已经知道什么是迭代器和可迭代对象,接下来了解一下 for 循环是如何工做的

再看一下前面的例子

当咱们执行上面的代码块时,发生了如下这些事情:

1. 在 for 语句内部对列表 ["You", "are", "awesome!"] 调用了 iter() 方法,返回结果是一个迭代器

2. 而后对迭代器调用 next() 方法,并将其返回值赋给变量 word

3. 以后,会执行 for 循环中关联的语句块。这个例子中是打印 word

4. 在 next() 方法抛出 StopIteration 以前会一直重复执行第 2,3 步

5. 一旦 next() 抛出 StopIteration,控制器会跳转到 else 子句(若是存在)并执行与 else 关联的语句块

注意:若是在步骤 3 中,for 循环语句遇到了 break 语句,则跳过 else 代码块

使用 while 语句实现 for 循环逻辑

咱们能够像下面这样使用 while 语句实现以前的逻辑

while 循环的行为实际上与 for 循环相同,上面的代码会有如下输出

反编译 for 循环

在本节,咱们将反编译 for 循环并逐步说明解释器在执行 for 循环时的指令。这里使用_dis_ 模块来反编译 for 循环。详细来讲,就是咱们将使用 dis.dis 方法来生成可读性更高的字节码

咱们会使用以前一直用的简单 for 循环示例。接下来将文件写入文件 for_loop.py

咱们能够调用 dis.dis 方法得到可读性高的字节码。在终端上运行如下命令

反编译输出的每列表示如下内容:

1.  第 1 列:代码行数

2. 第 2 列:若是是跳转指令,则有 ">>" 符号

3. 第 3 列:以字节为单位的字节码偏移量

4. 第 4 列:字节码指令自己

5. 第 5 列:展现指令的参数。若是括号中有内容,它只是对参数作了更好的可读性转化

如今咱们来一步步浏览反编译后的字节码,并尝试了解实际发生了什么

1. 第 1 行,即,"for word in [“You”, “are”, “awesome!”]:" 转译为:

0 SETUP_LOOP 28 (to 30)

该语句将 for 循环中的代码块推送到栈中。这段代码块会跨越 28 个字节,达到 "30"

这意味着,若是 for 循环中有 break 语句,那么控制器将跳转到偏移位置 "30"。注意当遇到 break 语句时是如何跳过 else 代码块的

2 LOAD_CONST 0 ((‘You’, ‘are’, ‘awesome!’))

接下来,列表被推送到栈顶(TOS,以后使用 TOS 表示栈顶或栈顶元素)

4 GET_ITER

该指令实现 "TOS = iter(TOS)"。这表示从列表获取一个迭代器(当前为 TOS),而后将迭代器推送给 TOS

6 FOR_ITER 12 (to 20)

该指令获取 TOS,做为当前的迭代器, 并调用 next() 方法

若是 next() 方法产生一个值,则将其做为 TOS 推送到栈,并执行吓一跳指令 "8 STORE_NAME"

一旦 next() 代表迭代器已经遍历结束(即抛出 StopIteration 异常),TOS(迭代器)将从栈中弹出,字节码计数器会增长 12。这表示控制器跳转到指令 "20 POP_BLOCK"

8 STORE_NAME 0 (word)

这个指令执行了转换 word = TOS,即,_next()_返回的值被赋给变量_word_

2. 第 1 行,即,_"print(word)"_ 转译为:

10 LOAD_NAME 1 (print)

将可调用方法_print_ 推送到栈中

12 LOAD_NAME 0 (word)

将栈中的_word_做为参数推送给_print_

14 CALL_FUNCTION 1

调用带位置参数的函数

像咱们看到的指令那样,与函数关联的参数会出如今 TOS 中。在得到可调用象的对(如_print_)以前,会弹出全部遇到的参数

一旦得到可调用对象,则把全部参数传递给它并调用

可调用对象执行结束后,把返回值推送到 TOS 中,这里是 None

16 POP_TOP

TOS(栈顶元素),即将函数的返回值从栈中移除(弹出)

18 JUMP_ABSOLUTE 6

此时字节码计数器为 “6”,这表示下一条指令将执行 "6 FOR_ITER"。这是循环遍历迭代器中元素的方式

注意,一旦迭代器中的元素都遍历结束,指令 "6 FOR_ITER" 会结束循环并跳转到 "20 POP_BLOCK"

20 POP_BLOCK

POP_BLOCK 会从代码块的栈中移除由 “0 SETUP_LOOP” 设置的代码块

3. 注意第 3 行(对应_else_),没有关联任何特殊指令。程序控制器会顺序执行下一条与_else_ 相关的指令

4. 第 4 行,即,_"print("See you later!__")"_ 转译为:

22 LOAD_NAME 1 (print)

推送与_print_ 相关的可调用方法到栈中

24 LOAD_CONST 1 ('See you later!')

推送可调用函数的参数对象到栈中

26 CALL_FUNCTION 1

可调用函数及其参数会从栈中弹出,而后执行函数并将其返回值推送到 TOS

28 POP_TOP

TOS(栈顶元素),即将函数返回值(这里是 None)从栈中移除

5. 下面的两个指令只是简单的将脚本的返回值(None)加载到栈并返回

30 LOAD_CONST 2 (None)

32 RETURN_VALUE

喔!如今咱们已经了解了 for 循环反编译后的指令。但愿这有助于更好地理解 for 循环的工做原理

相关文章
相关标签/搜索