R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

标签不是箱子:名字与对象

2

封口的箱子:可变与不可变

3

箱中箱:容器与复制

4

交出箱子的时候:函数与参数

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

机场标准流程:双下划线方法

9

规格书的规格书:继承与 MRO

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

储物柜号码:哈希与相等,综合

名牌机场:Python 对象模型
第3课

时间 1:40–2:30

箱中箱:容器与复制

失物报告单

LOST-003

水洒在一班的包里,二班的包却湿了

报告人

带初中修学旅行的尹老师

丢了什么

各班的用品套装。把基础套装“复制”后分给一班和二班,结果水洒在一班的包里,二班包里的毛巾也湿了。

结案条件

查明两个班的套装到哪一层为止是同一个箱子,并做出从里到外完全独立的套装。

天空岛国际机场 行李中心

上午十点,带队老师拿着湿毛巾的照片来到中心。据老师说,套装明明是用 copy() 复制的,两个班的毛巾却一起湿了。

都美兰班长把套装翻过来给大家看。外面的箱子确实是两个。可外箱里装的不是东西,而是插着的标签。“只是外箱买了新的。里面插的标签,指的还是同一个毛巾袋。”

3-1. 箱子里插着的是标签(10 分钟)

列表、元组、dict、set 这样的容器,装的不是对象本身,而是指向对象的引用,也就是标签。就像第 1 课里名字是标签一样,列表的每个格子也是标签。

所以同一个对象可以同时出现在好几个格子里。

row = [0, 0]
pair = [row, row]
row.append(7)
print(pair)
# [[0, 0, 7], [0, 0, 7]]
print(pair[0] is pair[1])
# True

这个性质最常闯祸的地方,就是二维列表。

grid = [[0] * 3] * 3
grid[0][0] = 9
print(grid)
# [[9, 0, 0], [9, 0, 0], [9, 0, 0]]
print(grid[0] is grid[1] is grid[2])
# True

[0] * 3 创建了一个列表。外面的 * 3 并没有把这个列表复制三份,而是插了三个指向同一个列表的标签。箱子只有一个,标签有三个。

另一方面,里面的 [0] * 3 没有问题。每个格子都指向同一个整数 0,但整数是不可变的,row[0] = 9 只是把那个格子的标签移到 9 上。共享之所以成问题,是因为共享的是可变对象。

修正方法是用推导式,每一行都创建一个新列表。

grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 9
print(grid)
# [[9, 0, 0], [0, 0, 0], [0, 0, 0]]

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

在 [[0] * 3] * 3 中,外层列表三个格子的箭头全都汇聚到同一个箱子上。推导式则为每一行各新建一个箱子。

3-2. 浅复制:只新建外箱(10 分钟)

复制列表常见的写法有五种,结果全都一样。

import copy
kit = [["water", "map"], ["snack"]]
copies = [kit[:], list(kit), kit.copy(), copy.copy(kit)]
print([c is kit for c in copies])
# [False, False, False, False]
print([c[0] is kit[0] for c in copies])
# [True, True, True, True]

这就是浅复制(shallow copy)。外箱是新做的,里面插的标签则原样移插过去。所以改动外箱和改动里面的箱子,结果是不一样的。

a = kit[:]
a.append(["hat"])          # 给外箱加一格:原件不变
print(len(kit), len(a))
# 2 3
a[0].append("towel")       # 改动里面的箱子:原件也能看到
print(kit[0])
# ['water', 'map', 'towel']

dict 也一样。d.copy()、dict(d)、{**d} 都是浅复制。

rooms = {"class1": ["water"], "class2": ["map"]}
dup = rooms.copy()
dup["class3"] = []               # 新键:原件里没有
dup["class1"].append("tea")      # 共享的列表:原件里也能看到
print(rooms)
# {'class1': ['water', 'tea'], 'class2': ['map']}

下面对同一个嵌套列表分别试试 =、浅复制和深复制。第 4 行改动的是里面的箱子,第 5 行是给外箱加一格。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

= 连外箱都是同一个,所以第 4、5 行在 kit 上都能看到。kit[:] 只有外箱是新的,所以只能看到第 4 行。copy.deepcopy 连里面的列表都新建了,kit 保持原样。

小钉提示

→ 浅复制只新建“一层”。里面装的只有数字或字符串时,浅复制就够了;里面有列表或 dict 时,想一想是不是需要深复制。

3-3. 深复制:一路新建到底(9 分钟)

copy.deepcopy 会顺着箱中箱、箱中箱中箱一路往下,全部新建。

import copy
kit = [["water", "map"], ["snack"]]
deep = copy.deepcopy(kit)
deep[0].append("rain")
print(kit[0], deep[0], deep[0] is kit[0])
# ['water', 'map'] ['water', 'map', 'rain'] False

一路往下的复制有两个陷阱:同一个箱子遇到两次,以及箱子装着自己、永远走不到头。deepcopy 用一本叫 memo 的账本同时解决这两个问题。已经复制过的对象记成 {id(原件): 副本},再遇到时就直接用账本里的副本。

x = [1]
pair = [x, x]
p2 = copy.deepcopy(pair)
print(p2[0] is p2[1], p2[0] is x)
# True False

memo = {}
copy.deepcopy(pair, memo)
print(id(x) in memo)
# True

原件里是同一个箱子的两个格子,在副本里也是同一个箱子。连共享关系都原样复制了下来。

