变量不是盒子
a = [1,2,3]
b = a
a.append(4)
print(b) # [1, 2, 3, 4]
如果把变量想象为盒子,那么无法解释Python中的赋值;应该把变量视作便利贴,上述的例子就可以理解了。
创建了对象之后,才会把变量分给对象
class Gizmo:
def __init__(self):
print('Gizmo id:'+str(id(self)))
x = Gizmo() # Gizmo id:2818379134344
y = Gizmo()*10 # 在乘法运算中使用Gizmo实例会抛出异常。
# Gizmo id:1795695359368 # 这里表明,在尝试求积之前其实会创建一个新的Gizmo实例。
# Traceback (most recent call last):
# File "D:/GZRobort/General_DBAI/test.py", line 21, in <module>
# y = Gizmo()*10
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
print(dir()) # ['Gizmo', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'x']; 但是,肯定不会创建变量y,因为在对赋值语句的右边进行求值时抛出了异常。
为了理解Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。因为变量只不过是标注,所以无法阻止为对象贴上多个标注。贴的多个标注,就是别名。
标识、相等性和别名
charles = {'name':'Charles','born':1832}
lewis = charles # lewis是charles的别名
print(lewis is charles)
print(id(lewis), id(charles)) # is运算符和id函数确认了这一点
lewis['balence'] = 950 # 向lewis中添加一个元素相当于向charles中添加一个元素
print(charles)
alex = charles = {'name':'Charles','born':1832,'balence':950} # alex指代的对象与赋值给charles的对象内容一样
print(alex == charles) # 比较两个对象,结果相等,这是因为dict类的__eq__方法就是这样实现的
print(alex is not charles) # 但它们是不同的对象。这是Python说明标识不同的方式:a is not b
输出:
True
2844253519160 2844253519160
{'name': 'Charles', 'born': 1832, 'balence': 950}
True
False
在这段代码中,lewis和charles是别名,即两个变量绑定同一个对象。而alex不是charles的别名,因为二者绑定的是不同的对象。alex和charles绑定的对象具有相同的值(==比较的就是值),但是它们的标识不同。
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is运算符比较两个对象的标识;id( )函数返回对象标识的整数表示。
在==和is之间选择
运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识。
is运算符比速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。
元组的相对不可变性
元组与多数Python集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识。
t1 = (1,2,[30,40]) # t1不可变,但是t1[-1]可变
t2 = (1,2,[30,40]) # 构建元组t2,它的元素与t1一样
print(t1 == t2) # 虽然t1和t2是不同的对象,但是二者相等——与预期相符
print(id(t1[-1])) # 查看t1[-1]列表的标识
t1[-1].append(99) # 就地修改t1[-1]列表
print(t1)
print(id(t1[-1])) # t1[-1]的标识没变,只是值变了
print(t1 == t2) # t1和t2不相等
输出:
True
2676098823304
(1, 2, [30, 40, 99])
2676098823304
False
默认做浅复制
t1 = [3,[55,44],(7,8,9)]
t2 = list(t1) # list(l1)创建l1的副本
print(t2)
print(t2 == t1) # 副本与源列表相等
print(t2 is t1) # 但是二者指代不同的对象。对列表和其他可变序列来说,还能使用简洁的l2=l1[:]语句创建副本
t1.append(100) # 把100追加到t1中,对t2没有影响
t1[1].remove(55) # 把内部列表t1[1]中的55删除。这对t2有影响,因为t2[1]绑定的列表与t1[1]是同一个
print('t1:',t1)
print('t2:',t2)
t2[1]+= [33,22] # 对可变的对象来说,如t2[1]引用的列表,+=运算符就地修改列表。这次修改在t1[1]中也有体现,因为它是t2[1]的别名
t2[2]+= (10,11) # 对元组来说,+=运算符创建一个新元组,然后重新绑定给变量t2[2]。这等同于t2[2]=t2[2]+(10, 11)。现在,t1和t2中最后位置上的元组不是同一个对象
print('t1:',t1)
print('t2:',t2)
输出:
[3, [55, 44], (7, 8, 9)]
True
False
t1: [3, [44], (7, 8, 9), 100]
t2: [3, [44], (7, 8, 9)]
t1: [3, [44, 33, 22], (7, 8, 9), 100]
t2: [3, [44, 33, 22], (7, 8, 9, 10, 11)]
构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题。
为任意对象做深复制和浅复制
浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对象的引用)。copy模块提供的deepcopy和copy函数能为任意对象做深复制和浅复制。
函数的参数作为引用时
共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。
def f(a,b):
a+= b
return a
x,y = 1,2
print(f(x,y))
print(x,y)
a,b = [1,2],[3,4]
print(f(a,b))
print(a,b)
t,u = (10,20),(30,40)
print(f(t,u))
print(t,u)
输出:
3
1 2
[1, 2, 3, 4]
[1, 2, 3, 4] [3, 4]
(10, 20, 30, 40)
(10, 20) (30, 40)
不要使用可变类型作为参数的默认值
防御可变参数
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 = ['S','T','M','D','P']
bus = TwilightBus(basketball_team)
bus.drop('S')
bus.drop('T')
print(basketball_team) # ['M', 'D', 'P'] ## 下车的学生从篮球队中消失了