Python进阶 -- 变量、深拷贝和浅拷贝

Python中的变量

Python中变量并不是我们常说的一个“箱子”,而是在对象上贴的“标签”,因为是标签,所以多个变量可以指向同一个箱子。

可以看下面的例子:

x = ('a', 'b')
y = ('a', 'b')

print(x is y) # True
print(id(x[-1])) # 4507066072
print(id(y[-1])) # 4507066072

在Cpython中,id可以返回一个对象的存储地址,我们可以看到,对于元组和字面常量这样的不可变对象,python为了节省开销,是将标签x,y贴在了同一个存储空间上,而不是为x和y分别建立“箱子”,再把元素放进去。

那么对于可变元素呢?

x = ['a', 'b', [1, 2]]
y = ['a', 'b', [1, 2]]
print(x is y) # False
print(id(x[1])) # 4507066072
print(id(y[1])) # 4507066072
print(id(x[-1])) # 
print(id(y[-1])) # 

我们可以用http://www.pythontutor.com/visualize.html#mode=display 这个网站来可视化python的执行,

对于上面的例子:


python中变量的存储.png

可以看到我们在建立对象x和y时贴标签的过程。

浅拷贝

浅拷贝就是只复制对象及其包含的引用,但是对对象内嵌套的对象不进行操作。

刚才看到用=将旧对象赋值给新变量的时候,其实只是将新变量指向了旧对象,而并没有对对象进行拷贝。要对对象进行拷贝,最简单的办法是用该对象的constructor:

l1 = [3, [2, 1], [4, 5]]
l2 = list(l1) # 用constructor建立拷贝
print(l1 == l2) # True,值相等
print(l1 is l2) # False,地址不同

在用constructor对对象进行拷贝时,为了节省内存,默认进行浅拷贝。出了constructor以外,对可遍历的对象用[:]也会创造拷贝,如l3 = l1[:]的效果等同于上面例子的第二条语句。

我们可以用下面的代码为例,可视化的看看浅拷贝的过程:

x = ['a', 'b', [1, 2], (3, 4)]
y = list(x)

对应的内存模型:


python中的浅拷贝.png

如果拷贝的对象中都包含可变对象,那么浅拷贝可能会造成一些问题:对一个变量的修改会造成另一个指向相同对象的变量值变化。

深拷贝

copy模块中的deepcopy()可以创造一个深拷贝。深拷贝不但会拷贝对象,对对象内嵌套的对象也会进行拷贝,而非创建引用。

看下面的例子:

from copy import copy, deepcopy


class SomeClass(object):
    def __init__(self, inner_lst=[]):  # 用可变对象作为默认参数非常不好,这里只是示例,实际代码中不要这样操作
        self.lst = inner_lst


if __name__ == '__main__':
    sc = SomeClass()
    sc_shallow_copy = copy(sc)  # 创建一个浅拷贝
    sc_deep_copy = deepcopy(sc)  # 创建一个深拷贝
    # 打印三个对象的地址
    print(id(sc), id(sc_shallow_copy), id(sc_deep_copy))  # 4404379720 4404380056 4405153976
    # 打印三个对象中嵌套对象的地址
    print(id(sc.lst), id(sc_shallow_copy.lst), id(sc_deep_copy.lst))  # 4405032392 4405032392 4404061192

可以看到,三个对象的内存地址都是不同的,可见并非引用,而是各自建立了一份拷贝;但是对于对象之中嵌套的list对象,浅拷贝只拷贝了引用,而深拷贝则为其也创建了拷贝,这就是深浅拷贝的主要区别所在。

拷贝中的例外

在Cpython解释器中,为了减少内存的消耗,对于不可变类型的拷贝实际上会采用引用的形式,也就是在原先的内存内容上,贴上一个新的标签,而不会真的在一块新内存中建立一份拷贝。可以看下面的例子:

if __name__ == '__main__':
    some_lst = [1, 2, []]
    some_lst_cpy = list(some_lst)  # 创建一个浅拷贝
    some_lst_cpy2 = some_lst[:]  # 创建一个浅拷贝
    print(some_lst is some_lst_cpy)  # False
    print(some_lst is some_lst_cpy2)  # False

    some_tuple = (1, 2, [])
    some_tuple_cpy = tuple(some_tuple)  # 试图创建一个浅拷贝,但是实际上只会创建一个reference
    some_tuple_cpy2 = some_tuple[:]  # 试图创建一个浅拷贝,但是实际上只会创建一个reference
    print(some_tuple is some_tuple_cpy)  # True
    print(some_tuple is some_tuple_cpy2)  # True

可以看到,对于可变类型list,解释器真的创建了一份拷贝;但是对于不可变类型tuple,解释器只是建立了一个引用(尽管这里的不可变类型并非绝对不可变的,对tuple中的list进行修改,仍然会改变这个tuple的值)。同样的情况会发生在对字符串字面量,数字字面量(数字较小时),frozenset进行拷贝时。