装着自己的列表也不会无限循环。打印这样的列表时,Python 会缩写成 [...]。

a = []
a.append(a)
print(a)
# [[...]]
b = copy.deepcopy(a)
print(b[0] is b, b is a)
# True False

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

第 3 行之后,列表的格子指向它自己。deepcopy 的副本也是一个指向自己的新列表,和原件之间没有连线。
  • 自定义类可以定义 __copy__、__deepcopy__ 方法,自己决定复制方式。知道有这么回事就够了。
  • 深复制要遍历整张对象图,既慢又耗内存。很多时候,只新建需要的那几层反而更好。

3-4. 封口的箱子不复制(6 分钟)

不可变对象不会被改动,所以没有理由做副本。copy 会直接返回原件。

import copy
t = (1, 2)
print(copy.copy(t) is t, copy.deepcopy(t) is t)
# True True
s = "gate"
print(copy.deepcopy(s) is s)
# True

不过,如果元组里有可变对象,情况就不同了。浅复制仍然返回原件;深复制则必须新建里面的列表,所以会创建一个新元组。

t2 = (1, [2])
print(copy.copy(t2) is t2)
# True
dt = copy.deepcopy(t2)
print(dt is t2, dt == t2, dt[1] is t2[1])
# False True False

深复制得到的列表里的字符串也是如此。列表是新的,但里面的 "map" 和原件是同一个字符串对象。

kit = [["map"], ["snack"]]
deep = copy.deepcopy(kit)
print(deep[0] is kit[0], deep[0][0] is kit[0][0])
# False True

深复制也不会重新打包封口的箱子。改不了的东西,大家共用也很安全。

3-5. copy.replace():只改一部分的新对象(5 分钟)

想“修改”不可变对象时,就创建一个只改了一部分的新对象。从 3.13 开始,copy.replace() 用统一的方式完成这件事。frozen=True 的数据类、namedtuple、datetime.date 等对象都支持它。

import copy
from dataclasses import dataclass
from collections import namedtuple

@dataclass(frozen=True)
class Ticket:
    gate: str
    seat: str

tk = Ticket("A1", "12C")
tk2 = copy.replace(tk, seat="14F")
print(tk, tk2)
# Ticket(gate='A1', seat='12C') Ticket(gate='A1', seat='14F')

Pass = namedtuple("Pass", "gate seat")
print(copy.replace(Pass("A1", "12C"), gate="B7"))
# Pass(gate='B7', seat='12C')

tk.seat = "1A"
# dataclasses.FrozenInstanceError: cannot assign to field 'seat'

原件 tk 保持不变,生成的是只有被改字段不同的新对象。用在不支持它的对象(比如列表)上,会报 TypeError: replace() does not support list objects。

结案(10 分钟)

再来看带队老师的代码。

import copy
kit = [["towel", "notebook"], ["raincoat"]]
class1 = kit.copy()
class2 = kit.copy()
print(class1 is class2, class1[0] is class2[0])
# False True
class1[0][0] = "wet towel"
print(class2[0])
# ['wet towel', 'notebook']

外箱彼此不同,但第一格的标签指向同一个毛巾袋。水就洒在那唯一的袋子里。修正方法有两种:

kit = [["towel", "notebook"], ["raincoat"]]
class1 = copy.deepcopy(kit)
class2 = copy.deepcopy(kit)
class1[0][0] = "wet towel"
print(class2[0], class1[0] is class2[0])
# ['towel', 'notebook'] False

def new_kit():
    return [["towel", "notebook"], ["raincoat"]]   # 每次调用都是新箱子
print(new_kit()[0] is new_kit()[0])
# False

写一个创建套装的函数,等于连要复制的原件都省掉了。每次调用都从头打包,没有任何东西会被共享。

带队老师走后,小钉调出了 13 号传送带的 X 光照片。照片上,两个幽灵箱子像是互相装在对方肚子里。班长在屏幕上写下一行:

a = []
b = [a]
a.append(b)
print(a)
# [[[...]]]

“互相装着对方的箱子啊。没有标签也没被清走,原因说不定就在这儿。”班长把照片夹进了文件夹。

结案报告 — LOST-003。copy() 是浅复制,两个班共用了里面的用品袋。用 copy.deepcopy 或每次新建套装的函数解决。

课后练习

  1. 对 board = [[]] * 2 执行 board[0].append("x"),board 会变成什么?用推导式改正它。
  2. 对 d = {"a": [1]} 执行 e = dict(d) 之后,预测 e["a"].append(2) 和 e["b"] = [3] 是否分别会在 d 上体现出来。
  3. 设 x = [1]、y = [x, x]、z = copy.deepcopy(y),z[0] is z[1] 是什么?再想想如果没有 memo 会怎样。

班长的工作手册

  • 容器装的不是对象,而是引用(标签)。同一个对象可以出现在好几个格子里。

  • [[0] * 3] * 3 是同一个列表三次。二维列表用推导式创建。

  • 浅复制(lst[:]、list()、.copy()、copy.copy):只新建外层,里面共享。

  • 深复制(copy.deepcopy):一路新建到底。用 memo 让共享和循环只复制一次。

  • 不可变对象复制后还是同一个对象。里面有可变对象时,只有 deepcopy 会新建。

  • copy.replace():只改了一部分的不可变新对象(3.13)。

← 上一课

第2课 · 封口的箱子:可变与不可变

下一课 →

第4课 · 交出箱子的时候:函数与参数

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex