笨办法学 Python3 第五版(预览)(二)(3)https://developer.aliyun.com/article/1483435
在这个 Python 代码中,我正在做以下事情:
- 我从
dis
模块中导入dis()
函数 - 我运行
dis()
函数,但使用'''
给它一个多行字符串 - 我接着将想要反汇编的 Python 代码写入这个多行字符串中
- 最后,我用
''')
结束多行字符串和dis()
函数
当你在 Jupyter 中运行这个代码时,你会看到它像我上面展示的那样输出字节码,但也许会有一些我们马上会讨论的额外内容。
这些字节存储在哪里?
当你运行 Python(版本 3)时,这些字节会存储在一个名为__pycache__
的目录中。如果你将这段代码放入一个名为ex19.py
的文件中,然后用python ex19.py
运行它,你应该会看到这个目录。
在这个目录中,你应该会看到一堆以.pyc
结尾的文件,名称类似于生成它们的代码。这些.pyc
文件包含了你编译后的 Python 代码的字节。
当你运行dis()
时,你正在打印.pyc
文件中数字的人类可读版本。
规则 2:跳转使序列变得非线性
像LOAD_CONST 10
这样的一系列简单指令并不是很有用。耶!你可以加载数字 10!太棒了!代码开始变得有用的地方是当你添加“跳转”概念使这个序列非线性。让我们看一个新的 Python 代码片段:
1 while True: 2 x = 10
要理解这段代码,我们必须预示一个稍后的练习,你将学习关于 while-loop
。代码 while True:
简单地表示“在 True
为 True
时继续运行我的代码 x = 10
。”由于 True
将始终为 True
,这将永远循环。如果在 Jupyter 中运行,它永远不会结束。
当你 dis()
这段代码时会发生什么?你会看到新的指令 JUMP_ABSOLUTE
:
1 dis("while True: x = 10") 2 3 0 LOAD_CONST 1 (10) 4 2 STORE_NAME 0 (x) 5 4 JUMP_ABSOLUTE 0 (to 0)
当我们讨论 x = 10
代码时,你看到了前两个指令,但现在在结尾我们有 JUMP_ABSOLUTE 0
。注意这些指令左边有数字 0
、2
和 4
?在之前的代码中我把它们剪掉了,这样你就不会被分心,但在这里它们很重要,因为它们代表每个指令所在位置的序列。所有 JUMP_ABSOLUTE 0
做的就是告诉 Python “跳转到位置 0 处的指令”,即 LOAD_CONST 1 (10)
。
通过这个简单的指令,我们现在已经将无聊的直线代码转变成了一个更复杂的循环,不再是直线了。稍后我们将看到跳转如何与测试结合,允许更复杂的移动通过字节序列。
为什么是反向的?
你可能已经注意到 Python 代码读起来像“当 True 为 True 时将 x 设置为 10”,但 dis()
输出更像是“将 x 设置为 10,跳转以再次执行”。这是因为规则 #1,它说我们只能生成一个 字节序列。不允许有嵌套结构,或任何比 INSTRUCTION OPTIONS
更复杂的语法。
为了遵循这个规则,Python 必须找出如何将其代码转换为产生所需输出的字节序列。这意味着将实际的重复部分移动到序列的末尾,以便它在一个序列中。当查看字节码和汇编语言时,你会经常发现这种“反向”的特性。
一个 JUMP 能前进吗?
是的,技术上,JUMP 指令只是告诉计算机在序列中处理不同的指令。它可以是下一个,前一个,或未来的一个。这是计算机跟踪当前指令“索引”的方式,它简单地递增该索引。
当你 JUMP 时,你在告诉计算机将这个索引更改到代码中的一个新位置。在我们的 while
循环代码中(下面),JUMP_ABSOLUTE
在索引 4
处(看左边的 4)。运行后,索引会更改为 0
,在那里是 LOAD_CONST
的位置,所以计算机再次运行该指令。这将永远循环。
1 0 LOAD_CONST 1 (10) 2 2 STORE_NAME 0 (x) 3 4 JUMP_ABSOLUTE 0 (to 0)
规则 3:测试控制跳转
JUMP 对于循环很有用,但是如何做决策呢?编程中的一个常见问题是提出类似的问题:
“如果 x 大于 0,则将 y 设置为 10。”
如果我们用简单的 Python 代码写出来,可能会像这样:
1 if x > 0: 2 y = 10
再次,这是预示你将来会学到的东西,但这足够简单可以理解:
- Python 将 测试
x
是否大于>
0 - 如果是,那么 Python 将运行行
y = 10
- 你看到那行缩进在
if x > 0:
下面了吗?这被称为“块”,Python 使用缩进来表示“这个缩进的代码是上面代码的一部分” - 如果
x
不大于0
,那么 Python 将跳过y = 10
行以跳过它
要使用我们的 Python 字节码来实现测试部分,我们需要一个实现测试部分的新指令。我们有 JUMP。我们有变量。我们只需要一种方法来比较两个东西,然后根据比较跳转。
让我们拿这段代码并用dis()
来看看 Python 是如何做到这一点的:
1 dis(''' 2 x = 1 3 if x > 0: 4 y = 10 5 ''') 6 7 0 LOAD_CONST 0 (1) # load 1 8 2 STORE_NAME 0 (x) # x = 1 9 10 4 LOAD_NAME 0 (x) # load x 11 6 LOAD_CONST 1 (0) # load 0 12 8 COMPARE_OP 4 (>) # compare x > 0 13 10 POP_JUMP_IF_FALSE 10 (to 20) # jump if false 14 15 12 LOAD_CONST 2 (10) # not false, load 10 16 14 STORE_NAME 1 (y) # y = 10 17 16 LOAD_CONST 3 (None) # done, load None 18 18 RETURN_VALUE # exit 19 20 # jump here if false 21 20 LOAD_CONST 3 (None) # load none 22 22 RETURN_VALUE # exit
这段代码的关键部分是COMPARE_OP
和POP_JUMP_IF_FALSE
:
1 4 LOAD_NAME 0 (x) # load x 2 6 LOAD_CONST 1 (0) # load 0 3 8 COMPARE_OP 4 (>) # compare x > 0 4 10 POP_JUMP_IF_FALSE 10 (to 20) # jump if false
这段代码的作用是:
- 使用
LOAD_NAME
加载x
变量 - 使用
LOAD_CONST
加载常量0
- 使用
COMPARE_OP
,进行>
比较并留下True
或False
结果供以后使用 - 最后,
POP_JUMP_IF_FALSE
使if x > 0
起作用。它“弹出”True
或False
值以获取它,如果读取到False
,它将JUMP
到指令 20 - 这将跳过设置
y
的代码,如果比较结果是False
,但是如果比较结果是True
,那么 Python 将运行下一条指令,从而开始y = 10
序列
花点时间仔细研究一下这个问题。如果你有打印机,尝试打印出来并手动设置x
为不同的值,然后跟踪代码的运行过程。当你设置x = -1
时会发生什么?
你说的“pop”是什么意思?
在前面的代码中,我跳过了 Python 如何“弹出”值来读取它的部分,但它将其存储在一个称为“堆栈”的东西中。现在只需将其视为一个临时存储位置,你可以将值“推入”其中,然后将其“弹出”。在你的学习阶段,你真的不需要深入了解更多。只需了解其效果是获取最后一条指令的结果。
等等,像*COMPARE_OP*
这样的测试也在循环中使用吗?
是的,基于你现在所知道的,你可能可以立即弄清楚它是如何工作的。尝试编写一个while-loop
,看看你是否可以根据你现在所知道的知识使其工作。如果你不能,不要担心,因为我们将在后续练习中涵盖这个内容。
规则 4:存储控制测试
在代码运行时,你需要一种方式来跟踪数据的变化,这通过“存储”来实现。通常这种存储是在计算机的内存中,你为存储在内存中的数据创建名称。当你编写这样的代码时,你一直在这样做:
1 x = 10 2 y = 20 3 z = x + y
在前面的每一行中,我们都在创建一个新的数据片段并将其存储在内存中。我们还为这些内存片段赋予了名称x
、y
和z
。然后我们可以使用这些名称从内存中“召回”这些值,这就是我们在z = x + y
中所做的。我们只是从内存中召回x
和y
的值然后将它们相加。
这就是这个小规则的大部分内容,但这个小规则的重要部分是你几乎总是使用内存来控制测试。
当然,你可以编写这样的代码:
1 if 1 < 2: 2 print("but...why?")
不过这是毫无意义的,因为它只是在一个毫无意义的测试之后运行第二行。1
始终小于2
,所以这是无用的。
当您使用变量进行测试以使测试基于计算动态化时,像COMPARE_OP
这样的测试就会发挥作用。这就是为什么我认为这是“代码游戏”的一条规则,因为没有变量的代码实际上并不在玩游戏。
仔细回顾以前的示例,并确定在哪些地方使用LOAD
指令加载值,以及使用STORE
指令将值存储到内存中。
规则 5:输入/输出控制存储
“代码游戏”的最后一条规则是您的代码如何与外部世界互动。拥有变量很好,但一个只包含您在源文件中键入的数据的程序并不是很有用。您需要的是输入和输出。
输入是您从文件、键盘或网络等地方将数据输入到代码中的方式。在上一个模块中,您已经使用open()
和input()
来做到这一点。每次打开文件、读取内容并对其执行操作时,您都会访问输入。当您使用input()
向用户提问时,您也使用了输入。
输出是如何保存或传输程序结果的。输出可以是通过print()
输出到屏幕,通过file.write()
输出到文件,甚至通过网络传输。
让我们对input('Yes? ')
的简单使用运行dis()
,看看它做了什么:
1 from dis import dis 2 dis("input('Yes? ')") 3 4 0 LOAD_NAME 0 (input) 5 2 LOAD_CONST 0 ('Yes? ') 6 4 CALL_FUNCTION 1 7 6 RETURN_VALUE
你可以看到现在有一个新的指令CALL_FUNCTION
,它实现了你在练习 18 中学到的函数调用。当 Python 看到CALL_FUNCTION
时,它会找到用LOAD_NAME
加载的函数,然后跳转到该函数以运行该函数的代码。函数如何工作背后有很多东西,但你可以将CALL_FUNCTION
看作类似于JUMP_ABSOLUTE
,但是跳转到指令中的一个命名位置。
将所有内容整合在一起
根据这五条规则,我们有以下代码游戏:
- 您将数据作为程序的输入读取(规则#5)
- 您将这些数据存储在存储器中(变量)(规则#4)
- 您使用这些变量执行测试…(规则#3)
- … 这样您就可以在各处跳转…(规则#2)
- … 指令序列…(规则#1)
- … 将数据转换为新变量(规则#4)…
- … 然后将其写入输出以进行存储或显示(规则#5)
尽管这看起来很简单,但这些小规则创造了非常复杂的软件。视频游戏是一个很好的例子,非常复杂的软件就是这样做的。视频游戏会读取您的控制器或键盘作为输入,更新控制场景中模型的变量,并使用高级指令将场景呈现到屏幕上作为输出。
现在花点时间回顾你已经完成的练习,看看你是否更好地理解它们。在你不理解的代码上使用dis()
是否有帮助,还是更加困惑?如果有帮助,那就尝试在所有代码上使用它以获得新的见解。如果没有帮助,那就记住它以备以后使用。当你在练习 26 上这样做时,这将会特别有趣。
字节码列表
随着你继续练习,我会让你在一些代码上运行dis()
来分析它在做什么。你需要完整的 Python 字节码列表来学习,可以在文档的末尾找到[dis()](https://docs.python.org/3/library/dis.xhtml#python-bytecode-instructions)
。
dis()
是一个支线任务
后续练习将包含一些短小的部分,要求你在代码上运行dis()
来研究字节码。这些部分是你教育过程中的“支线任务”。这意味着它们不是理解 Python 必不可少的,但如果你完成它们,可能会在以后有所帮助。如果它们太难了,那就跳过它们,继续进行课程的其他部分。
dis()
最重要的一点是它直接让你了解Python认为你的代码在做什么。如果你对代码的工作原理感到困惑,或者只是好奇 Python 实际在做什么,这会对你有所帮助。
练习 28:记忆逻辑
今天是你开始学习逻辑的一天。到目前为止,你已经尽可能地阅读和写入文件,使用终端,并且已经学会了 Python 的许多数学功能。
从现在开始,你将学习逻辑。你不会学习学术界喜欢研究的复杂理论,而只会学习使真实程序运行并且真正的程序员每天都需要的简单基本逻辑。
学习逻辑必须在你进行一些记忆工作之后进行。我希望你能坚持做这个练习整整一个星期。即使你感到无聊透顶,也要坚持下去。这个练习有一组逻辑表格,你必须记住它们,以便让你更容易完成后面的练习。
我警告你,一开始这可能不会很有趣。这将会非常无聊和乏味,但这会教会你作为程序员所需要的一项非常重要的技能。你将需要能够记忆生活中重要的概念。一旦你掌握了这些概念,大多数都会变得令人兴奋。你将与之奋斗,就像与章鱼搏斗一样,然后有一天你会理解它。所有记忆基础知识的工作以后会有很大的回报。
以下是一个提示,如何在不发疯的情况下记忆某些内容:每天分散一点时间进行学习,并记录下你最需要重点学习的内容。不要试图连续坐下两个小时来记忆这些表格。这样做是不会奏效的。你的大脑只会记住你最开始学习的 15 或 30 分钟的内容。相反,创建一堆索引卡,每一列在正面(True 或 False),背面是对应的列。然后拿出来,看到“True 或 False”立即说“True!”不断练习直到能够做到这一点。
一旦你能做到这一点,每天晚上开始在笔记本上写下自己的真值表。不要只是复制它们。尝试从记忆中完成。当遇到困难时,快速瞥一眼我这里的表格以刷新记忆。这样做将训练你的大脑记住整个表格。
不要花费超过一周的时间在这上面,因为你将在学习过程中应用它。
真值术语
在 Python 中,我们有以下术语(字符和短语)来确定某些东西在程序中是否为“True”或“False”。计算机上的逻辑完全是关于查看这些字符和一些变量的组合在程序的某一点是否为 True。
and
or
not
!=
(不等于)==
(等于)>=
(大于等于)<=
(小于等于)True
False
你实际上之前已经遇到过这些字符,只是可能不是这些术语。这些术语(and、or、not)实际上的工作方式与你期望的一样,就像英语中一样。
真值表
现在我们将使用这些字符制作你需要记忆的真值表。首先是not X
的表:
这是X or Y
的表格:
现在是X and Y
的表格:
接下来是not
与or
组合的表格,即not (X or Y)
:
你应该将这些表格与or
和and
的表格进行比较,看看是否注意到了模式。这是not (X and Y)
的表格。如果你能找出模式,也许就不需要记忆它们了。
现在我们来讨论等式,即以各种方式测试一件事是否等于另一件事。首先是X != Y
:
最后是X == Y
:
现在使用这些表格编写你自己的卡片,并花一周时间记忆它们。请记住,这本书中没有失败,只有每天尽力而为,然后再多努力一点。
常见学生问题
我不能只学习布尔代数背后的概念而不记忆这些吗? 当然可以,但那样的话,你在编码时就必须不断查阅布尔代数的规则。如果你先记忆这些,不仅可以提高你的记忆能力,而且使这些操作变得自然。之后,布尔代数的概念就很容易了。但请按照适合你的方式去做。
练习 29:布尔练习
你从上一个练习中学到的逻辑组合被称为“布尔”逻辑表达式。布尔逻辑在编程中被广泛使用。它是计算的基本部分,熟练掌握这些逻辑表达式就相当于熟练掌握音乐中的音阶。
在这个练习中,你将把你记住的逻辑练习放入 Python 中尝试。拿出每个逻辑问题,并写下你认为答案会是什么。在每种情况下,答案将是
True 或 False。一旦你把答案写下来,你将在终端中启动 Python 并输入每个逻辑问题以确认你的答案。
1\. True and True 2\. False and True 3\. 1 == 1 and 2 == 1 4\. "test" == "test" 5\. 1 == 1 or 2 != 1 6\. True and 1 == 1 7\. False and 0 != 0 8\. True or 1 == 1 9\. "test" == "testing" 10. 1 != 0 and 2 == 1 11. "test" != "testing" 12. "test" == 1 13. not (True and False) 14. not (1 == 1 and 0 != 1) 15. not (10 == 1 or 1000 == 1000) 16. not (1 != 10 or 3 == 4) 17. not ("testing" == "testing" and "Zed" == "Cool Guy") 18. 1 == 1 and (not ("testing" == 1 or 1 == 0)) 19. "chunky" == "bacon" and (not (3 == 4 or 3 == 3)) 20. 3 != 3 and (not ("testing" == "testing" or "Python" == "Fun"))
我还会给你一个技巧,帮助你解决更复杂的问题。
每当你看到这些布尔逻辑语句,你都可以通过这个简单的过程轻松解决它们:
- 找到一个相等测试(== 或 !=)并用其真值替换
- 找到括号中的每个
and/or
并首先解决它们 - 找到每个
not
并反转它 - 找到任何剩余的
and/or
并解决它 - 完成后,你应该有 True 或 False
我将演示一个对 #20 的变体:
1 3 != 4 and not ("testing" != "test" or "Python" == "Python")
这是我逐步进行每个步骤并展示翻译的过程,直到将其简化为一个结果:
- 解决每个相等测试:
3 != 4
是True
,所以用True
替换得到True and not ("testing" != "test"
or "Python" == "Python")
"testing" != "test"
是True
,所以用True
替换 that 得到True and not (True
or "Python" == "Python")
"Python" == "Python"
是 True,所以用True
替换,得到True and
not (True or True)
- 找到括号中的每个
and/or
:
(True or True)
是True
,所以替换得到True and not (True)
- 找到每个
not
并反转它:
not (True)
是False
,所以替换得到True and False
- 找到任何剩余的
and/or
并解决它们:
True and False
是False
,你完成了
有了这个,我们就完成了,知道结果是 False。
警告!
更复杂的可能一开始看起来很难。你应该能够很好地尝试解决它们,但不要灰心。我只是让你为更多这些“逻辑体操”做好准备,这样以后更酷的东西会更容易。坚持下去,记录下你做错的地方,但不要担心它还没有完全进入你的脑海。它会的。
你应该看到的结果
在你尝试猜测这些之后,你的 Jupyter 单元格可能会是这样的:
1 >>> True and True 2 True 3 >>> 1 == 1 and 2 == 2 4 True
学习练习
- Python 中有很多类似于
!=
和==
的运算符。尽量找到尽可能多的“相等运算符”。它们应该类似于<
或<=
。 - 写出每个相等运算符的名称。例如,我称
!=
为“不等于”。 - 通过键入新的布尔运算符来玩 Python,在按下回车键之前,试着大声说出它是什么。不要思考。大声说出脑海中首先出现的东西。写下来,然后按下回车,并记录你答对和答错的次数。
- 丢掉第 3 个学习练习中的纸张,这样你就不会在以后不小心尝试使用它。
常见学生问题
为什么 "test" and "test"
返回 "test"
或 1 and 1
返回 1
而不是 True
? Python 和许多其他语言喜欢返回其布尔表达式的操作数之一,而不仅仅是 True
或 False
。这意味着如果你执行 False and 1
,你会得到第一个操作数(False
),但如果你执行 True and 1
,你会得到第二个操作数(1
)。试着玩弄一下这个。
!=
和 <>
之间有什么区别吗?Python 已经弃用了 <>
,请使用 !=
。除此之外,应该没有任何区别。
有没有捷径? 有。任何具有 False
的 and
表达式立即为 False
,所以你可以在那里停止。任何具有 True
的 or
表达式立即为 True
,所以你可以在那里停止。但确保你能处理整个表达式,因为以后这会变得有用。