代码游戏的5个简单规则

内容

By Zed A. Shaw

编程游戏的5个简单规则

如果你玩围棋或国际象棋这样的游戏,你会知道规则相当简单,但它们所带来的游戏却极其复杂。真正好的游戏具有简单规则和复杂互动的独特品质。编程也是一种游戏,只有几条简单规则就能创造复杂互动,在这个练习中,我们将学习这些规则是什么。

在我们做这之前,我需要强调的是,你在编程时很可能不会直接使用这些规则。有些语言会直接利用这些规则,你的 CPU 也会使用它们,但在日常编程中,你很少会用到它们。如果是这样,那么为什么要学习这些规则呢?

因为这些规则无处不在,了解它们将有助于理解您编写的代码。当代码出现问题时,它将帮助您调试代码。如果您想知道代码是如何工作的,您可以将其“拆解”到基本规则,真正看到它是如何工作的。这些规则就像是一个秘籍_代码_。完全是有意的双关语。

我还要警告你,不要指望一下子完全理解这个。把这个练习看作是为本模块中的其余练习做准备。你应该深入研究这个练习,遇到困难时,继续下一个练习作为休息。你要在这个练习和下一个练习之间来回跳跃,直到概念“点亮”,开始变得清晰。你也应该尽可能深入地研究这些规则,但不要陷在这里。奋斗几天,继续前进,回来再试。只要你继续努力,你实际上不可能“失败”。

规则1:一切都是一系列指令

所有程序都是一系列指令,告诉计算机要做什么。当你输入类似以下代码时,你已经看到 Python 在执行这些指令了:

x = 10
y = 20
z = x + y'

这段代码从第1行开始,一直到第2行,直到结束。这是一系列指令,但在 Python 中,这 3 行指令被转换为另一系列指令,看起来像这样:

LOAD_CONST 0 (10) # 加载数字 10 STORE_NAME 0 (x) # 将其存储在 x 中

LOAD_CONST 1 (20) # 加载数字 20 STORE_NAME 1 (y) # 将其存储在 y 中

LOAD_NAME 0 (x) # 加载 x(其值为10) LOAD_NAME 1 (y) # 加载 y(其值为20) BINARY_ADD # 将它们相加 STORE_NAME 2 (z) # 将结果存储在 z 中

这看起来与 Python 版本完全不同,但我敢打赌你可能能够弄清楚这个指令序列在做什么。我已经添加了注释来解释每个指令,你应该能够将其与上面的 Python 代码联系起来。

我不是在开玩笑。现在花点时间,将 Python 代码的每一行与这个“字节码”的行连接起来。使用我提供的注释,我相信你可以弄清楚,这样做可能会在你的脑海中点亮关于 Python 代码的一盏灯。

没有必要记住这些指令,甚至理解每一条。你应该意识到的是,你的 Python 代码被翻译成了一系列更简单的指令,告诉计算机做什么。这些指令的序列被称为“字节码”,因为它通常以计算机能理解的一系列数字的形式存储在文件中。你在上面看到的输出通常被称为“汇编语言”,因为它是这些字节的人类“可读”(勉强)版本。

这些更简单的指令是从顶部开始处理的,一次只做一件小事情,当程序退出时到达末尾。这就像你的Python代码一样,但使用更简单的指令 选项语法。另一种看待这个问题的方式是x = 10的每个部分可能会在这个“字节码”中成为自己的指令。

代码游戏的第一条规则是:你所写的一切最终都会变成一系列字节,作为计算机的指令输入,告诉计算机应该做什么。

如何获得这个输出?

要自己获得这个输出,您可以使用一个名为dis的模块,它代表“disassemble”。这种代码传统上被称为“字节码”或“汇编语言”,所以dis的意思是“dis-assemble”。要使用dis,您可以导入它并像这样使用dis()函数:

from dis import dis
dis('''
x = 10
y = 20
z = x + y
''')

在这段 Python 代码中,我正在做以下操作:

  1. 我从dis模块中导入dis()函数。
  2. 我运行dis()函数,但使用'''给它一个多行字符串。
  3. 然后,我将要反汇编的Python代码写入这个多行字符串中。
  4. 最后,我用''')结束多行字符串和dis()函数。

当你在Jupyter中运行这段代码时,你会看到它会输出字节码,就像我上面展示的那样,但也许会有一些额外的内容,我们马上会讨论到。

这些字节存储在哪里?

当你运行 Python(版本 3)时,这些字节会存储在一个名为 __pycache__ 的目录中。如果你将这段代码放入一个名为 ex19.py 的文件中,然后用 python ex19.py 运行它,你应该会看到这个目录。

查看此目录,您应该看到一堆以.pyc结尾的文件,这些文件的名称与生成它们的代码类似。这些.pyc文件包含您编译的 Python 代码的字节形式。

当你运行 dis() 时,你会打印出 .pyc 文件中数字的人类可读版本。

规则2:跳跃使序列变得非线性

LOAD_CONST 10 这样的简单指令序列并不是很有用。耶!你可以加载数字10!太棒了!代码开始变得有用的地方是当你添加“跳转”概念使这个序列变得 非线性。让我们看一个新的 Python 代码片段:

    x = 10

