• 【原创】面向对象作业:选课系统中用pickle储存多个对象间组合引用关系的那些坑


    转载请注明出处https://www.cnblogs.com/oceanicstar/p/9030121.html


    想直接看结论先提前列出:

      1、存储一个对象,文件不是真的给你存储的了对象这种东西,存储的都是一些代码而已。

      具体是哪些代码呢?

      想想看,我们保存对象的目的,是为了方便以后从文件里加载回来时,能让计算机自动帮你构建回之前的那个对象。那么文件里头会存储一些什么代码呢?

        ①要加载文件时,能够重构回之前的那个对象,至少能够实例化出这个对象的类的定义代码得存储到文件里头吧

        ②如果这个类继承了一些父类的东西,或者跟其他类有组合关系之类的blabla…那么这些类的定义代码也会储存到文件里头

        ③这个对象自己的属性和方法(在类定义之外自己定义的)得存储到文件里头吧

        总之,一切目的都是为了重构时找到所有必须的素材(各种类、函数、变量的定义代码,还有相互之间的实例化、引用等关系),就跟只有集齐七颗龙珠才能召唤神龙一样……

      2、我们将每个对象pickle到不同文件后再加载load回来时,pickle反序列化load加载回来都是重构了一个原来对象的副本,pickle文件里存储了构建出这些对象需要引用的类、方法、对象等引用关系。

      3、如果想要加载回来后的对象组合关系还能对应上的话,是不能把这多个对象分开dump到不同文件的,必要要同时dump到一个文件内。

      4、由于上述原因,实际应用中,我们要储存多个对象间的组合引用关系,往往需要使用字典/列表/元组等容器来盛放这些有组合关系的对象,然后将这个容器一次dump到一个文件中去。


    ——以下故事纯属虚构,如有雷同,怕是你转载我的吧!转载请注明出处,谢谢!

    大海:我X,这选课系统咋这么难写啊,写了我10几天,老是有bug出来,明明我保存了老师、学员和班级间的组合关系,怎么加载回来进行值的更改,就不会相互自动关联地改变值了呢?

    流星:要想讲明白pickle储存多个对象之间的组合关系问题,要先从一个面向对象的例子开始说起。。。

    大海:啥例子?

    流星:是一个简化了的例子,你看下面

     1 class A:
     2     def __init__(self, name):
     3         self.name = name
     4         self.b_list = []
     5 
     6 
     7 class B:
     8     def __init__(self, name):
     9         self.name = name
    10         self.a_list = []
    11 
    12 
    13 # 各实例化一个对象
    14 a1 = A('A类1号')
    15 b1 = B('B类1号')
    16 
    17 # 打印 2个实例各自的列表属性
    18 print('-------当前各自的列表属性-------')
    19 print('a1的b_list属性:', a1.b_list)
    20 print('b1的a_list属性:', b1.a_list)
    21 
    22 # 在实例b1的列表属性中建立组合关系
    23 b1.a_list.append(a1)
    24 
    25 # 打印对象组合关系
    26 print('
    -------当前的组合关系-------')
    27 print('a1的b_list属性:', a1.b_list)
    28 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    29 
    30 # pickle序列化保存到 2个文件里
    31 import pickle
    32 with open('a1.pk', 'wb') as f1:
    33     pickle.dump(a1, f1)
    34 with open('b1.pk', 'wb') as f2:
    35     pickle.dump(b1, f2)

    运行结果

    -------当前各自的列表属性-------
    a1的b_list属性: []
    b1的a_list属性: []
    
    -------当前的组合关系-------
    a1的b_list属性: []
    b1的a_list中的A对象的b_list属性: []

    大海:哈哈,我看懂了,b1的a_list列表属性添加了a1对象,建立了组合关系!

    流星:是的,而且我们还把a1对象和b1对象分别存到了文件里头

    大海:(满怀自信地)对!都用的是pickle序列化dump到文件,再load回来的话,他们组合关系肯定还是在的吧!

    流星:是吗?那么让我们来验证一下吧

     1 import pickle
     2 
     3 class A:
     4     def __init__(self, name):
     5         self.name = name
     6         self.b_list = []
     7 
     8 
     9 class B:
    10     def __init__(self, name):
    11         self.name = name
    12         self.a_list = []
    13 
    14 # 将a1和b1加载回来
    15 with open('a1.pk', 'rb') as f1:
    16     a1 = pickle.load(f1)
    17 with open('b1.pk', 'rb') as f2:
    18     b1 = pickle.load(f2)
    19 
    20 # 打印对象组合关系
    21 print('-------加载回来的组合关系-------')
    22 print('a1的b_list属性:', a1.b_list)
    23 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    24 
    25 # 再实例化一个b2
    26 b2 = B('B类2号')
    27 a1.b_list.append(b2)
    28 
    29 # 打印对象组合关系
    30 print('
    -------给a1列表属性添加b2后的组合关系-------')
    31 print('a1的b_list属性:', a1.b_list)
    32 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)

    运行结果

    -------加载回来的组合关系-------
    a1的b_list属性: []
    b1的a_list中的A对象的b_list属性: []
    
    -------给a1列表属性添加b2后的组合关系-------
    a1的b_list属性: [<__main__.B object at 0x00000000025C2A90>]
    b1的a_list中的A对象的b_list属性: []

    大海:咦?怎么回事?我们不是已经建立了b1和a1间的组合关系吗?那b1中的a_list里的A类对象就应该是a1啊?

       那么我们给a1对象的b_list属性列表添加上了对象b2(上面结果确实添加了一个B object对象),

       同样的b1中的a_list里的A类对象的b_list属性不也应该添加上对象b2了吗,为啥上面结果打印结果还是个空列表 [ ] 呢?

       额……不明白……

       到底现在加载回来的a1对象,跟b1中的a_list里的那个A类对象还是同一个么?

    流星:那我们打印a1对象和b1的a_list列表属性看看?

    1 # 打印 a1对象
    2 print('
    a1对象:')
    3 print(a1)
    4 
    5 # 打印 b1对象中a_list列表
    6 print('b1对象的a_list列表:')
    7 print(b1.a_list)

    运行结果

    a1对象:
    <__main__.A object at 0x00000000025F24E0>
    
    b1对象的a_list列表:
    [<__main__.A object at 0x000000000367D278>]

    大海:#%&*#@()&%#@,X!……果然不是同一个对象了!

       怎么搞的,我们不是把a1和b1这2个对象都pickle了吗?组合关系怎么乱了呢?

    流星:稍等片刻,答案即将揭晓。。。来条华丽的分割线吧


    大海:这分割线一点都不华丽啊!

    流星:……或许等我学完前端就华丽了吧

    大海:……

    流星:其实,这里首先要理解的是我们将每个对象pickle到不同文件后再加载load回来时,每个对象被恢复到一个与原来对象值相等的对象,但本质上不是同一个对象,而是重构了个新的对象。

       换句话说,每次pickle反序列化load加载回来都是原来对象的一个副本,那么我们从把两个有组合关系的对象a1和b1分别用pickle序列化dump到两个文件里头就是不对的,这样加载回来的时候,a1和b1对象的属性也都是在各自文件load加载过程中独立复制生成的

       具体来解释,就是:

       a1对象的b_list属性,在a1.pk文件load加载回来的过程,可以理解为:a1对象加载回来时,计算机开辟一个内存空间放a1对象,发现a1里头有个b_list属性啊,值是空列表[ ],好的,那么给他开辟一个内存空间放这个空列表属性吧

       b1对象的a_list属性里头有值,并且是个A类的实例对象(在保存到文件之前的程序里是a1),而在b1.pk文件load加载回来的过程中python重构了一个与A类的实例对象(但不是现在的a1了)间的组合关系,可以理解为:b1对象加载回来时,python要重构一个b1对象,发现b1里头有个a_list属性啊,值是个列表,里头居然还装了个A类的实例对象,但是这文件里头没有说明这个A类的实例对象是谁呀,只告诉我了有个b1对象要返回,这个A类的实例对象返回给谁呢?算了,给b1开辟一个内存空间放这个列表属性吧,并且去构造一个A类的新的实例对象,这样至少保留了b1的属性值不变吧,嗯!就这么干!

       好了,这下a1对象和b1对象都各自加载完成了,但是这样计算机并没有把b1对象的a_list属性中的A类实例对象当成是a1来关联。。。

    大海:那么,怎么才能在pickle序列化保存后,a1与b1间的组合关系还能加载回来呢?

    流星:再来条华丽的……

    大海:……


    流星:其实,上面的pickle保存有个关键问题是,有组合关系的多个对象在pickle序列化保存到文件时,如果想要加载回来后的对象组合关系还能对应上的话,是不能把这多个对象分开dump到不同文件的!

       这的原因就像上面解释的一样

    大海:前面说的太啰嗦,我听不懂啊!

    流星:……

    大海:能简单用人话解释下可以吗?

    流星:好吧,作为神的我就尽量……

    大海:……

    流星:其实,当a1和b1对象分开dump到不同的文件时,加载回来是分开独立加载的,因为pickle反序列化重构对象间的关系是在load方法执行时一次性加载回来生成的,所以在load加载回b1对象时(也就是运行b1 = pickle.load(f2)时),就独立地把b1对象的关系建立好了,即b1的a_list属性里头有组合关系需要关联的对象的那个A类的实例对象占用的内存地址也分配好了,这个过程是与运行 a1 = pickle.load(f1)相互独立的,毫无关系,所以load加载回a1对象的内存地址也是另外独立分配的,也就是说,现在加载回来的b1与a1对象已经没有组合关系了,跟b1有组合关系的是在运行b1 = pickle.load(f2)时,计算机在内存里自动生成的一个A类的实例对象,这个A类的实例对象被放在了b1的a_list属性列表里头。

    流星:这下明白了吧……?

    大海:好像有点点明白了……可是,那我该怎么做才能在文件里加载回a1与b1组合关系呢?

    流星:那就再来一条华……

    大海:别来了,算我求你了好嘛?

    流星:好吧!那就最后再来一条吧!

    大海:……


    流星:答案是——把a1与b1dump到同一个文件里头!

    大海:哦,我知道,pickle是可以多次dump到一个文件的!我们可以先把a1对象dump到文件里,然后再把b1对象也dump到同一个文件里头呗,然后load回来的时候,load两次对吧,我聪明吧?哈哈~

    流星:拉倒吧,你去试试看,这样能还原回来我们要的组合关系吗?

    大海:肯定行,你等着……这就运行给你看!

     1 class A:
     2     def __init__(self, name):
     3         self.name = name
     4         self.b_list = []
     5 
     6 
     7 class B:
     8     def __init__(self, name):
     9         self.name = name
    10         self.a_list = []
    11 
    12 
    13 # 各实例化一个对象
    14 a1 = A('A类1号')
    15 b1 = B('B类1号')
    16 
    17 # 打印 2个实例各自的列表属性
    18 print('-------当前各自的列表属性-------')
    19 print('a1的b_list属性:', a1.b_list)
    20 print('b1的a_list属性:', b1.a_list)
    21 
    22 # 在实例b1的列表属性中建立组合关系
    23 b1.a_list.append(a1)
    24 
    25 # 打印对象组合关系
    26 print('
    -------当前的组合关系-------')
    27 print('a1的b_list属性:', a1.b_list)
    28 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    29 
    30 # pickle序列化分两次dump到 1个文件里
    31 import pickle
    32 with open('a1b1.pk', 'wb') as f:
    33     pickle.dump(a1, f)
    34     pickle.dump(b1, f)

    运行结果

    -------当前各自的列表属性-------
    a1的b_list属性: []
    b1的a_list属性: []
    
    -------当前的组合关系-------
    a1的b_list属性: []
    b1的a_list中的A对象的b_list属性: []

    大海:嘿嘿,马上要load回来啦,看好了啊!

     1 class A:
     2     def __init__(self, name):
     3         self.name = name
     4         self.b_list = []
     5 
     6 
     7 class B:
     8     def __init__(self, name):
     9         self.name = name
    10         self.a_list = []
    11 
    12 
    13 # 将a1和b1加载回来
    14 import pickle
    15 with open('a1b1.pk', 'rb') as f:
    16     a1 = pickle.load(f)
    17     b1 = pickle.load(f)
    18 
    19 # 打印对象组合关系
    20 print('-------加载回来的组合关系-------')
    21 print('a1的b_list属性:', a1.b_list)
    22 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    23 
    24 # 再实例化一个b2
    25 b2 = B('B类2号')
    26 a1.b_list.append(b2)
    27 
    28 # 打印对象组合关系
    29 print('
    -------给a1列表属性添加b2后的组合关系-------')
    30 print('a1的b_list属性:', a1.b_list)
    31 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    32 
    33 # 打印 a1对象
    34 print('
    a1对象:')
    35 print(a1)
    36 
    37 # 打印 b1对象中a_list列表
    38 print('
    b1对象的a_list列表:')
    39 print(b1.a_list)

    运行结果

    -------加载回来的组合关系-------
    a1的b_list属性: []
    b1的a_list中的A对象的b_list属性: []
    
    -------给a1列表属性添加b2后的组合关系-------
    a1的b_list属性: [<__main__.B object at 0x00000000024A32B0>]
    b1的a_list中的A对象的b_list属性: []
    
    a1对象:
    <__main__.A object at 0x0000000002571470>
    
    b1对象的a_list列表:
    [<__main__.A object at 0x0000000002572A90>]

    大海:我X,怎么还是不行啊!a1对象和b1.a_list里的那个A类实例对象还是不同!到底要怎么才行啊!啊!啊!

    流星:大哥,别鸡冻……

    大海:啊!啊!啊!解决不了问题,我就鸡冻!

    流星:你鸡冻起来也别拍我行吗?我都快被你拍死了……

    大海:啊!啊!啊!怎么回事啊!

    流星:我直接告诉你行了吧= =!

    大海:你倒是快讲啊!

    流星:好好好……那就再来一条……

    大海:%¥&#@*!¥&*

    流星:别拍了,不来了……

    大海:快说!

    流星:其实答案就是——把a1与b1对象dump到同一个文件里头!……

    大海:你大爷,刚刚不就是这么说的吗?

    流星:那是因为我话还没说完呢,你就是没耐性,不等我把话说完你就吵着说明白了……

    大海:那你继续说完啊!

    流星:你别打断我……除了要把a1与b1对象dump到同一个文件里头,还要保证,是同一次dump命令序列化的

    大海:说人话!

    流星:= =!我的意思就是,可以把a1与b1合并成一个元组,这样就可以通过这个元组把a1与b1对象一次性dump到文件里了

    大海:我X,这也行啊。。。我怎么没想到。。。

    流星:是啊,不信我运行下你看看。先dump文件……

     1 class A:
     2     def __init__(self, name):
     3         self.name = name
     4         self.b_list = []
     5 
     6 
     7 class B:
     8     def __init__(self, name):
     9         self.name = name
    10         self.a_list = []
    11 
    12 
    13 # 各实例化一个对象
    14 a1 = A('A类1号')
    15 b1 = B('B类1号')
    16 
    17 # 打印 2个实例各自的列表属性
    18 print('-------当前各自的列表属性-------')
    19 print('a1的b_list属性:', a1.b_list)
    20 print('b1的a_list属性:', b1.a_list)
    21 
    22 # 在实例b1的列表属性中建立组合关系
    23 b1.a_list.append(a1)
    24 
    25 # 打印对象组合关系
    26 print('
    -------当前的组合关系-------')
    27 print('a1的b_list属性:', a1.b_list)
    28 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    29 
    30 # pickle序列化用元组1次dump到同1个文件
    31 import pickle
    32 with open('a1b1.pk', 'wb') as f:
    33     pickle.dump((a1, b1), f)

    流星:打印结果当然还是

    -------当前各自的列表属性-------
    a1的b_list属性: []
    b1的a_list属性: []
    
    -------当前的组合关系-------
    a1的b_list属性: []
    b1的a_list中的A对象的b_list属性: []

    流星:再一次load回来

     1 class A:
     2     def __init__(self, name):
     3         self.name = name
     4         self.b_list = []
     5 
     6 
     7 class B:
     8     def __init__(self, name):
     9         self.name = name
    10         self.a_list = []
    11 
    12 
    13 # 将a1和b1加载回来
    14 import pickle
    15 with open('a1b1.pk', 'rb') as f:
    16     a1, b1 = pickle.load(f)
    17 
    18 # 打印对象组合关系
    19 print('-------加载回来的组合关系-------')
    20 print('a1的b_list属性:', a1.b_list)
    21 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    22 
    23 # 再实例化一个b2
    24 b2 = B('B类2号')
    25 a1.b_list.append(b2)
    26 
    27 # 打印对象组合关系
    28 print('
    -------给a1列表属性添加b2后的组合关系-------')
    29 print('a1的b_list属性:', a1.b_list)
    30 print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list)
    31 
    32 # 打印 a1对象
    33 print('
    a1对象:')
    34 print(a1)
    35 
    36 # 打印 b1对象中a_list列表
    37 print('
    b1对象的a_list列表:')
    38 print(b1.a_list)

    运行结果

    -------加载回来的组合关系-------
    a1的b_list属性: []
    b1的a_list中的A对象的b_list属性: []
    
    -------给a1列表属性添加b2后的组合关系-------
    a1的b_list属性: [<__main__.B object at 0x00000000025D2A90>]
    b1的a_list中的A对象的b_list属性: [<__main__.B object at 0x00000000025D2A90>]
    
    a1对象:
    <__main__.A object at 0x00000000025D1470>
    
    b1对象的a_list列表:
    [<__main__.A object at 0x00000000025D1470>]

    大海:我去,真的一样了!牛X!

    流星:那必须的!这里同时把a1,b1对象dump到一个文件时,储存的组合关系是在这个文件里头的,再load出来返回给新程序的a1和b1对象,也是直接把组合关系指定重构给了新的a1和b1对象,没有再去单独开辟内存空间去生成其他的对象了

    大海:原来是这样,厉害!

    流星:哈哈,为了庆祝,再来条华丽的分割线吧?

    大海:来吧,随便你来!


    流星:其实再来个分割线是为了再举2个pickle序列化的例子的,或许看了下面2个例子,会更好地理解这个问题

    大海:好!你发出来,我研究研究

    流星:嗯,先看个列表对象“递归引用”的例子

    大海:啥?“递归引用”?我瞅瞅……

    >>> l = [1, 2, 3]  
    >>> l.append(l)  
    >>> l  
    [1, 2, 3, [...]]  
    >>> l[3]  
    [1, 2, 3, [...]]  
    >>> l[3][3]  
    [1, 2, 3, [...]]  
    >>> p = pickle.dumps(l)  
    >>> l2 = pickle.loads(p)  
    >>> l2  
    [1, 2, 3, [...]]  
    >>> l2[3]  
    [1, 2, 3, [...]]  
    >>> l2[3][3]  
    [1, 2, 3, [...]]  

    大海:我去,原来列表还能这么玩啊!?

    流星:是啊,看过“递归引用”了,那么也能有点接受下面这个“循环引用”的例子了吧……

    大海:还有……“循环引用”……?

    >>> a = [1, 2]  
    >>> b = [3, 4]  
    >>> a.append(b)  
    >>> a  
    [1, 2, [3, 4]]  
    >>> b.append(a)  
    >>> a  
    [1, 2, [3, 4, [...]]]  
    >>> b  
    [3, 4, [1, 2, [...]]]  
    >>> a[2]  
    [3, 4, [1, 2, [...]]]  
    >>> b[2]  
    [1, 2, [3, 4, [...]]]  
    >>> a[2] is b  
    1  
    >>> b[2] is a  
    1  
    >>> f = file('temp.pkl', 'w')  
    >>> pickle.dump((a, b), f)  
    >>> f.close()  
    >>> f = file('temp.pkl', 'r')  
    >>> c, d = pickle.load(f)  
    >>> f.close()  
    >>> c  
    [1, 2, [3, 4, [...]]]  
    >>> d  
    [3, 4, [1, 2, [...]]]  
    >>> c[2]  
    [3, 4, [1, 2, [...]]]  
    >>> d[2]  
    [1, 2, [3, 4, [...]]]  
    >>> c[2] is d  
    1  
    >>> d[2] is c  
    1  

    大海:咦,这个“循环引用”也用到了把a、b两个列表对象合并成一个元组dump到同一个文件里头啊

    流星:是的!之所以这么做,是因为“递归引用”和“循环引用”也类似对象间的组合关系,本质都是一个对象与一个对象建立了内存地址的引用(递归引用是引用自己)关系。要用元组的形式,同时一次性将2个对象序列化dump到同一个文件,保留指定的2个对象的引用关系,并且反序列化时就能把这个引用关系把指定返回给重构的2个对象了。

    大海:原来如此……

    流星:嗯,我们可以看看,如果“循环引用”的例子,改成将a与b分2次dump到1个文件里头,结果会怎样?

    >>> f = file('temp.pkl', 'w')  
    >>> pickle.dump(a, f)  
    >>> pickle.dump(b, f)  
    >>> f.close()  
    >>> f = file('temp.pkl', 'r')  
    >>> c = pickle.load(f)  
    >>> d = pickle.load(f)  
    >>> f.close()  
    >>> c  
    [1, 2, [3, 4, [...]]]  
    >>> d  
    [3, 4, [1, 2, [...]]]  
    >>> c[2]  
    [3, 4, [1, 2, [...]]]  
    >>> d[2]  
    [1, 2, [3, 4, [...]]]  
    >>> c[2] is d  
    0  
    >>> d[2] is c  
    0  

    流星:你看,这里分2次dump到1个文件的话,第一次由a对象单独dump进文件的字符串信息load回的对象c,c[2]引用的不再是对象d了(d是由b对象dump进文件的字符串信息load回的),d[2]引用的也不再是对象c了,所以a与b本身的相互引用关系,已经在分开2次dump时丢失掉了。那这里c[2],d[2]引用的是谁呢?是各自从文件load重构成列表对象c和d时,为了保证c[2]和d[2]的值与之前相等,计算机自己用list类重新生成的两个新的列表对象让他们各自引用

    大海:明白了!

    流星:最后,推荐个详细讲解pickle模块的博客文章:《python pickle模块

  • 相关阅读:
    一个人事工资模块
    Delete From 带 inner join
    打开SQL AnyWhere *.db数据库
    开启查询IO操作统计
    一个大数据量表访问优化联动下拉框查询优化
    一个简单的配置文件读取类
    MSSQL2005 双机热备说明
    数据库镜像
    GridView + ObjectDatasource 的一个范例代码
    往带自增长列的数据表中导数据
  • 原文地址:https://www.cnblogs.com/oceanicstar/p/9030121.html
Copyright © 2020-2023  润新知