这是Cpython解释器的一个优化,叫做内化(interning),但是并非对所有的字符串字面量和数字字面量都是如此,到底对符合哪些标准的字符串和数字进行内化的具体实施细节尚不清楚。

函数参数传递中的浅拷贝

在C++中,参数传递有几种经典的方式:pass by reference, pass by value, pass by address。但是在python中,所有参数传递只有一种方式:pass by sharing。其实也就是只有引用传递,所有传递入函数的形参都是实参的别名(引用)。

因此在传递参数时,需要特别注意将可变对象作为参数传递,因为函数中是可能改变这些变量的值的,因此在写代码之前我们要充分思考我们的意图是否是要改变这些可变对象的值,否则就可能产生意料之外的后果。

class Names(object):
    """ 一个公司花名册 """

    def __init__(self, inner_lst=None):
        if inner_lst is None:
            self.lst = []
        else:
            self.lst = inner_lst


if __name__ == '__main__':
    some_people = ["Xiao Zhang", "Xiao Wang", "Lao Li"]  # 公司老员工
    n = Names(some_people)
    n.lst.append("Lao Xiao")  # 新加入员工
    print(some_people)  # ['Xiao Zhang', 'Xiao Wang', 'Lao Li', 'Lao Xiao']

由于前面说的引用机制,Names类在进行对象初始化时,实际上会将self.lst作为一个引用,指向inner_lst,因此在对Names类对象的lst进行操作时,操作也会影响到inner_lst。这会造成一些问题:例如我们要区分公司老员工和今年新加入员工,在用上面这个花名册操作之后,我们发现这个新加入员工同样也被加入了老员工的列表,这和我们的预期是不符合的。

要解决上面这个问题很简单,了解了引用和拷贝的区别之后,我们只需要进行一个拷贝,就可以防止发生这样的问题:

class Names(object):
    """ 一个公司花名册 """

    def __init__(self, inner_lst=None):
        if inner_lst is None:
            self.lst = []
        else:
            self.lst = list(inner_lst)  # 用constructor创建一个拷贝


if __name__ == '__main__':
    some_people = ["Xiao Zhang", "Xiao Wang", "Lao Li"]  # 公司老员工
    n = Names(some_people)
    n.lst.append("Lao Xiao")  # 新加入员工
    print(some_people)  # ['Xiao Zhang', 'Xiao Wang', 'Lao Li']

Python中对象的生存周期

Python中的对象创建

前面已经讲过Python中的变量是类似于“贴标签”的过程,那么是先创建了一个对象,再将后创建的标签贴在它身上呢,还是先创建了标签,再将标签贴在后创建的对象上呢?也就是说,变量名和对象的创建,孰先孰后?

看下面的例子:

class SomeClass(object):
    def __init__(self):
        print("Memory address of current object: ", id(self))


if __name__ == '__main__':
    x = SomeClass()  # Memory address of current object:  4559627936
    y = SomeClass() + 5  # Memory address of current object:  4559999048
    # 会raise一个error,因为没有定义SomeClass类的"__add__"方法、

从这例子可以看到,是先创建了一个对象,后创建变量(也就是“标签”),再将标签贴在这个变量上的。

Python中的垃圾回收

程序自然不能无限制使用内存,事实上,程序使用的内存越少,后续操作能够顺利执行的可能性就越大。这就是说当程序不再需要某个对象时,需要有一种机制将这些不用的对象所占据的内存进行释放,这个机制就叫做垃圾回收。

CPython中最主要的垃圾回收机制就是引用计数(reference counting)。每个对象都有一个计数器,统计有多少个指向它的引用。当这个引用数清零之后,CPython会唤起对象的__del__方法来删除对象并释放该对象占用的内存。在CPython2.0中,垃圾回收机制会检测循环引用(也就是a指向b,b指向c,c指向a的这类情况),将这组只有循环引用的对象也进行垃圾回收。

为了验证,可以看下面例子:

import weakref


def send_message():
    print("Object destroyed!")


if __name__ == '__main__':
    x = {1, 2, 3}
    status = weakref.finalize(x, send_message)  # 类似于一个装饰器,在对象被回收时运行send_message程序
    y = x
    del x  # 删除引用x
    # 检查对象{1, 2, 3}是否仍然存在
    print(status.alive)  # True
    del y
    print(status.alive)  # Object destroyed! False

weakref会创建一个弱引用,即不累加对象中引用计数器的引用。在开始时我们的对象{1, 2, 3}有两个引用x,y指向它,在删除了x之后,对象仍然存在;但是在y被删除后,不再有指向该对象的引用,因此对象被回收。

从这个例子中我们还可以发现,del删除的是“标签”,也就是引用,而不是对象。

你可能感兴趣的:(Python进阶 -- 变量、深拷贝和浅拷贝)