Python中的赋值运算符

Python中的赋值运算符(=)对不同的使用场景具有不同的“内涵”,可能导致输出不符合预期。

1. Python特点

Python中所有数据类型都是类,包括数字、字符串等所谓的基本数据类型(“primitive” data type)。例如,整数数据类型int是一个类,当把一个整数赋值给一个变量时

a = 1

1实际上是类的实例,a创建了一个对象。在IDLE中,输入a.后停留一段时间,弹出就是该对象的属性和方法列表。
在这里插入图片描述

2. 赋值运算符的行为

当在Python中第一次把一个对象赋值给变量时,如

a = 1

相当于在内存中开辟了一块空间来存放该对象,变量绑定该对象。所以,赋值运算符实际上是创建引用。因为只有一个变量绑定在该对象上,引用计数等于1,如下图所示。

在这里插入图片描述
如果对a进行拷贝,如

b = a

拷贝同样使用了赋值运算符,因此它同样是创建引用。由于a把引用拷贝给了b,b和a绑定了同一个对象,它们指向同一块内存区域。这时候,有两个变量绑定了该对象,引用计数等于2。

在这里插入图片描述
根据以上两个例子,完全有理由认为,Python的赋值运算符就是创建引用。

(当引用计数等于0的时候,对象被销毁,内存被释放

在这里插入图片描述

3. 变量的IDentity

identity是Python中变量的“身份证”。内置的id()函数返回变量的identity,对于CPython,id()返回的identity就是该变量绑定的对象在内存中的地址。

4. 可变和不可变数据类型

Python的数据类型分为两大类——可变(mutable)数据类型和不可变(immutable)数据类型。判断一种数据类型是否可变的依据是该类型的数据是否允许改变。换一种说法是,改变该类型的数据时,identity是否变化。

参考官方文档,数值类型(int和float)、字符串(str)和元组(tuple)是不可变数据类型,列表(list)、字典(dict)和集合(set)是可变数据类型。

利用id()函数验证改变变量的值时,地址是否发生改变。同时可以验证复合对象(compound object,把两种或多种对象组合成一种对象,如[1, 1.1, ‘ab’, (5, 6), [2, 3], {‘a’: 3}, set({1,2})] )的内部数据元素发生变化时,地址是否发生改变。

4.1 可变数据类型操作

4.1.1 赋值

a = [1, 2, 3]
print(id(a))
a = [1, 2, 3]
print(id(a))

观察输出结果可以发现,a两次地址是不同的。因为对于解释器来说,第一行把a绑定在对象[1, 2, 3]上,第三行给a再次赋值的时候,首先把a和原来的对象[1, 2, 3]解除绑定,然后把a绑定在另一个对象[1, 2, 3]上,尽管两个对象的值完全一致,但它们仍然是放在不同位置的两个对象。

4.1.2 拷贝

前面说到赋值运算符的操作是把变量绑定在对象上,如

a = [1, 2, 3]
b = a
print(id(a))
print(id(b)) # id(a) == id(b)
print(a is b) # True

4.1.3 修改

实际上就是b就是a指向的对象的引用。如果改变a的元素改变,相应b也会改变。

a.append(4)
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4]

原因是,它们指向同一块区域,该区域的内容改变,变量的值也随之改变。

4.2 不可变数据类型操作

4.2.1 赋值

同样地,对变量进行两次相同值的赋值,它们的地址也不会相同

a = 257
print(id(a))
a = 257
print(id(a)) // 两次地址不同

4.2.2 拷贝

a = 257
b = a
print(id(a))
print(id(b)) // 两个地址相同

4.2.3 修改

不可变类型不允许修改,所以任何“修改”都是重新赋值,相当于把引用绑定在另一个对象上

a = 257
print(id(a))
a = 258
print(id(a)) // 两次地址不同

5. 小整数池

4.2 不可变数据类型操作中没有使用1、2、3这种小的整数来作为例子的原因是为了避免小整数的地址对判断干扰。CPython中有一个小整数的概念,范围是[-5, 256]。因为在使用中我们会多次使用小整数,也会因此多次分配和释放内存,所以CPython为了提高效率,提前为这一范围内的整数分配了空间,所以只要在小整数范围内的同一个值,无论多少次赋值,地址都不变。

a = 1
b = 1
c = 1
print(a is b is c) # True

6. 浅拷贝

文档上解释,浅拷贝的底层操作是新建一个复合对象,并把原复合对象里的对象的引用添加到新的复合对象里。

A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.

所以,复合对象是新的,但内部的对象还是原复合对象的引用。以一个包含整数和列表的列表(列表的列表,复合对象)为例

import copy
a = [257, 258, [259, 260]] # 复合对象
				           # 避免使用小整数,干扰判断
b = copy.copy(a)

根据文档内容,猜测b具有以下特点:

  1. a和b的地址不同
  2. b内部元素的地址与a内部元素地址一致
print('a:', id(a))
print('b:', id(b))
for i in range(len(a)):
   print('a['+str(i)+']: '+str(id(a[i]))+', b['+str(i)+']:'+str(id(b[i])))

"""
输出
a: 1759710552768
b: 1759710551488
a[0]: 1759710370736, b[0]:1759710370736
a[1]: 1759710370768, b[1]:1759710370768
a[2]: 1759710552960, b[2]:1759710552960
"""

整数是不可变数据类型,而列表是可变数据类型。如果改变a[0]的值,b[0]不会改变;如果a[2]增添元素,b[2]也会改变。因为b的元素只是a的元素的引用。

a[0]=261
a[2].append(262)
print(a) # [261, 258, [259, 260, 262]]
print(b) # [257, 258, [259, 260, 262]]
for i in range(len(a)):
	print('a['+str(i)+']: '+str(id(a[i]))+', b['+str(i)+']:'+str(id(b[i])))
