环境图表(Environment Diagram)是理解程序运行以及变量变化的重要方式. 环境图表的变换过程招式着程序在各个阶段的状态. 环境图表的使用可以在Python Tutor找到.
什么是环境图表?
环境图表(Environment Diagram)是一种很意识流的概念, 并不是为了实际应用而生. 环境图表给予了初学者们对于程序运行过程中每一步的状态(尤其是针对各个变量的状态)提供可视化展现. 当程序愈发复杂(更高阶的方程, nonlocal state等)时, 环境图表提供了一种系统化的方式来追踪各个变量的状态. 多加练习一些复杂的程序的分析可以为以后对程序的理解提供很好的帮助.
相关名词
- Frame: 每个
frame
相当于一个模块, 每一个在其模块中的变量将会被追踪. 对于变量来说, 它所在的frame
相当于其扎根的地方. 每一个方程调用都将会有其独立的frame
. - Global Frame: 顾名思义,
global frame
指的是初始的模块, 也就是环境模块. - Parent Frame: 每一个除了
global frame
以外的frame
都有自己的parent frame
(父模块). 每当我们调用一个函数方程, 一个新的frame
会被建立, 而其被建立的frame
就是其parent frame
. 当你在追踪变量的时候, 如果你在当前frame
下找不到这个变量, 那么你可以去其parent frame
去寻找那个变量的定义. - Frame Number: 每一个
frame
也可能含有一个frame number
. 它是用来标记作为parent frame
的frame
的. 如果一个frame
不作为parent frame
, 则其可以不标记frame number
.
初识环境图表
运行机制
在登陆Python Tutor以后, 我们可以输入想要运行的程序并查看程序在每一步的运行过程. 例如, 我们输入以下内容:
并点击运行, 即可发现如下显示:
其中, 分析其所代表的含义我们可以了解到:
其中淡绿色箭头代表着刚刚被运行(Just executed)的一行(程序), 红色箭头代表着下一个(Next to execute)将被运行的一行. 从左侧的代码中我们可以看到, 第一行的程序 1
from math import pi
import
语句(Import statement). 意思是从math这个集群包中引入pi
这个变量. 此时, 一个名为pi
的变量在我们的程序中存在了. 当前处于的模块为global frame
, 所以在右侧, 我们可以看到, 我们的global frame
模块中存在了一个名为pi
的变量, 其值为约3.1416
. 其中, 左侧的pi
作为变量名称(Name)出现, 而右侧的3.1416
作为变量值(Value)展示. 我们可以看到, 在每一个frame
里, 每一个变量名(Name)都对应着一个变量值(Value). 在同一个frame
内, 不能存在两个重复名称的变量名.
赋值变换
再来看以下的代码:
我们发现, 横线上方是我们运行了前两行(对变量a
和b
赋值)以后的结果. 我们看到, 右侧显示我们在global frame
中存在了名为a
, b
的变量及其对应的值. 第三行代码的作用是将a+b
的值赋值给b
, 并将原本b
的值赋值给a
. 我们看到, 当第三行代码运行结束的时候, 在右侧的变量a
, b
的值发生了更改. 因为我们没有产生新的变量名, 而原有变量名的值改变了, 所以我们只需要在环境图表中更改已经变换了的变量值.
函数方程调用
下面的这段代码表述了一个计算的过程:
值得注意的是, 代码中的min
, max
并不是传统变量, 而是python
语言系统自带的函数方程. 第一行代码将变量f
赋值为min
函数, 意味着我们可以像使用min
函数一样使用f
. 随后, 我们又将f
重新赋值为max
. 第三行时我们给g
赋值min
, 给h
赋值max
. 随后我们又给max
这个变量名赋值一个新的含义使其等同于g
也就是min
函数的意义. 在右侧, 我们可以发现, 在global frame
中多出来一共4个变量名. 原本系统自带的max
, min
函数并不会出现在frame
中, 但是由于我们给max
这个变量名赋予了新的定义, 我们将其也写入frame
中. 运行了代码的前4行以后的结果为, 变量名f
, h
的意义均为系统原本的max
方程. 由于其是函数方程, 所以我们用箭头指向func max(...)
. 其中func
代表其为函数性质, 而max(...)
则代表其具体含义. 对于其他变量均为同理. 接下来我们将看到第五行计算的过程: 为了更加简要, 上图列出了其计算的过程. 其分析过程大家可以尝试着理解一下作为小练习. 但是我们好像并没有发现右侧的global frame
中出现任何变化. 这是因为我们只是进行了计算, 并没有进行明确的赋值给任何的新定义的变量名, 并且我们调用的函数本质上都是系统的自带函数, 所以不需要新建frame
.
自定义方程的调用
如果我们用到了自己定义的方程的话, 改如何应对呢? 下图展示了一个例子(示例中展示的是程序运行的最终形态):
从左侧我们可以看到, 我们先从operator
库中import
了一个变量mul
. 其性质是一个系统自带方程. 由此我们可以看到在右侧的global frame
中也有这个名为mul
的变量名存在, 其性质是Built-in function
也就是系统自带方程. 代码的第二行和第三行自定义了一个名为square
的方程, 并在第四行进行了调用. 经过第三行以后我们的global frame
中就会出现square
这个变量名了, 因为我们完成了对于这个变量名的定义. 通过其箭头指向我们知道其性质为func
, 内容为square(x)
. 接下来让我们关注他的第四行调用的过程. 因为我们调用了一个新的自定义函数, 我们将建立一个新的frame
名为square
也就是我们方程的名字. 由于在我们定义的时候(第二行), 我们将函数的input
定义了变量名为x
(注意, 这个变量x
只在square
函数内部存在并有效), 我们的square
模块中有了一个名为x
的变量 (也可见global frame
中square
变量性质表达处), 其值为我们第4行给square
函数的input
, 也就是-2
. 经过计算, 我们得知了其return value
为4. 注意, 右侧红色字体的return value
的含义并不是一个变量名, 因为我们在程序中并没有在square
函数内有任何对于其他变量名的定义与赋值, 这里的return value
谨代表方程return
的值. 当前程序所在的frame
, 也就是当前加深颜色的frame
, 是我们的local frame
(当前模块), 因为这是我们当前正在处理运行的模块.