问题:pickle 无法保存 namedtuple
具体描述:
报错信息
Traceback (most recent call last):
File "/home/liyd/anaconda3/envs/py3/lib/python3.6/site-packages/numpy/lib/npyio.py", line 529, in save
pickle_kwargs=dict(fix_imports=fix_imports))
File "/home/liyd/anaconda3/envs/py3/lib/python3.6/site-packages/numpy/lib/format.py", line 664, in write_array
pickle.dump(array, fp, protocol=3, **pickle_kwargs)
_pickle.PicklingError: Can't pickle <class '__main__.cam'>: attribute lookup cam on __main__ failed
python-BaseException
查看上述报错信息,定位到错误出现在一个叫做__main__.cam的类里,pickle是在__main__中调用的,所以尝试在__main__中寻找cam,这不奇怪,看一下pickle调用的位置:
追根溯源
for sequence in seqList:
audioDataDic= {}
for cam_number in range(1, 4):
DATA, CFGforGCF = InitGCF(datasetPath_au, sequence, cam_number)
audioData = {f'{sequence}_cam{cam_number}': DATA}
audioDataDic.update(audioData)
# save the imgDataList as {sequence}_sampleList.npz
folderPath = f'{datasetPath_save}/audio/{sequence}'
if not os.path.exists(folderPath):
os.makedirs(folderPath)
np.save(f'{folderPath}/{sequence}_audio.npz', audioDataDic)# audioDic=audioDataDic
print(f'save audio.npz for {sequence}')
问题不在pickle,而在cam,为什么main中找不到cam呢?因为cam类是定义在函数里的,出了函数类,cam的定义就消失了,cam成了一个孤儿对象,pickle当然也找不到cam的定义。看一下cam的定义部分:
孤儿对象无法保存
class DataMain:
def __init__(self,cfgGCF):
……
def loadCamAlign(self,datasetPath):
……
cam = {
'Pmat': np.concatenate(([data[0][0]], [data[1][0]], [data[2][0]]), axis=0),
'K': np.concatenate(([data[0][1]], [data[1][1]], [data[2][1]]), axis=0),
'alpha_c': np.concatenate(([data[0][2]], [data[1][2]], [data[2][2]]), axis=0),
'kc': np.concatenate(([data[0][3]], [data[1][3]], [data[2][3]]), axis=0),
}
self.cam = namedtuple('cam',cam.keys())(**cam)
dataPath = f'{datasetPath}/rigid010203.mat'
data = scio.loadmat(dataPath)['rigid'][0]
self.align_mat = data[0][1]
cam的定义实际上在DataMain.loadCamAlign.cam里且没有引用名,pickle找的时候只知道去__main__.cam里找,自然是找不到的。那么怎么才能找到呢?答案就是把类的定义放在外面。
解决:
定义cam类
分析一下定义self.cam = namedtuple('cam',cam.keys())(**cam)这句话:
namedtuple('cam',cam.keys())
首先,namedtuple是一个类型工厂,输入类型的名字和参数,可以输出一个新的类。这个新的类目前没有引用指向它。想让pickle查到它,就要给它一个名字,并且放在函数外面,避免退出函数的时候类定义被销毁,参考1中说明了这个原理。
(**cam)
其次,采用这个“匿名类”新建了一个对象,其参数由字典cam决定。最后,把这个单例对象返回给self.cam。
重新定义cam类
camCls = namedtuple('cam',['Pmat','K','alpha_c','kc'])
camCls此时存储了这个类的索引,如果pickle从camCls中获取这个类的信息,就能够把cam保存下来了。但是实际运行发现,pickle找不到camCls。这是为什么呢?
因为pickle保存的时候,只拿到了类的对象,对象中存储了类名cam,pickle就拿着cam去寻找定义,类的内容如果存储在camCls中pickle是不知道的。一般认为,Python中所有的名字都不重要,类的名字、对象的名字都可以随意更换,只要指向的实体的对的,程序就能正常工作,但是保存操作中,名字是重要的,需要依靠名字去找到正确的实体。参考2中也举了类似的例子,说明更换名字的问题。
确保cam类名字正确
正确的起名方法是这样的:
camCls = namedtuple('camCls',['Pmat','K','alpha_c','kc'])
回想一下,平时定义类的时候为什么没有出现这个问题?因为类的定义大多是发生在__main__中的,而类的名字一般也不会随意更换为缩写,就算更换了一般也不会刚好需要保存。但是namedtuple新建的单例对象恰好是可以同时满足这些要求。
总结:
改进后的代码变成了:
camClsAttr = ['Pmat','K','alpha_c','kc']
camCls = namedtuple('camCls', camClsAttr)
class DataMain:
def __init__(self,cfgGCF):
……
def loadCamAlign(self,datasetPath):
……
cam = {
'Pmat': np.concatenate(([data[0][0]], [data[1][0]], [data[2][0]]), axis=0),
'K': np.concatenate(([data[0][1]], [data[1][1]], [data[2][1]]), axis=0),
'alpha_c': np.concatenate(([data[0][2]], [data[1][2]], [data[2][2]]), axis=0),
'kc': np.concatenate(([data[0][3]], [data[1][3]], [data[2][3]]), axis=0),
}
self.cam = camCls(**cam)
dataPath = f'{datasetPath}/rigid010203.mat'
data = scio.loadmat(dataPath)['rigid'][0]
self.align_mat = data[0][1]
将定义类放在了函数外面,避免对象成为孤儿对象,让pickle保存时也能找到类的定义。这个时候cam不再是一个匿名类,退化成一个普通的类。同样也要注意unpickle的过程中,需要让pickle能以相同的方式找到相同的类型。
参考: