流畅的Python(八)-对象引用、可变性和垃圾回收

一、核心要义

本章主要讨论对象对象名称之间的区别。名称不是对象,而是单独的东西。

二、代码示例

1、标识、相等性和别名

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 10:58
# @Author  : Maple
# @File    : 01-标识,相等性和别名.py
# @Software: PyCharm


p1 = {'name':'maple','gender':'male','age':32}
p2 = p1
p3 ={'name':'maple','gender':'male','age':32}


if __name__ == '__main__':
    # 1.p1所指向对象的标识(CPython中指对象的内存地址)
    print(id(p1)) # 1404688094144
    print('*******************************')

    # 2.p1和p2指向同一个对象,p2也被称作p1的别名
    print(id(p2))# 1404688094144
    print(id(p1) == id(p2)) # True
    print('*******************************')

    # 3.p3虽然和p1的值完全相同,但其实指向不同的对象
    # 值相等
    print(p1 == p3) # True
    # 但其实指向不同的对象
    print(id(p3)) # 1404688094208
    print(id(p3) == id(p1)) # False
    # 实际开发很少使用id方式判断是否 同一个对象,而是使用is
    print(p3 is p1) # False

2、元组的相对不可变性

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 11:05
# @Author  : Maple
# @File    : 02-元组的相对不可变性.py
# @Software: PyCharm

"""
元组的不可变性是指tuple数据结构的物理内容(即保存的引用)不可变,但如果引用 的某个可变对象本身发生了变化
,元组的值其实也会发生变化
"""

# 元组中的第三个元素[30,40]是可变对象-列表
t1 = (1,2,[30,40])
t2 = (1,2,[30,40])


if __name__ == '__main__':
    # 1. 最开始,t1和t2的值是相等的
    print(t1 == t2) # True

    # 2.修改t1中第三个元素的值
    ## 修改前列表的标识
    print(id(t1[-1])) # 2250527148480
    t1[-1].append(99)
    print(t1[-1]) # [30, 40, 99]
    ## 修改后列表的标识(与修改前相同,也就是引用本身 并不会发生变化)
    print(id(t1[-1])) # 2250527148480
    ## 但是引用 所对应的值很显然已经发生了变化
    print(t1[-1] == t2[-1]) # False

3、浅拷贝

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 11:21
# @Author  : Maple
# @File    : 03-浅拷贝.py
# @Software: PyCharm

"""
浅拷贝:
   副本中的元素是源容器中元素的引用
"""


l1 = [3,[66,55,44],(7,8,9)]
# l2是l1的浅拷贝
l2 = list(l1)

if __name__ == '__main__':
    #0. l1和l2是不同的对象
    print(l1 is l2 ) # False
    # 但是l1和l2中各个元素的引用都是指向同一个对象
    for i in range(len(l1)):
        print(l1[i] is l2[i]) # True,True,True

    # 1. l1中追加100,对l2没有影响
    l1.append(100)
    print('l1:',l1,';l2:',l2) # l1: [3, [66, 55, 44], (7, 8, 9), 100] ;l2: [3, [66, 55, 44], (7, 8, 9)]

    #2. l1中第一个元素修改为30,对l2没有影响,因为对数值进行修改,会直接生成一个新的对象,
    l1[0] = 30
    print('l1:', l1, ';l2:', l2) # l1: [30, [66, 55, 44], (7, 8, 9), 100] ;l2: [3, [66, 55, 44], (7, 8, 9)]

    #3. l1第二个元素移除55,l2会同步移除
    l1[1].remove(55)
    print('l1:', l1, ';l2:', l2) # l1: [30, [66, 44], (7, 8, 9), 100] ;l2: [3, [66, 44], (7, 8, 9)]

    #4. l2中第二个元素增加[33,22],因为list是可变对象,+=操作会就地改变对象本身(可参考第二章`拼接和增量操作`部分)
       #因此l1中的第二个也会同步发生变化
    l2[1] += [33,22]
    print('l1:', l1, ';l2:', l2) #l1: [30, [66, 44, 33, 22], (7, 8, 9), 100] ;l2: [3, [66, 44, 33, 22], (7, 8, 9)]

    #5. l2中第三个元素增加(10,11),因为元组是不可变对象,+=操作会生成一个新的对象(可参考第二章`拼接和增量操作`部分)
       #因此不会影响l1中的对应值
    l2[2] +=(10,11)
    print('l1:', l1, ';l2:', l2) #l1: [30, [66, 44, 33, 22], (7, 8, 9), 100] ;l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

