【原创】面向对象作业:选课系统中用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储存多个对象之间的组合关系问题,要先从一个面向对象的例子开始说起。。。

大海:啥例子?

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

 class A:
def __init__(self, name):
self.name = name
self.b_list = [] class B:
def __init__(self, name):
self.name = name
self.a_list = [] # 各实例化一个对象
a1 = A('A类1号')
b1 = B('B类1号') # 打印 2个实例各自的列表属性
print('-------当前各自的列表属性-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list属性:', b1.a_list) # 在实例b1的列表属性中建立组合关系
b1.a_list.append(a1) # 打印对象组合关系
print('\n-------当前的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # pickle序列化保存到 2个文件里
import pickle
with open('a1.pk', 'wb') as f1:
pickle.dump(a1, f1)
with open('b1.pk', 'wb') as f2:
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回来的话,他们组合关系肯定还是在的吧!

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

 import pickle

 class A:
def __init__(self, name):
self.name = name
self.b_list = [] class B:
def __init__(self, name):
self.name = name
self.a_list = [] # 将a1和b1加载回来
with open('a1.pk', 'rb') as f1:
a1 = pickle.load(f1)
with open('b1.pk', 'rb') as f2:
b1 = pickle.load(f2) # 打印对象组合关系
print('-------加载回来的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # 再实例化一个b2
b2 = B('B类2号')
a1.b_list.append(b2) # 打印对象组合关系
print('\n-------给a1列表属性添加b2后的组合关系-------')
print('a1的b_list属性:', a1.b_list)
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列表属性看看?

 # 打印 a1对象
print('\na1对象:')
print(a1) # 打印 b1对象中a_list列表
print('b1对象的a_list列表:')
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两次对吧,我聪明吧?哈哈~

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

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

 class A:
def __init__(self, name):
self.name = name
self.b_list = [] class B:
def __init__(self, name):
self.name = name
self.a_list = [] # 各实例化一个对象
a1 = A('A类1号')
b1 = B('B类1号') # 打印 2个实例各自的列表属性
print('-------当前各自的列表属性-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list属性:', b1.a_list) # 在实例b1的列表属性中建立组合关系
b1.a_list.append(a1) # 打印对象组合关系
print('\n-------当前的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # pickle序列化分两次dump到 1个文件里
import pickle
with open('a1b1.pk', 'wb') as f:
pickle.dump(a1, f)
pickle.dump(b1, f)

运行结果

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

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

 class A:
def __init__(self, name):
self.name = name
self.b_list = [] class B:
def __init__(self, name):
self.name = name
self.a_list = [] # 将a1和b1加载回来
import pickle
with open('a1b1.pk', 'rb') as f:
a1 = pickle.load(f)
b1 = pickle.load(f) # 打印对象组合关系
print('-------加载回来的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # 再实例化一个b2
b2 = B('B类2号')
a1.b_list.append(b2) # 打印对象组合关系
print('\n-------给a1列表属性添加b2后的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # 打印 a1对象
print('\na1对象:')
print(a1) # 打印 b1对象中a_list列表
print('\nb1对象的a_list列表:')
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文件……

 class A:
def __init__(self, name):
self.name = name
self.b_list = [] class B:
def __init__(self, name):
self.name = name
self.a_list = [] # 各实例化一个对象
a1 = A('A类1号')
b1 = B('B类1号') # 打印 2个实例各自的列表属性
print('-------当前各自的列表属性-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list属性:', b1.a_list) # 在实例b1的列表属性中建立组合关系
b1.a_list.append(a1) # 打印对象组合关系
print('\n-------当前的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # pickle序列化用元组1次dump到同1个文件
import pickle
with open('a1b1.pk', 'wb') as f:
pickle.dump((a1, b1), f)

流星:打印结果当然还是

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

流星:再一次load回来

 class A:
def __init__(self, name):
self.name = name
self.b_list = [] class B:
def __init__(self, name):
self.name = name
self.a_list = [] # 将a1和b1加载回来
import pickle
with open('a1b1.pk', 'rb') as f:
a1, b1 = pickle.load(f) # 打印对象组合关系
print('-------加载回来的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # 再实例化一个b2
b2 = B('B类2号')
a1.b_list.append(b2) # 打印对象组合关系
print('\n-------给a1列表属性添加b2后的组合关系-------')
print('a1的b_list属性:', a1.b_list)
print('b1的a_list中的A对象的b_list属性:', b1.a_list[0].b_list) # 打印 a1对象
print('\na1对象:')
print(a1) # 打印 b1对象中a_list列表
print('\nb1对象的a_list列表:')
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模块

上一篇:云起冬季实战营第二期期学习报告——Linux指令入门


下一篇:CBO为什么不走索引?