要理解这段代码,我们必须预示后面的练习,您将在其中了解while循环。代码while True:简单地表示“在TrueTrue的情况下,持续运行我下面的代码x = 10”。由于True将永远为True,这将永远循环。如果您在Jupyter中运行此代码,它将永远不会结束。

当你运行dis()这段代码时会发生什么?你会看到新的指令JUMP_ABSOLUTE

当 True 时:x = 10
 0 载入常量               1 (10)
 2 存储名称               0 (x)
 4 绝对跳转            0 (到 0)

当我们讲解 x = 10 代码时,你看到了前两条指令,但现在在结尾处我们有 JUMP_ABSOLUTE 0。注意这些指令左侧有数字 024?在之前的代码中,我将它们剪切掉,以免让你分心,但在这里它们很重要,因为它们代表了序列中每条指令所在的位置。JUMP_ABSOLUTE 0 的作用就是告诉 Python “跳转到位置为 0 的指令”,即 LOAD_CONST 1 (10)

通过这简单的指令,我们已经将枯燥的直线代码转变成了一个更复杂的循环,不再是直线。稍后我们将看到跳转如何与测试结合,允许更复杂的移动穿越字节序列。

为什么这是倒过来的?

你可能已经注意到,Python 代码读起来像 "while True is True set x equal to 10",但 dis() 输出更像 "set x equal to 10, jump to do it again"。这是因为规则 #1 规定我们只能生成 字节序列。不允许有嵌套结构,或者比 INSTRUCTION OPTIONS 更复杂的语法。

为了遵循这个规则,Python 必须找出如何将其代码转换为一系列字节,以产生期望的输出。这意味着将实际的重复部分移动到序列的末尾,这样它就会在一个序列中。当查看字节码和汇编语言时,你会经常发现这种“倒置”的特性。

跳跃可以向前吗?

是的,从技术上讲,跳转指令只是告诉计算机在序列中处理不同的指令。它可以是下一个指令,前一个指令,或者未来的一个指令。其工作原理是计算机跟踪当前指令的“索引”,然后简单地递增该索引。

当你执行JUMP时,你在告诉计算机将此索引更改为代码中的新位置。在我们的while循环代码中(如下所示),'JUMP_ABSOLUTE'位于索引4(请看左边的4)。运行后,索引会更改为0,即LOAD_CONST所在的位置,因此计算机会再次运行该指令。这将无限循环。

     0 载入常量               1 (10)
     2 存储名称               0 (x)
     4 绝对跳转            0 (到 0)

规则 3:测试控制跳转

JUMP 用于循环很有用,但是如何做决策呢?编程中常见的一种情况是提出问题,比如:

“如果 x 大于 0,则将 y 设为 10。”

如果我们用简单的 Python 代码来写,可能会是这样的:

如果 x > 0:
    y = 10

再次强调,这是在暗示你将来会学到的东西,但这足够简单,可以想明白:

  1. Python会测试x是否大于>0。
  2. 如果是,Python将运行y = 10这行代码。
  3. 你看到if x > 0:下面缩进了吗?这被称为“代码块”,Python使用缩进来表示“这个缩进的代码是上面代码的一部分”。
  4. 如果x不大于0,那么Python会跳过y = 10这行代码。

为了使用我们的Python字节码来实现这一点,我们需要一个新的指令来执行测试部分。我们有JUMP。我们有变量。我们只需要一种方法来_比较_两个东西,然后根据比较结果进行跳转。

让我们拿那段代码并dis()它,看看Python是如何做的:

调试('''
x = 1
if x > 0:
    y = 10
''')

0 LOAD_CONST 0 (1) # 载入 1 2 STORE_NAME 0 (x) # x = 1

4 LOAD_NAME 0 (x) # 载入 x 6 LOAD_CONST 1 (0) # 载入 0 8 COMPARE_OP 4 (>) # 比较 x > 0 10 POP_JUMP_IF_FALSE 10 (to 20) # 如果为假则跳转至 20

12 LOAD_CONST 2 (10) # not false, load 10 14 STORE_NAME 1 (y) # y = 10 16 LOAD_CONST 3 (None) # done, load None 18 RETURN_VALUE # exit

如果为假则跳转至此

20 载入常量 3 (None) # 载入空值 22 返回数值 # 退出

这段代码的关键部分是 COMPARE_OPPOP_JUMP_IF_FALSE

   4 LOAD_NAME           0 (x)     # 载入 x
   6 LOAD_CONST          1 (0)     # 载入 0
   8 COMPARE_OP          4 (>)     # 比较 x > 0
  10 POP_JUMP_IF_FALSE  10 (to 20) # 如果为假则跳转至 20

这段代码的功能是:

  1. 使用 LOAD_NAME 加载 x 变量。
  2. 使用 LOAD_CONST 加载 0 常量。
  3. 使用 COMPARE_OP 进行 > 比较,并留下 TrueFalse 结果供以后使用。
  4. 最后,POP_JUMP_IF_FALSE 使得 if x > 0 起作用。它“弹出” TrueFalse 值以获取它,如果读取到 False,则会 JUMP 到第 20 条指令。
  5. 这样做将跳过设置 y 的代码,如果比较结果为 False,_但是_如果比较结果为 True,那么 Python 将直接执行下一条指令,从而开始 y = 10 序列。

