美文网首页
【解决您的困扰】Python变量本质与内存分析

【解决您的困扰】Python变量本质与内存分析

作者: Python野路子 | 来源:发表于2020-02-22 22:39 被阅读0次

    Python不像C/C++、Java等语言一样,可以不用先声明变量类型而直接对变量进行赋值。对Python语言来讲,对象的类型和内存都是在运行时确定的,它是动态类型语言。

    在python中一切皆为对象,当程序运行时,所有的变量和数据都是存储在内存里不同的地方,我们可以将内存想象为由多个紧挨着的房间组成的小区,每个房间都有门牌号(即内存地址,内存中每个存储单元的编号)。

    image

    我们来看下常见例子:

    例子1

    a = 257,我们创建一个变量,并且将数字257赋值给变量a。当有对象被定义时,python将对象的数值放入新的房间,并分配一个门牌号,这样便完成了对象的定义。

    image

    例子2

    >>> a = 300
    >>> b = a
    >>> a
    300
    >>> id(a)
    2196454328336
    >>> b
    300
    >>> id(b)
    2196454328336
    >>> b = b + 1
    >>> b
    301
    >>> id(b)
    2196484657072
    >>> id(a)
    2196454328336
    
    
    image

    例子3

    我们再来看个与例2类似的代码:

    >>> a = 3
    >>> b = a
    >>> a is b
    True
    >>> b = b + 1
    >>> a is b
    False
    >>> b = b - 1
    >>> a is b
    True
    
    

    我们可以看到b = b + 1之后,我们通过is判断a和b指向的不是同一个地址。后面b = b - 1得到值为3,通过判断a和b指向的是同一个地址,这是为啥呢?

    解释:这个和python内存机制有关,为了避免频繁的申请和销毁内存空间,浪费性能。Python对小整数使用了对象池技术,这个小整数范围[-5, 256],这些整数对象提前创建好的,并放入内存固定位置,不会被当作垃圾回收,所以程序中任何语句用到这范围内的整数,都会直接找到上述地址,使用这些已经创建好的对象,对于其他整数,使用赋值语句都会开辟新的内存空间创建新的一个对象。

    image

    例子4

    同样对于字符串,Python也有类似的机制。

    >>> s1 = 'qmpython'  # 如果之前没有出现过'qmpython'就在内存中新建一个字符串对象,内容为'qmpython'。
    >>> s2 = 'qmpython'
    >>> s1 is s2
    True
    >>> s3 = 'qm'+'python' # 得到结果也是'qmpython',而之前给s1赋值时已经创建了一个'qmpython',所以直接让s3指向该地址,与s1相同。
    >>> s2 is s3
    True
    
    >>> s4 = '&qmpython'
    >>> s5 = '&qmpython'
    >>> s4 is s5 # 由于存在特殊字符,所以尽管两者内容相同,但是Python还是为其各自创建了一个对象,分别占用一块内存,id不同。
    False
    
    

    同样在Python中为了提高字符串的使用效率,提供了intern(字符串驻留)技术,intern机制是指同样的字符串对象只保存一份,该字符串会放到缓存池中,是共用的。如果程序中用到了一个字符串,Python会首先检查内存中是否已经存在相同内容的字符串对象,如果存在,则直接使用该字符串的地址,而不会创建新的字符串对象,从而提高效率。

    注意:一旦字符串中包含特殊符号(包括空格、#、!、中文等),上述规则即失效,此时即使已经存在相同内容的字符串,Python也会创建新字符串对象。

    需要注意:只包含单个字符的字符串也是共用的,会一直驻留在内存中,如:

    >>> s5 = '#'
    >>> s6 = '#'
    >>> s5 is s6
    True
    
    

    例子5

    >>> a = [300, 5, 400]
    >>> id(a)
    2018867765960
    >>> a[1]
    5
    >>> a[1] = 'abc'
    >>> a
    [300, 'abc', 400]
    >>> b = a
    >>> id(b)
    2018867765960
    >>> b.extend([500,600]) # extend时对列表b直接扩展,再加入新元素。extend并没有创建新列表,只是在其他列表上扩充。
    >>> id(b)
    2018867765960
    >>> id(a)   # a,b指向同一个内存地址。
    2018867765960
    
    
    image

    如果是列表加法b = b + [3,4]则是创建一个新列表,此时b不再和a指向同一个地址了,b指向新创建的列表。

    整数、浮点数、逻辑、字符串、元组等类型的数据,一旦创建出来,其在内存中的内容就不可再修改。即是不可变类型。

    image

    我们之前学的元组(tuple),尽管可以调用引用元素,元组中的这些地址号码禁止修改,所以创建后不能再指向其他地方,即不可以赋值,因此不能改变对象自身,所以也算是不可变对象。

    >>> a = (1,2,3,[4,5,6],7)
    >>> a
    (1, 2, 3, [4, 5, 6], 7)
    >>> a[3]=8
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'tuple' object does not support item assignment
    >>> a[3][2] = 5  # 改变元组中元素引用的对象
    >>> a
    (1, 2, 3, [4, 5, 5], 7)
    
    

    所谓“元组不可修改”,是指其内部存储的地址号码不可修改,至于这些地址所指向的内存中是否被修改,与元组无关。

    列表内存中的数值可以修改,以便指向其他数据的内存。 image

    列表、集合、字典等类型的数据,其在内存中的内容可以随时修改,扩充。即可变对象。

    v = [1, 2, 3]
    values = [11, 22, v]
    v = 999
    print(values)
    
    

    如果不运行,因为后面v重新赋值为999,所以误以为结果是[11, 22, [999],其实是错误的。最终结果运行为:[11, 22, [1, 2, 3]]

    image

    例子6

    l = [1,2,3]
    for item in l:
        item = 8
        # 这里只是让item指向l[i]所指向的对象,item = 8,则item指向新的对象8,不改变l[i]
    print(l) # [1,2,3]
    for i in range(len(l)):
        l[i] = 8  # l[i]指向新的对象 8
    print(l) # [8,8,8]
    
    

    例子7

    l1 = []
    a = 0
    for i in range(1,5):
        a = i
        l1.append(a) # 添加的是a指向的对象
    print(l1) # [1, 2, 3, 4]
    
    l2 = []
    b = [1,2,3]
    for i in range(1,5):  # 1~4
        b[1] = i  # 每次将列表b中第二个元素引用改变指向新的对象,循环到最后第二个元素指向对象4
        l2.append(b) # 添加的是b指向的对象,它包括列表元素的引用,列表本身没有改变,只是列表项[1]指向的对象变了
    print(l2) # [[1, 4, 3], [1, 4, 3], [1, 4, 3], [1, 4, 3]]
    # 不是预料的 [[1, 1, 3], [1, 2, 3], [1, 3, 3], [1, 4, 3]]
    
    

    注意:上面的例子,我们一般人思维就是每次都改变b第二个元素指向的值,然后再追加到l2列表中,这样到最后结果是 [[1, 1, 3], [1, 2, 3], [1, 3, 3], [1, 4, 3]],但是实际上却是错误的,l2添加的是引用b指向的对象,循环每次改变的是列表[1]指向的对象,b引用还是指向刚开始的。所以,每次列表实际上都是添加同一个对象。

    例子8

    使用可变类型对象(列表、字典、集合),作为函数参数的默认值时需要注意的问题!

    def add_num(li=[]):
        li.append(1)
        return li
    
    if __name__ == '__main__':
        x = add_num()
        print(x) # [1]
    
        y = add_num()
        print(y) # [1, 1]
    
        z = add_num()
        print(z) # [1, 1, 1]
    
    

    看到上面结果是不是匪夷所思呢,这是为什么呢?

    这是因为,如果函数的参数默认值可变对象(列表、集合、字典等),该默认值在程序运行之初的“编译阶段”就已经创建,并一直保留在内存中。所以每次调用函数,修改的都是同一默认值对象。

    所以上面程序,第一次调用li空值,然后添加1到空列表中,第二次调用使用的还是默认值,所以在第一次的内存上添加一个元素,依次类推。

    例子9

    >>> a = [1, 2, 3]
    >>> b = a
    >>> b
    [1, 2, 3]
    >>> b[0] = 0
    >>> b
    [0, 2, 3]
    >>> a
    [0, 2, 3]
    
    

    上面我们将a赋值给b,在改变a指向的对象元素,b也会相应改变,但是有时候我们只需要将一份数据的原始内容保留一份,再去处理而不影响之前的数据。python提供了copy模块处理这个情况,我们现在来学习深浅拷贝。 浅拷贝:copy.copy(),不管多复杂的数据结果,只会拷贝第一层。

    深拷贝:copy.deepcopy(),拷贝所有数据(可变类型)

    浅拷贝

    import copy
    
    a = 10000
    b = copy.copy(a)
    c = copy.deepcopy(a)
    print(id(a), id(b), id(c))
    print(a is b)
    print(c is b)
    
    ==》 
    2151125387376 2151125387376 2151125387376
    True
    True
    
    s1 = ' qmpython!@'
    s2 = copy.copy(s1)
    s3 = copy.deepcopy(s1)
    print(id(s1), id(s2), id(s3))
    print(s1 is s2)
    print(s1 is s3)
    
    ==》
    2151125800752 2151125800752 2151125800752
    True
    True
    
    

    从上面例子可以看出数值和字符串深浅拷贝之后都是指向同一个对象。

    import copy
    
    a = [1,2,3]
    b = copy.copy(a)
    c = copy.deepcopy(a)
    
    print(a == b)  # == 判断2对象值内容是否相同
    print(a == c)
    
    print(a is b)  # is 判断2个对象是否相同,身份标识和内容
    print(c is b)
    
    ==》
    True
    True
    False
    False
    
    

    上面a,b,c指向不同的对象,地址不一样,值相同,是因为进行了拷贝,创建新的对象,内容拷贝了a的一份。所以指向的地址不一样,但是内容是一样的。对于这么只有一层的,深浅拷贝也只有一层可拷贝,所以效果是一样的。

    a,b,c三个引用指向了不同的对象,所以任何一个引用指向的对象改变并不会影响另外的。 image

    上面只有一层的列表看不出深浅拷贝的区别,下面我来看看嵌套列表中深浅拷贝的区别

    import copy
    
    a = [1, 2, 3, [4, 5, 6]]
    b = copy.copy(a)
    c =  copy.deepcopy(a)
    
    print(a == b) 
    print(c == b)
    
    print(a)  
    print(b)
    print(c)
    
    print(a is b) 
    print(c is b)  # a,b,c地址不一样,值一样,但是c的可变类型已改变,指向了赋值的新的地址空间。
    
    a[3][0] = 9
    print(a)
    print(b)
    print(c)
    
    print(a == b) 
    print(a == c)
    
    ==》
    True
    True
    [1, 2, 3, [4, 5, 6]]
    [1, 2, 3, [4, 5, 6]]
    [1, 2, 3, [4, 5, 6]]
    False
    False
    [1, 2, 3, [9, 5, 6]]
    [1, 2, 3, [9, 5, 6]]
    [1, 2, 3, [4, 5, 6]]
    True
    False
    
    

    上面我们可以看出修改a引用中元素引用的重新指向,b对应的也改变,因为它只拷贝了第一层,而c没变,是因为拷贝了所有的层次。深拷贝会完全复制原变量相关的所有数据,在内存中生成一套完全一样的内容,在这个过程中我们对这两个变量中的一个进行任意修改都不会影响其他变量。

    image

    4.针对特殊情况-元组。

    import copy
    
    a = (1, 2, 3)
    b = copy.copy(a)
    c = copy.copy(a)
    
    print(a == b) 
    print(a == c)
    print(a is b) 
    print(a is c)
    
    ==》
    True
    True
    True
    True
    
    

    元组本身是不可变类型,就像str、int、boor一样,所以在深浅拷贝时,它们的内存地址是一样的。

    当元组中嵌套可变类型的时候:

    import copy
    
    a = (1, 2, 3, [1, 2, 3], 4)
    b = copy.copy(a)
    c = copy.deepcopy(a)
    
    print(a == b) 
    print(a == c)
    print(a is b) 
    print(a is c)
    
    a[3][0] = 6
    print('-'*10)
    print(a == b) 
    print(a == c)
    print(a is b) 
    print(a is c)
    
    print(a)
    print(b) 
    print(c)
    
    print(id(a))
    print(id(b)) 
    print(id(c))
    
    ==》
    True
    True
    True
    False
    ----------
    True
    False
    True
    False
    (1, 2, 3, [6, 2, 3], 4)
    (1, 2, 3, [6, 2, 3], 4)
    (1, 2, 3, [1, 2, 3], 4)
    2151124539704
    2151124539704
    2151125702088
    
    

    当元组中嵌套可变类型时,浅拷贝不会开辟空间,深拷贝会重新开辟一块内存空间。

    总结:

    1.数据为不可变类型时,浅和深拷贝的值一样,且指向同一内存地址。如前面说的数值,字符串,元组。

    2.数据为可变类型时:

    1. 没有嵌套的情况,深浅拷贝都是一样的,都拷贝第一层。
    2. 在嵌套的情况下,浅拷贝,拷贝第一层。深拷贝,拷贝第一层以及嵌套层次中的所有的。

    例子10

    列表以整数,就是将方括号里面的元素内容重复多次。

    >>> a = [0] * 3
    >>> a
    [0, 0, 0]
    >>> b = [a] * 2
    >>> b
    [[0, 0, 0], [0, 0, 0]]
    >>> b[0][1] = 666
    >>> b
    [[0, 666, 0], [0, 666, 0]]
    >>> a
    [0, 666, 0]
    
    

    上面我们将列表[0]3,得到一个元素值重复3次的列表并且赋值给a,然后[a] * 2就是[a, a],而变量a[0, 0, 0],所以[a] * 2也就是[[0, 0, 0], [0, 0, 0]]

    image

    然后修改b元素的值b[0][1] = 666,通过输出结果我们可以看出,b[1][1]a也被修改了。

    >>> b[1][1]
    666
    >>> b[1]
    [0, 666, 0]
    >>>
    
    
    image

    列表乘法执行的是拷贝,子列表都指向同一内存!因为[0]中没有子列表,所以[0]*3并不涉及列表乘法的浅拷贝问题,只有存在子列表(或字典等其他可变容器)时,才需要考虑浅拷贝问题。

    相关文章

      网友评论

          本文标题:【解决您的困扰】Python变量本质与内存分析

          本文链接:https://www.haomeiwen.com/subject/jlnrqhtx.html