网站首页 > 技术文章 正文
本文是《流畅的Python》第八章的读书笔记,本文先讨论对象标识、值和别名等概念。随后,会揭露元组的一个神奇特性:元组是不可变的,但是其中的值可以改变。然后就引申到浅复制和深复制。最后的话题是引用和函数参数:可变的参数默认值导致的问题,以及如何安全地处理函数的调用者传入的可变参数。
本文的内容有点儿枯燥,但是这些话题却是解决 Python 程序中很多不易察觉的 bug 的关键。
标识、相等性和别名
在Python中,每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;可以把标识理解为对象在内存中的地址。 is 运算符比较两个对象的标识,而==比较两个对象的值; id() 函数返回对象的内存地址。
t1 = {'name': 'Lili', 'born': 2001}
t2 = {'name': 'Lili', 'born': 2001}
t1 == t2
True
t1 is t2
False
(id(t1), id(t2))
(140059319293992, 140059319293848)
当然,你可以在自己的类中定义 __eq__ 方法,从而决定 == 如何比较实例。如果不覆盖 __eq__ 方法,那么从 object 继承的方法比较对象的 ID,因此这种后备机制认为用户定义的类的各个实例是不同的。
元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。 如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
t1 = (1, 2, [30, 40])
t1[2].append(100)
t1
(True, False)
默认做浅复制
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)
(l2 == l1, l2 is l1)
(True, False)
(id(l1), id(l2))
(140059319253960, 140059319197384)
可以看出,副本与源列表相等,但是二者指代不同的对象。对列表和其他可变序列来说,还能使用简洁的 l2 = l1[:] 语句创建副本。
然而,构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。看看下面的代码:
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # ?
l1.append(100) # ?
l1[1].remove(55) # ?
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22] # ?
l2[2] += (10, 11) # ?
print('l1:', l1)
print('l2:', l2)
l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
? l2 是 l1 的浅复制副本。此时的状态如图。
? 把 100 追加到 l1 中,对 l2 没有影响。
? 把内部列表 l1[1] 中的 55 删除。这对 l2 有影响,因为 l2[1] 绑定的列表与 l1[1]是同一个。
? 对可变的对象来说,如 l2[1] 引用的列表, += 运算符就地修改列表。这次修改在l1[1] 中也有体现,因为它是 l2[1] 的别名。
? 对元组来说, += 运算符创建一个新元组,然后重新绑定给变量 l2[2]。现在, l1 和 l2 中最后位置上的元组不是同一个对象。
为任意对象做深复制和浅复制
有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。
为了演示 copy() 和 deepcopy() 的用法,定义了一个简单的类 Bus。这个类表示运载乘客的校车,在途中乘客会上车或下车。
import copy
class Bus:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
bus1 = Bus(['Alice', 'Bill', 'David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
id(bus1), id(bus2), id(bus3) # ?
(140059318933488, 140059318933600, 140059318933264)
id(bus1.passengers), id(bus2.passengers), id(bus3.passengers) # ?
(140059319442504, 140059319442504, 140059319440968)
bus1.drop('Bill')
bus2.passengers # ?
['Alice', 'David']
bus3.passengers # ?
['Alice', 'Bill', 'David']
? 使用 copy 和 deepcopy,创建 3 个不同的 Bus 实例。
? 审查 passengers 属性后发现,bus1 和 bus2 共享同一个列表对象,因为 bus2 是bus1 的浅复制副本。
? bus1 中的 'Bill' 下车后,bus2 中也没有他了。
? bus3 是 bus1 的深复制副本,因此它的 passengers 属性指代另一个列表。
深复制有时可能太深了。例如,对象可能会引用不该复制的外部资源或单例值。我们可以实现特殊方法 __copy__() 和 __deepcopy__(),控制 copy 和 deepcopy 的行为。
函数的参数传递
Python 唯一支持的参数传递模式是共享传参(call by sharing),共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。函数可能会修改接收到的任何可变对象。
不要用可变类型作为参数的默认值
默认参数最好指向不可变对象,否则会引起难以调试的问题。
class HauntedBus:
"""备受幽灵乘客折磨的校车"""
def __init__(self, passengers=[]): # ?
self.passengers = passengers # ?
def pick(self, name):
self.passengers.append(name) # ?
def drop(self, name):
self.passengers.remove(name)
? 如果没传入 passengers 参数,使用默认绑定的列表对象,一开始是空列表。
? 这个赋值语句把 self.passengers 变成 passengers 的别名,而没有传入passengers 参数时,后者又是默认列表的别名。
? 在 self.passengers 上调用 .remove() 和 .append() 方法时,修改的其实是默认列表,它是函数对象的一个属性。
bus1 = HauntedBus()
bus1.pick('Carrie')
bus2 = HauntedBus()
print(bus2.passengers) # ['Carrie']
bus2.passengers is bus1.passengers
['Carrie']
True
可以看出 bus2 的列表竟然不为空。这种问题很难发现。实例化 HauntedBus 时,如果传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪的事就发生了,这是因为 self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
防御可变参数
如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。
class TwilightBus:
"""让乘客销声匿迹的校车"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] # ?
bus = TwilightBus(basketball_team) # ?
bus.drop('Tina') # ?
bus.drop('Pat')
basketball_team # ?
['Sue', 'Maya', 'Diana']
basketball_team 中有 5 个学生的名字,使用这队学生实例化 TwilightBus。两个学生下车了,下车的学生从篮球队中消失了!
当 passengers 不为 None 时,self.passengers 变成 passengers 的别名,而后者是传给 __init__ 方法的实参的别名。在 self.passengers 上调用 .remove() 和 .append() 方法其实会修改传给构造方法的那个列表。
这里的问题是,校车为传给构造方法的列表创建了别名。正确的做法是,校车自己维护乘客列表。修正的方法很简单:在 init 中,传入 passengers 参数时,应该把参数值的副本赋值给 self.passengers。
正确的做法应该如下:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
总结
变量保存的是引用,这一点对 Python 编程有很多实际的影响。
- 简单的赋值不创建副本。
- 对 += 或 *= 所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。
- 为现有的变量赋予新值,不会修改之前绑定的变量。这叫重新绑定:现在变量绑定了其他对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
- 函数的参数以别名的形式传递,这意味着,函数可能会修改通过参数传入的可变对象。这一行为无法避免,除非在本地创建副本,或者使用不可变对象(例如,传入元组,而不传入列表)。
- 使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也就变了,这会影响以后使用默认值的调用。
- 上一篇: 学会这15点,让你分分钟拿下Redis数据库
- 下一篇: 怎么使用VBA对象库(vba 对象)
猜你喜欢
- 2024-09-18 面试时被问到单例模式,怎么回答才能让面试官眼前一亮?
- 2024-09-18 JVM系列之:对象的锁状态和同步(对象锁和方法锁)
- 2024-09-18 心理测试:四个卫生间你会选择哪一个,测出你是一个怎样的人!
- 2024-09-18 用这个方法,发现你命中注定的另一半
- 2024-09-18 Mybatis为什么查询结果为空时返回值为NULL或空集合?
- 2024-09-18 ES6对象拦截器 Proxy(electron拦截请求)
- 2024-09-18 求教!CAD多段线在布局视口中变成了“空心”,怎么办
- 2024-09-18 张明楷刑法笔记(13-15)结果、因果关系、结果归属
- 2024-09-18 Mybatis查询结果为空时,为什么返回值为NULL或空集合?
- 2024-09-18 一个单相思的女孩,写给暗恋对象的信
- 1513℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 560℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 506℃MySQL service启动脚本浅析(r12笔记第59天)
- 485℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 483℃启用MySQL查询缓存(mysql8.0查询缓存)
- 463℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 443℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 440℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)