"""
a[0]: 1759710370896, b[0]:1759710370736
a[1]: 1759710370768, b[1]:1759710370768
a[2]: 1759710552960, b[2]:1759710552960
"""

观察a和b的值以及a和b内部元素的地址,发现a的整数改变不影响b,a内部的列表修改会影响b,同时a[0]的地址改变,其他没有变化,与预测一致。

除此之外,因为a和b是不同的对象,地址不同,对a增添元素不改变b

a.append('a')
print(a) # [261, 258, [259, 260, 262], 'a']
print(b) # [261, 258, [259, 260, 262]]

另外,4.1.1中关于可变数据的赋值提到

观察输出结果可以发现,a两次地址是不同的。因为对于解释器来说,第一行把a绑定在对象[1, 2, 3]上,第三行给a再次赋值的时候,首先把a和原来的对象[1, 2, 3]解除绑定,然后把a绑定在另一个对象[1, 2, 3]上,尽管两个对象的值完全一致,但它们仍然是放在不同位置的两个对象。

假如,进行如下操作

b[2] = [259, 260, 262]

根据前面理论的预测:

  1. a[2]和b[2]的地址不同
  2. a[2]元素的增添不改变b[2]
for i in range(len(a)):
   print('a['+str(i)+']: '+str(id(a[i]))+', b['+str(i)+']:'+str(id(b[i])))
"""
a[0]: 1759710370896, b[0]:1759710370736
a[1]: 1759710370768, b[1]:1759710370768
a[2]: 1759710552960, b[2]:1759710265792
"""
a[2].append(263)
print(a) # [261, 258, [259, 260, 262, 263]]
print(b) # [257, 258, [259, 260, 262]]

原因是b[2]重新赋值时,b[2]绑定在新的对象上,a[2]改变不影响b[2]

7. 深拷贝

深拷贝创建一个新的复合对象,并把原复合对象内的对象逐个(递归)拷贝至新的复合对象。相当于把原复合对象的值拷贝至新复合对象

A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.

import copy
a = [257, 258, [259, 260]] # 复合对象
				           # 避免使用小整数,干扰判断
b = copy.deepcopy(a)

根据理论预测:

  1. a和b的地址不相同
  2. 对a的任何操作不影响b
b = copy.deepcopy(a)
print(id(a)) # 1759710554560
print(id(b)) # 1759710553792
for i in range(len(a)):
	print('a['+str(i)+']: '+str(id(a[i]))+', b['+str(i)+']:'+str(id(b[i])))
"""
a[0]: 1759710371600, b[0]:1759710371600
a[1]: 1759710371504, b[1]:1759710371504
a[2]: 1759703794240, b[2]:1759707410688
"""
a[2].append(261)
a.append(262)
print(a) # [257, 258, [259, 260, 261], 262]
print(b) # [257, 258, [259, 260]]

a[2]与b[2]地址不同很容易理解,但是a[0]==b[0],a[1]==b[1]可能和预期结果不同。重新关注深拷贝的解释,其中有一个关键词recursively。它的意思是对于复合对象,把原复合对象里的每一个对象都深拷贝至新的复合对象。测试以下代码

import copy
c = 257
d = [257]
e = copy.deepcopy(c)
f = copy.deepcopy(d)
print(c is e) # True
print(d is f) # False

推测深拷贝的底层操作,对于不可变数据类型的深拷贝是创建引用,对于可变数据类型的拷贝是绑定新的对象。注意:可变数据类型的内部还是不可变数据类型,所以它的内部元素只是创建引用,例如

print(d[0] is f[0]) # True

这是可以理解的,因为对于不可变数据类型,改变值相当于重新赋值,所以重新创建一块空间存放相同的值意义不大,并且增加了申请和释放内存的时间。

8. 类的拷贝与属性

类的实例是对象遵循以上原则。重点关注类内属性定义的位置问题。
类的属性可以在初始化函数里,也可以在初始化函数外面。它们的区别是:初始化函数外定义的属性不需要类的实例就可以获得,而初始化函数里的属性必须通过实例才能获得。

class C:
	a = 1
	b = []
	def __init__(self):
		self.c = 2
		self.d = [1]
print(C.a) # 1
print(C().a) # 1
#print(C.c) # AttributeError: type object 'C' has no attribute 'c'
print(C().c) # 2

从上面的结果可以看出,初始化函数外定义的属性在类生成的时候随之生成,而初始化函数内定义的属性在实例化的时候才生成。那么,可以预测:

  1. 类的初始化函数外定义的属性是该类所有实例“共享”的,而类的初始化函数内定义的属性是该类各个实例所独占的;
  2. 对于类的初始化函数外定义的不可变数据类型属性,该属性只在各个类的实例初始化的时候共享。一旦该属性的值改变,由于它是不可变数据类型,地址也发生改变,成为了实例独占的属性;
  3. 对于类的初始化函数外定义的可变数据类型属性,该属性是真正被该类各实例所共享。因为属性是可变数据类型,修改该属性的值不会改变地址,所以它的值被各个实例共享,除非是重新赋值
  4. 对于类的初始化函数内定义的属性,任何情况下都是被该类各实例所独占

通过以下代码:

c1 = C()
c2 = C()
c1.a = 3
print(c1.a, c2.a) # 3 1
c1.b.append(1)
print(c1.b, c2.b) # [1] [1]
c2.b = [1]
c1.b.append(2)
print(c1.b, c2.b) # [1, 2] [1]

其他验证代码忽略。
以上解释了准备工作——创建用户类3. 如何创建用户类中为什么要把属性放在初始化函数内。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值