补充:l1和l2的内存图:

(1)最初状态

流畅的Python(八)-对象引用、可变性和垃圾回收_第1张图片

(2)最终状态

流畅的Python(八)-对象引用、可变性和垃圾回收_第2张图片

4、深拷贝

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 11:44
# @Author  : Maple
# @File    : 04-深拷贝.py
# @Software: PyCharm

import copy

class Bus:
    def __init__(self,passengers = None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = passengers

    def pick(self,name):
        self.passengers.append(name)

    def drop(self,name):
        self.passengers.remove(name)

if __name__ == '__main__':
    bus1 = Bus(['Maple','Alice','Carry'])
    # bus1的浅拷贝
    bus2 = copy.copy(bus1)

    #bus1的深拷贝
    bus3 = copy.deepcopy(bus1)

    # 首先bus1,bus2和bus3是不同的对象
    print(id(bus1),',',id(bus2),',',id(bus3)) # 1973781178160 , 1973781642592 , 1973781754688

    # 但是由于bus2是bus1的浅拷贝,所以bus1.passengers和bus2.passengers指向同一个对象,而bus3.passengers则指向另外一个对象
    print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)) # 1973781759040 1973781759040 1973781758912

    # 从bus1.passengers中移除Maple,bus1.passengers也会发生相应变化
    bus1.drop('Maple')
    print(bus2.passengers) # ['Alice', 'Carry']

    # bus3.passengers 不受影响
    print(bus3.passengers) # ['Maple', 'Alice', 'Carry']

5、函数参数引用

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 13:08
# @Author  : Maple
# @File    : 05-函数参数引用.py
# @Software: PyCharm

l1 = [3,[66,55,44],(7,8,9)]


def f(a):
    """函数形参是实参的别名,即两者指向同一个对象"""
    return id(a)

def f2(a,b):
    # 形参a是实参的一个副本
    print('局部变量a运算前:',id(a))
    a +=b
    print('局部变量a运算后:',id(a))
    return a





if __name__ == '__main__':

    # 1. 形参和实参指向同一个对象
    print(id(l1)) #1596413019840
    print(f(l1)) #1596413019840

    print('************************')


    # 2. 数值类型测试
    a1 = 1
    b1 = 2
    print('全局变量a:',id(a1)) #  140724639640480
    """ 局部变量a运算前: 140724639640480
        局部变量a运算后: 140724639640544
        形参a返回值: 3
    """
    print('形参a返回值:',f2(a1,b1)) # 3: 形参a指向一个新的对象(对于数值类型,+运算会生成一个新的对象)
    print(a1) # 1:实参a1并不会发生变化

    print('*******************************')


    # 3.列表类型测试
    a2 = [1,2,3]
    b2 = [4]
    print('全局变量a:', id(a2))  # 2096208446016
    """ 局部变量a运算前: 2096208446016
        局部变量a运算后: 2096208446016
        形参a返回值: [1, 2, 3, 4]
    """
    print('形参a返回值:', f2(a2, b2))  # 3: 形参a就地修改原对象(对于列表类型,+运算会就地修改对象本身)
    print(a2)  # [1, 2, 3, 4]:实参a2也会同步发生变化
    print('*******************************')

    # 4.元组测试
    a3 = (1, 2, 3)
    b3 = (4,)
    print('全局变量a:', id(a3))  # 3147866564096
    """ 局部变量a运算前: 3147866564096
        局部变量a运算后: 3147866469536
        形参a返回值: (1, 2, 3, 4)
    """
    print('形参a返回值:', f2(a3, b3))  # 3: 形参a指向一个新的对象(对于元组类型,+运算会生成一个新的对象)
    print(a3)  # (1, 2, 3):实参a3并不会发生变化

6、使用不可变对象作为函数参数默认值

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 14:35
# @Author  : Maple
# @File    : 06-使用不可变对象作为函数参数默认值.py
# @Software: PyCharm

"""
函数参数的默认值应该尽量选择不可变类型,如果使用可变类型(比如list)可能会存在一些隐患
"""


class Bus:
    # passengers默认值使用可变类型的list
    def __init__(self,passengers = []):
         # self.passengers 是passengers的一个别名,即两者是同一个对象
         # 同时passengers又是空列表的一个别名
         # 而空列表作为默认值,会在函数定义时加载的,也就是默认值会变成函数对象的一个属性
         self.passengers = passengers

    def pick(self,name):
        self.passengers.append(name)

    def drop(self,name):
        self.passengers.remove(name)


