失物报告单
LOST-003
报告人
带初中修学旅行的尹老师
丢了什么
各班的用品套装。把基础套装“复制”后分给一班和二班,结果水洒在一班的包里,二班包里的毛巾也湿了。
结案条件
查明两个班的套装到哪一层为止是同一个箱子,并做出从里到外完全独立的套装。
天空岛国际机场 行李中心
上午十点,带队老师拿着湿毛巾的照片来到中心。据老师说,套装明明是用 copy() 复制的,两个班的毛巾却一起湿了。
都美兰班长把套装翻过来给大家看。外面的箱子确实是两个。可外箱里装的不是东西,而是插着的标签。“只是外箱买了新的。里面插的标签,指的还是同一个毛巾袋。”
列表、元组、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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
复制列表常见的写法有五种,结果全都一样。
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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
小钉提示
→ 浅复制只新建“一层”。里面装的只有数字或字符串时,浅复制就够了;里面有列表或 dict 时,想一想是不是需要深复制。
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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
__copy__、__deepcopy__ 方法,自己决定复制方式。知道有这么回事就够了。不可变对象不会被改动,所以没有理由做副本。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深复制也不会重新打包封口的箱子。改不了的东西,大家共用也很安全。
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。
再来看带队老师的代码。
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 或每次新建套装的函数解决。
board = [[]] * 2 执行 board[0].append("x"),board 会变成什么?用推导式改正它。d = {"a": [1]} 执行 e = dict(d) 之后,预测 e["a"].append(2) 和 e["b"] = [3] 是否分别会在 d 上体现出来。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)。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。