21.1 变量与对象的关系
Python中任何事物都是对象,包括变量、函数和类,都是对象。表达式a=‘s’中的字符串’s’就是对象。换句话说,变量a指向了对象’s’。变量就相当于一个标签,这个标签标识了一个对象。
每一个对象都是由三部分组成:标识符(identity)、类型(type)、值(value)组成。
标识符:每个对象都有唯一的标识,来标识自己,可以使用内置函数id()来查看对象的标识。例如查看变量a的标识,标识可以简单的认为这个值是该对象的内存地址:
>>> id(a)
4310511984
类型:标识对象存储的数据的类型,数据类型限制了对象的取值范围和可以执行的操作。可以使用内置函数type()来查看对象类型。例如查看变量a所指向对象的类型,从输出看它是个str类型的对象,也就是字符串类型:
>>> type(a)
<class 'str'>
值:表示对象存储的数据信息。使用print()函数可以打印出来。
>>> print(a)
's'
a='s’在内存的中的表示,可以用这个图来表示:
当执行b=a后,b也将指向a指向的那个对象,可以这样表示:
21.2 对象比较
21.2.1 '=='操作符比较值
执行a == b相当于是去执行a.__eq__(b)
,而 Python 大部分的数据类型都会去重载__eq__
这个函数,其内部的处理通常会复杂一些。比如,对于列表,__eq__
函数会去遍历列表中的元素,比较它们的顺序和值是否相等。元组也一样。
t1 = (1, 2, [3, 4])
t2 = (1, 2, [3, 4])
print(t1 == t2)
t1[-1].append(5)
print(t1 == t2)
21.2.2 'is’操作符比较身份
它们是否是同一个对象,是否指向同一个内存地址。
下面有两个类,一个是Person类,另一个是单例模式的MyClass类。通过输出可以看到s1和s2是同一个对象,因为对象的id都是4448078200。虽然b和c两个对象的属性值都一样,但是他们不是同一个对象,因为他们的对象id不同,一个是4448078256,另一个是4448078312。
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
class Singleton(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
class MyClass(Singleton):
pass
if __name__ == '__main__':
s1 = MyClass()
s2 = MyClass()
b = Person("Leon", 12)
c = Person("Leon", 12)
print(id(s1), id(s2)) # 相同的对象, 输出4448078200 4448078200
assert s1 == s2
print(id(b), id(c)) # 两个不同的对象, 输出4448078256、 4448078312
assert b == c # AssertionError
21.2.3 自定义对象比较规则
通过__eq__
可以自定义对象==操作符的比较规则。
比如下面这段代码,比较两个Person对象,如果他们的name和age的属性数值相等,则认为这两个对象==操作比较时就是True。
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, obj):
return self.name == obj.name and self.age == obj.age
if __name__ == '__main__':
a = Person("Leon", 23)
b = Person("Leon", 12)
c = Person("Leon", 12)
print(a, b, c) # 三个不同的对象
assert b == c # 这两个对象==操作符结果是False
assert a == b # 这两个对象的==操作符结果是True
21.3 对象拷贝
21.3.1 浅拷贝(shallow copy)
浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。创建浅拷贝对象有三种方式:
- 使用数据类型本身的构造器可以得到原对象的浅拷贝对象。
l2 就是 l1 的浅拷贝,s2 是 s1 的浅拷贝。
l1 = [[1, 2], (30, 40)]
l2 = list(l1)
print(l1 == l2) # True
print(l1 is l2) # 创建了一个新的对象 False
s1 = set([1, 2, 3])
s2 = set(s1)
- 使用列表切片方式得到原对象的浅拷贝对象。
l1 = [1, 2, 3]
l2 = l1[:]
print(l1 == l2) # True
print(l1 is l2) # 创建了一个新的对象 # False
- 函数 copy.copy()进行浅拷贝
import copy
l1 = [1, 2, 3]
l2 = copy.copy(l1)
如果原对象中的元素是可变的,改变原对象也会影响拷贝后的对象。比如:
l1 = [[1, 2], (30, 40)]
l2 = list(l1)
l1.append(100) # 原对象增加一个元素100,不会影响浅拷贝对象l2
l1[0].append(3) # 影响l2
print(l1) # 输出 [[1, 2, 3], (30, 40), 100]
print(l2) # 输出 [[1, 2, 3], (30, 40)]
l1[1] += (50, 60) # 通过+号,l1第二个元素新建了一个列表
print(l1) # 输出 [[1, 2, 3], (30, 40, 50, 60), 100]
print(l2) # 输出 [[1, 2, 3], (30, 40)] ,没受l1变化的影响
因为,浅拷贝对象里面的元素是原对象中子对象的引用。所以原对象(l1)里面可变元素的改变,影响到了l2。
l1.append(100),表示对 l1 的列表新增元素 100。这个操作不会对 l2 产生任何影响,因为 l2 和 l1 作为整体是两个不同的对象,并不共享内存地址。
l1的第一个元素[1,2]增加了一个元素3,则l2的第一个元素也变成了[1, 2, 3]。l1的第二个元素是元组,这个元组增加了两个元素(50, 60),因为元组是不可变的,这里表示对 l1 中的第二个元组拼接,然后重新创建了一个新元组作为 l1 中的第二个元素,而 l2 中没有引用新元组,因此 l2 并不受影响。
21.3.2 深拷贝(deep copy)
深拷贝则会递归地拷贝原对象中的每一个子对象。因此,深拷贝后的对象和原对象互不相关。
deepcopied = copy.deepcopy(origin)得到深拷贝对象。
import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1) # l1 和 l2 互不相干,各自改变不影响对方
l1.append(100)
l1[0].append(3)
l1
[[1, 2, 3], (30, 40), 100]
l2
[[1, 2], (30, 40)]
21.3.3 不是拷贝
元组的构造器和切片得到的对象与原对象指向同一个对象。不是拷贝,因为拷贝是得到不同的对象。
元组的构造器不是拷贝,只是多了一个引用,指向的是同一个对象
t1 = (1, 2, 3)
t2 = tuple(t1)
print(t1 == t2) # True
print(t1 is t2) # True 指向的是同一个对象
元组的切片不是拷贝,只是多了一个引用,指向的是同一个对象
t1 = (1, 2, 3)
t2 = t1[:]
print(t1 == t2) # True
print(t1 is t2) # True 指向的是同一个对象