花点时间仔细阅读,试着理解它。如果你有打印机,尝试打印出来,手动设置 x 为不同的值,然后追踪代码的运行过程。当你将 x = -1 时会发生什么。

你说的“pop”是什么意思?

在上面的代码中,我跳过了Python如何“弹出”值以读取它的细节,但它将其存储在一个称为“堆栈”的东西中。现在只需将其视为一个临时存储位置,您可以将值“推入”其中,然后再将其“弹出”。在您的学习阶段,您真的不需要深入了解得更多。只需理解其效果是获取最后一条指令的结果。

等等,COMPAREOP 这样的测试也用在循环中吗?

是的,根据你现在所知道的,你可能已经能够弄清楚它是如何工作的。尝试编写一个while循环,看看你是否可以根据现在所知道的让它正常工作。如果不能,不要担心,因为我们将在后面的练习中涵盖这个内容。

规则 4:存储控制测试

在代码运行时,您需要一种方式来跟踪变化的数据,这是通过“存储”来实现的。通常这种存储是在计算机的内存中,您会为内存中存储的数据创建名称。当您编写类似以下代码时,您就在做这件事:

x = 10
y = 20
z = x + y'

在上述每一行中,我们都在创建一个新的数据并将其存储在内存中。我们还为这些内存块赋予了名称 xyz。然后我们可以使用这些名称从内存中“召回”这些值,这就是我们在 z = x + y 中所做的。我们只是从内存中召回 xy 的值,然后将它们相加。

这就是故事的大部分内容,但这个小规则的重要部分是,你几乎总是使用记忆来控制测试。

当然,你可以像这样编写代码:

如果 1 < 2:
    print("但是...为什么?")

这是毫无意义的,因为这只是在一个毫无意义的测试之后运行第二行。1永远小于2,所以它是无用的。

COMPARE_OP 这样的测试非常出色的地方在于你可以使用变量使测试根据计算动态化。这就是为什么我认为这是“代码游戏规则”的一部分,因为没有变量的代码并不真正参与游戏。

花时间回顾之前的例子,找出使用LOAD指令加载值和使用STORE指令将值存储到内存的地方。

规则 5:输入/输出控制存储

代码游戏的最终规则是你的代码如何与外部世界互动。拥有变量很棒,但一个只包含你在源文件中键入的数据的程序并不是很有用。你需要的是_输入_和_输出_。

输入是从文件、键盘或网络等设备将数据传递到代码中的方式。在上一个模块中,您已经使用了 open()input() 来实现这一点。每次打开文件、读取内容并对其执行操作时,您都会访问输入。当您使用 input() 向用户提问时,也会使用输入。

输出是如何保存或传输程序结果的方式。输出可以通过 print() 输出到屏幕,通过 file.write() 输出到文件,甚至可以通过网络输出。

在这一点上输入和输出的唯一问题是字节码输出有点复杂。让我们看一个简单的例子:

dis("input('Yes? ')")```

0 载入名称 0 (输入) 2 载入常量 0 ('是吗? ') 4 调用函数 1 6 返回值

这个 dis() 运行并没有太大帮助,因为现在我们要进入一个稍后会讲到的高级主题,叫做“函数”,所以我们就到这里,把这些规则整合起来吧。

将所有内容整合在一起

采用我们拥有的5条规则,我们有以下的代码游戏:

  1. 你将数据作为程序的输入(规则#5)。
  2. 你将这些数据存储在存储器(变量)中(规则#4)。
  3. 你使用这些变量执行测试...(规则#3)。
  4. ...这样你可以在...之间跳转(规则#2)
  5. ...指令序列...(规则#1)
  6. ...将数据转换为新变量(规则#4)...
  7. ...然后将其写入输出以进行存储或显示(规则#5)。

尽管这看起来很简单,但这些小规则会创造出非常复杂的软件。视频游戏是一个很好的例子,展示了这种非常复杂的软件。视频游戏会读取您的控制器或键盘输入,更新控制场景中模型的变量,并使用高级指令将场景渲染到您的屏幕上作为输出。

你的下一步是学习应用所有这些规则的Python。

接下来的练习将参考这里的概念,以解释while循环if语句、布尔逻辑以及这些规则的更高级应用。通过学习这些规则,希望能更容易理解每个事物是如何工作的,但你告诉我。这样说通了吗?


从《笨办法学编程》学更多
总结
本文介绍了编程的五个简单规则。第一条规则是一切都是一系列指令,程序最终会被转换成计算机能理解的字节序列。第二条规则是跳转使序列变得非线性,通过跳转指令可以实现循环等复杂操作。第三条规则是测试控制跳转,通过条件判断可以实现根据不同情况执行不同的指令。文章通过示例代码和字节码展示了这些规则的应用,强调了理解这些规则有助于理解代码、调试代码以及深入学习编程。