“==”和“is”
- ==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识
# id()返回对象标识的整数表示
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
lewis is charles
id(charles), id(lewis)
lewis['balance'] = 950
alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
# alex的值与charles相等,但是标识不同
alex == charles
alex is not charles
在变量和单例值之间比较时,应该使用is,is运算符比==速度快。
元组的相对不可变性
- 元组中不可变的是元素标识
t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
t1 == t2
id(t1[-1])
# 元组中的列表可以变化,列表是引用
t1[-1].append(99)
t1
id(t1[-1])
# 改变后不相等
t1 == t1
浅复制、深复制
- 浅复制:l2 = list(l1)、列表切片[:]默认为浅复制
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)
l1.append(100)
l1[1].remove(55)
# 列表是可变的,还是共享同一个序列
l2[1] += [33, 22]
# 元组不可变,自加操作会产生新的元组
l2[2] += (10, 11)
浅复制中的嵌套列表、元组为引用,浅复制的嵌套列表使用的是同一id的引用进行“+=”自加等改变列表的操作都会改变。由于元组为不可变序列,进行“+=”自加赋值运行会创建新的元组。
- 不要使用可变类型作为参数的默认值,例如列表。
使用列表作为参数的默认值时,如果初始化对象时传入列表,则一切正常。但是如果没有传入列表,则两个初始化对象的参数列表将是同一个列表的引用。 - copy模块的深复制deepcopy和浅复制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)
import copy
bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
# 浅复制对象
bus2 = copy.copy(bus1)
# 深复制对象
bus3 = copy.deepcopy(bus1)
# 查看内存标识id,对象id不一样
id(bus1), id(bus2), id(bus3)
bus1.dorp('Bill')
# bus1的对象列表改变了,浅复制的bus2也跟着变了
bus2.passengers
# 浅复制对象的列表属性id一样
id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
bus3.passengers
- 循环引用
a = [10, 20]
b = [a, 30]
a.append(b)
a
from copy import deepcopy
c = deepcopy(a)
c
函数的参数作为引用时
共享传参:指函数的各个形式参数获得实参中各个引用的副本,函数内部的形参是实参的别名。这样的结果将导致传入的可变对象可能会改变。
- 不要使用可变类型作为参数的默认值
# 传入的列表默认是空列表
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)
bus1 = HauntedBus(['Alice', 'Bill'])
bus1.passengers
bus1. pick('Charlie')
bus1.drop('Alice')
bus1.passengers
# 使用默认的空列表创建对象,一切正常
bus2 = HauntedBus()
bus2.pick('Carrie')
bus2.passengers
# 再默认列表创建一个对象,这里就出现问题了
bus3 = HauntedBus()
# bus3 和 bus2 使用了同一个默认对象的引用
bus3.passengers
bus3.pick('Dave')
bus2.passengers
# bus2 和 bus3 的passengers 属性标签一样
bus2.passengers is bus3.passengers
bus1.passengers
- 防御可变参数
在传入可变参数的时候,需要明确调用方是否期望修改传入的参数。
# 不希望被修改的就创建一个新的列表
class TwilightBus:
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)
del和垃圾回收
- del语句删除名称,而不是对象。当对象的最后一个引用名称被删除后,对象就被当作垃圾回收。
CPython立即回收使用的主要算法是引用计数。当对象的引用计数归零时,对象立即就被销毁。
不可变类型的“善意谎言”
- 使用另一个元组构建元组,得到的是同一个元组
t1 = (1, 2, 3)
t2 = tuple(t1)
t2 is t1
t3 = t1[:]
t3 is t1
- 字符串字面量可能会创建共享的对象
t1 = (1, 2, 3)
t3 = (1, 2, 3)
# 这里是两个不同的元组对象,不相等
t3 is t1
s1 = 'ABC'
s2 = 'ABC'
# 产生了共享的字符串对象
s2 is s1
总结
- 首先弄清楚了“==”和“is”的区别
- 进一步了解了元组的相对不可变性,即元组里的列表依然可变
- 进一步掌握对象的浅复制和深复制
- 以及要尽量避免使用可变类型作为参数的默认值,以及可变参数需要考虑要不要接受修改传进来的参数。
- 了解了CPython的del和垃圾回收机制,当对象的引用计数为零时,对象被回收。
- 最后了解了不可变类型的一些把戏,可以用于节省内存。