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’在内存的中的表示,可以用这个图来表示:

python 变量的id与对象的id python变量和对象的区别_浅拷贝

当执行b=a后,b也将指向a指向的那个对象,可以这样表示:

python 变量的id与对象的id python变量和对象的区别_python 变量的id与对象的id_02

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)

浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。创建浅拷贝对象有三种方式:

  1. 使用数据类型本身的构造器可以得到原对象的浅拷贝对象。
    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)
  1. 使用列表切片方式得到原对象的浅拷贝对象。
l1 = [1, 2, 3]
l2 = l1[:]

print(l1 == l2) # True

print(l1 is l2)  # 创建了一个新的对象 # False
  1. 函数 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  指向的是同一个对象