【原创】面向对象作业:选课系统中用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('\n-------当前的组合关系-------')
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('\n-------给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('\na1对象:')
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('\n-------当前的组合关系-------')
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('\n-------给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('\na1对象:')
35 print(a1)
36 
37 # 打印 b1对象中a_list列表
38 print('\nb1对象的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('\n-------当前的组合关系-------')
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('\n-------给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('\na1对象:')
34 print(a1)
35 
36 # 打印 b1对象中a_list列表
37 print('\nb1对象的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模块

posted @ 2018-05-12 22:09  海上流星  阅读(1006)  评论(1编辑  收藏  举报