if __name__ == '__main__':
    # 查看对象的默认属性:此时还是默认属性值还不包括Maple
    print(Bus.__init__.__defaults__)

    bus1 = Bus()
    bus1.pick('Maple')

    # 再次查看对象的默认属性,因为函数又一次加载了,同时在bus1.pick中已经添加了一个元素,所以默认属性值中会有Maple
    print(Bus.__init__.__defaults__)

    bus2 = Bus()
    # bus2的passengers竟然有值,明明初始化是空,原因分析
    # 1. bus1创建时,self.passengers指向 空列表对象,同时往该对象添加元素'Maple'
    # 2. bus2创建时,self.passengers指向 同一个`空列表`对象,只不过此时对象(默认属性)里已经有了值:Maple
    print(bus2.passengers) # ['Maple']

7、防御可变参数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 14:53
# @Author  : Maple
# @File    : 07-防御可变参数.py
# @Software: PyCharm

class TwilightBus:
    """篮球队成员从 bus下车后,竟然同时从篮球队消失,不符合常理"""
    def __init__(self,basketball_team):
        if basketball_team is None:
            self.basketball_team = []
        else:
            self.basketball_team = basketball_team

    def pick(self,member):
        self.basketball_team.append(member)

    def drop(self,member):
        self.basketball_team.remove(member)



class TwilightBus2:
    def __init__(self,basketball_team):
        if basketball_team is None:
            self.basketball_team = []
        else:
            # 创建basketball_team的副本(浅拷贝) 赋值给self.basketball_team
            self.basketball_team = list(basketball_team)

    def pick(self,member):
        self.basketball_team.append(member)

    def drop(self,member):
        self.basketball_team.remove(member)


if __name__ == '__main__':

    #1. TwilightBus测试
    basketball_team = ['Maple','Kelly','Jack']
    bus = TwilightBus(basketball_team)
    bus.drop('Maple')
    # Maple从bus下车后,连带从basketball_team中消息了,为何?
    # 1. self.basketball_team和 basketball_teams 指向同一个对象(互为别名),
    #     因此针对 self.basketball_team的操作,会影响basketball_teams的值
    # 解决方式是创建 basketball_teams的副本,让针对self.basketball_team的操作不会影响到basketball_teams
    print(basketball_team) # ['Kelly', 'Jack']

    #2. TwilightBus2测试
    basketball_team2 = ['Maple', 'Kelly', 'Jack']
    bus2 = TwilightBus2(basketball_team2)
    bus2.drop('Maple')
    print(basketball_team2)  # ['Maple', 'Kelly', 'Jack']

八、del和弱引用

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/8 15:06
# @Author  : Maple
# @File    : 08-del和弱引用.py
# @Software: PyCharm

"""
1.del删除变量名称而已,并不会直接删除对象
2.当对象的引用计数变为0时,对象立即就被销毁
"""

import weakref

s1 = {1,2,3}
s2 = s1
def bye():
    print('随风而逝....')

a_set = {0,1}


if __name__ == '__main__':

    ender = weakref.finalize(s1,bye)
    print(ender.alive) # True

    # 虽然删除了s1,但{1,2,3}仍然还剩下s2这个引用
    del s1
    # 所以ender仍然alive
    print(ender.alive) # True

    # s2指向一个新的对象,{1,2,3}的引用计数变为0
    s2 = 'spam'
    # 因此ender.alive变为False
    print(ender.alive) # 随风而逝..../False

    # 遗留一个问题,按理说函数形参 也会指向{1,2,3},所以当s1和s2被删除后,{1,2,3}应该还剩一个引用
    # 但因为 形参是对{1,2,3}的弱引用,并不会占用引用计数
    """控制台会话:Python控制台会自动把_变量绑定到结果不为None的表达式结果上
    >>>import weakref
    >>>a_set = {0,1}
     # wref即是对a_set的弱引用
    >>>wref = weakref.ref(a_set)
    # 返回弱引用对象的值:即{0,1},此时系统会自动分配一个变量_指向{0,1}
    >>>wref()
    {0, 1}
     # a_set重新指向一个新的对象(注意此时仍然有一个变量_ 指向{0,1},所以{0,1}并不会被销毁)
    >>>a_set = {1}
      # 仍然是{0,1}
    >>>wref()
    {0, 1}
    # 执行完,系统自动分配的变量指向False,因此没有变量指向{0,1},该对象会被销毁,之后wref()也就为None了
    >>>wref() is None
    False
    >>>wref() is None
    True
    """

你可能感兴趣的:(Python,python,开发语言)