Lec1 环境模型
开头例子
# 以下代码的输出结果是什么?
functions = []
for i in range(5):
def func(x):
return x + i
functions.append(func)
for f in functions:
print(f(12))
16 16 16 16 16
结果是不是出乎以外? 即便并不令人惊讶,将来也会有一些时候发生意料之外的情况。因此,我们需要了解关于”幕后“发生了什么?而我们主要使用的工具,称为环境图(environment model),有人称内存图,这是用图形方式跟踪代码运行过程中计算机内部发生的事情的方法。
环境图
每当Python需要处理一个对象时,该对象都会存储在内存中;此外,Python还需要一种方法将名称与其存储在内存中的对象关联起来。因此,在我们的图示中,我们需要跟踪两个重要类别的内容:
- 正在使用的对象
- 引用这些对象的名称
在我们的图示中,我们将这两个内容分别保留在两个不同的区域中,这对应于逻辑上分离的内存区域
- 堆(存储对象的地方)
- 栈(我们用来跟踪名称的地方)
因此,一个空的环境图通常会从将绘图分成这两个区域开始。
x = 307
x = 308
y = x
y = 342
我们将x称为变量,它绑定到堆上表示307的对象;我们还将箭头本身(表示变量x与其绑定值之间的链接)称为引用。第2行,x = 308
我们看到另一个赋值语句,因此我们以完全相同的方式进行
代表307的对象,它现在孤零零地留在堆上,没有任何指向它的引用。没有指向该对象的箭头意味着我们不再有任何方法从程序中访问它!通过一种称为“垃圾回收”的过程,Python将有效地删除无法再从我们的程序中访问的对象,从而释放出该内存以供以后使用。
Python的垃圾回收过程的实际内部机制相当复杂,但在我们的图示中,我们将通过模仿Python垃圾回收,来简化这一过程:引用计数。Python通过跟踪每个堆上对象的引用数量来实现这一点(在我们的图示中,就是指向每个对象的箭头数量);当一个对象的引用数量降到0时,Python就会移除它,并释放相关内存以供以后使用
列表
mylist = [309, 310, "cat"]
列表的内容(项目)在内存中显式存储为对其他Python对象的引用,而不是对象本身。在这个例子中,我们将列表中的引用绘制成与全局帧中的引用不同的颜色,但这纯粹是为了使图示更易于查看和解释;红色和蓝色箭头表示的含义完全相同。例如,在下面这段代码中,我们最终会得到变量x
和列表的第一个位置(索引0)都指向同一个对象
x = 311
mylist = [x, 312]
那如果是
x = []
呢?
因为每个格子代表一个数据项,因此不能简单画成空格子。我们用一个竖线来表示
列表的操作
现在我们已经了解了如何表示列表,让我们来讨论一些常见的列表操作,并看看它们如何在我们的图表中工作
索引和项赋值
我们对列表执行的最常见操作之一是查找列表中包含的某个对象。
当我们使用 mylist[0]
来索引列表时,Python 首先访问方括号左边的内容,以确定我们要查看哪个对象。在这个例子中,我们通过查看全局变量里保存的 mylist
,找到它指向的内存中的列表对象。然后,方括号中的部分告诉我们查看索引 0,所以我们跟随列表中的第一个引用,找到表示整数 309 的对象.
如果我们对绘制每一步过程非常挑剔,值得注意的是,Python 还会创建一个新对象来表示表达式中的 0,并用它来索引列表。但由于这个 0 会立即被垃圾回收(没有箭头指向它),我们通常会在图示中省略这样的细节。
列表也是可变的,因为我们可以通过更改它们存储的引用来修改它们
列表切片和复制
我们可以用切片(slicing)来找到给定列表的子列表,通常的是x[start:stop],或者是x[start: stop: step]
我们也可以在这里使用负索引,所以 x[-N:]
是一种获取 x
中最后 N
个元素的方法。省略两个参数,如 x[:]
,给我们一个 x
的副本。然而,重要的是,这只是 x
的浅拷贝,因为它是一个包含 x
中所有相同引用的新列表。x[::-1]
,它生成一个新列表,其中包含 x
中的所有引用,但顺序相反; 我们还可以用x[::-1]来获取倒序的的新列表
x = [[9,8], [7,6,5]]
y1 = x
y2 = x[:]
x[0] = 20
x[1][0] = 30
print(x)
print(y1)
print(y2)
# 上述代码的输出是什么
Solution:
执行完 y=x[:]
后环境图
执行完x[1][0] = 30
后环境图
[20, [30, 6, 5]] [20, [30, 6, 5]] [[9, 8], [30, 6, 5]]
添加元素
介绍三个内置的list方法
append: 允许我们在列表尾部添加一个元素
pythonx = 7 y = [4,5,6] y.append(x)
extend: 允许我们在列表尾部添加多个元素
pythonx = [1, 2, 3] y = [4, 5] x.extend(y)
insert: 前面两个都是尾部插入数据,如果我们想在某个索引插入,可以用
x.insert(N, val)
。它会在索引N
处插入一个新的引用,使得val
现在位于索引N
删除元素
- pop方法允许我们根据索引从列表中删除一个元素。
x.pop(N)
以索引N
作为输入 - remove方法允许我们根据值而不是索引从列表中删除一个元素。
x.remove(v)
以任意对象v
作为输入。如果该对象在列表x
中存在,列表中最早出现的那个对象将被移除
连接
- +: 这里的最终结果与使用
extend
类似,只是这次我们没有在原地修改y
来添加新元素,而是创建了一个全新的列表。z = x + y
- Python 确实支持
+=
运算符在列表上,但它的行为可能与你预期的不同:它会原地修改左边的列表。也就是说,a += b
实际上等价于a.extend(b)
;它会修改a
,而不是创建一个新列表。
- Python 确实支持
考虑上面的环境图,写出对应的代码
Solution:
b = [311]
a = [307, [308], [309, 310], y, [[y]]]
考虑上面的环境图,写出对应的代码
元组
元组通过一个用逗号分隔的表达式列表表示,该列表不被方括号 []
或花括号 {}
包围。虽然元组通常被圆括号 ()
包围,但这并不是绝对必要的。例如以下两行代码都创建了元组
(1,2,3)
1,2,3
注意,单独的圆括号并不会创建元组。例如,(7)
只是一个整数,而不是元组。如果你想创建一个只包含一个元素的元组,你需要在元素后面加一个逗号,例如 (7,)
或 7,
。在元组、列表和字典中,尾随的逗号是被忽略的。
创建一个空元组时,圆括号表示元组。例如,你可以使用 ()
创建一个空元组,也可以使用其构造函数 tuple()
来创建空元组。
何时使用环境图
我们的最终目标是,通过手工绘制这些图将来可能完全不再必要;我们预期这一点最终会实现,这不仅仅因为这些图不再有用,而是因为你的大脑会在潜意识中自动考虑这些步骤。然而,要内化这些工作原理需要时间,手工绘制图表可以帮助你更快地内化这些内容。而且,拥有一种特定、一致的绘图方式意味着我们都有一个共享的语言,可以用来讨论 Python 内部的概念(超越语法本身)