Skip to content

Lec1 环境模型

开头例子

python
# 以下代码的输出结果是什么?
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还需要一种方法将名称与其存储在内存中的对象关联起来。因此,在我们的图示中,我们需要跟踪两个重要类别的内容:

  • 正在使用的对象
  • 引用这些对象的名称

在我们的图示中,我们将这两个内容分别保留在两个不同的区域中,这对应于逻辑上分离的内存区域

  • 堆(存储对象的地方)
  • 栈(我们用来跟踪名称的地方)

因此,一个空的环境图通常会从将绘图分成这两个区域开始。

截屏2024-06-02 05.36.40

python
x = 307
x = 308
y = x
y = 342

我们将x称为变量,它绑定到堆上表示307的对象;我们还将箭头本身(表示变量x与其绑定值之间的链接)称为引用。第2行,x = 308 我们看到另一个赋值语句,因此我们以完全相同的方式进行

截屏2024-06-02 05.33.28

代表307的对象,它现在孤零零地留在堆上,没有任何指向它的引用。没有指向该对象的箭头意味着我们不再有任何方法从程序中访问它!通过一种称为“垃圾回收”的过程,Python将有效地删除无法再从我们的程序中访问的对象,从而释放出该内存以供以后使用。

Python的垃圾回收过程的实际内部机制相当复杂,但在我们的图示中,我们将通过模仿Python垃圾回收,来简化这一过程:引用计数。Python通过跟踪每个堆上对象的引用数量来实现这一点(在我们的图示中,就是指向每个对象的箭头数量);当一个对象的引用数量降到0时,Python就会移除它,并释放相关内存以供以后使用

列表

python
mylist = [309, 310, "cat"]

截屏2024-06-02 05.53.43

列表的内容(项目)在内存中显式存储为对其他Python对象的引用,而不是对象本身。在这个例子中,我们将列表中的引用绘制成与全局帧中的引用不同的颜色,但这纯粹是为了使图示更易于查看和解释;红色和蓝色箭头表示的含义完全相同。例如,在下面这段代码中,我们最终会得到变量x和列表的第一个位置(索引0)都指向同一个对象

python
x = 311
mylist = [x, 312]

截屏2024-08-21 12.09.36

那如果是 x = []呢?

因为每个格子代表一个数据项,因此不能简单画成空格子。我们用一个竖线来表示

截屏2024-06-03 02.19.43

列表的操作

现在我们已经了解了如何表示列表,让我们来讨论一些常见的列表操作,并看看它们如何在我们的图表中工作

索引和项赋值

我们对列表执行的最常见操作之一是查找列表中包含的某个对象。

截屏2024-08-21 12.14.25

当我们使用 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]来获取倒序的的新列表

python
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[:]后环境图

截屏2024-08-21 15.07.43

执行完x[1][0] = 30后环境图

截屏2024-08-21 15.09.01

[20, [30, 6, 5]] [20, [30, 6, 5]] [[9, 8], [30, 6, 5]]

添加元素

介绍三个内置的list方法

  • append: 允许我们在列表尾部添加一个元素

    python
    x = 7
    y = [4,5,6]
    y.append(x)
  • extend: 允许我们在列表尾部添加多个元素

    python
    x = [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,而不是创建一个新列表。

image-20240821153450224

考虑上面的环境图,写出对应的代码

Solution:

python
b = [311]
a = [307, [308], [309, 310], y, [[y]]]

image-20240821153502685

考虑上面的环境图,写出对应的代码

元组

元组通过一个用逗号分隔的表达式列表表示,该列表不被方括号 [] 或花括号 {} 包围。虽然元组通常被圆括号 () 包围,但这并不是绝对必要的。例如以下两行代码都创建了元组

python
(1,2,3)
1,2,3

注意,单独的圆括号并不会创建元组。例如,(7) 只是一个整数,而不是元组。如果你想创建一个只包含一个元素的元组,你需要在元素后面加一个逗号,例如 (7,)7,。在元组、列表和字典中,尾随的逗号是被忽略的。

创建一个空元组时,圆括号表示元组。例如,你可以使用 () 创建一个空元组,也可以使用其构造函数 tuple() 来创建空元组。

何时使用环境图

我们的最终目标是,通过手工绘制这些图将来可能完全不再必要;我们预期这一点最终会实现,这不仅仅因为这些图不再有用,而是因为你的大脑会在潜意识中自动考虑这些步骤。然而,要内化这些工作原理需要时间,手工绘制图表可以帮助你更快地内化这些内容。而且,拥有一种特定、一致的绘图方式意味着我们都有一个共享的语言,可以用来讨论 Python 内部的概念(超越语法本身)