【原创】面向对象作业:选课系统中用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模块》