Python中类名的自动全名资格是如何工作的?[与对象序列化相关]
(你可以直接跳到问题部分,跳过介绍。)
在从用户自定义类中保存(序列化)Python对象时,常常会遇到一个问题:
# This is program dumper.py
import pickle
class C(object):
pass
with open('obj.pickle', 'wb') as f:
pickle.dump(C(), f)
实际上,试图从另一个程序 loader.py
中恢复这个对象时,
# This is program loader.py
with open('obj.pickle', 'rb') as f:
obj = pickle.load(f)
会出现
AttributeError: 'module' object has no attribute 'C'
实际上,这个类是通过名字("C")进行保存的,而 loader.py
程序对 C
一无所知。一个常见的解决办法是通过以下方式导入:
from dumper import C # Objects of class C can be imported
with open('obj.pickle', 'rb') as f:
obj = pickle.load(f)
不过,这个解决办法有一些缺点,包括需要导入所有被保存对象引用的类(可能有很多);此外,局部命名空间会被 dumper.py
程序中的名字污染。
现在,解决这个问题的方法是在保存之前对对象进行完全限定:
# New dumper.py program:
import pickle
import dumper # This is this very program!
class C(object):
pass
with open('obj.pickle', 'wb') as f:
pickle.dump(dumper.C(), f) # Fully qualified class
使用上面的原始 loader.py
程序进行恢复现在可以直接工作(不需要执行 from dumper import C
)。
问题:现在,来自 dumper.py
的其他类似乎在保存时自动进行了完全限定,我想知道这是怎么回事,以及这是否是一个可靠且有文档支持的行为:
import pickle
import dumper # This is this very program!
class D(object): # New class!
pass
class C(object):
def __init__(self):
self.d = D() # *NOT* fully qualified
with open('obj.pickle', 'wb') as f:
pickle.dump(dumper.C(), f) # Fully qualified pickle class
现在,使用原始的 loader.py
程序进行恢复也可以正常工作(不需要执行 from dumper import C
);print obj.d
输出的是一个完全限定的类,这让我感到惊讶:
<dumper.D object at 0x122e130>
这种行为非常方便,因为只需要对顶层的被保存对象使用模块名进行完全限定(dumper.C()
)。但这种行为是否可靠且有文档支持?为什么类是通过名字("D")进行保存的,但在恢复时却决定 self.d
属性是类 dumper.D
(而不是某个本地的 D
类)?
附注:问题的进一步细化:我刚刚注意到一些有趣的细节,可能指向这个问题的答案:
在保存程序 dumper.py
中,print self.d
输出 <__main__.D object at 0x2af450>
,这是在第一个 dumper.py
程序中(没有 import dumper
)。另一方面,执行 import dumper
并在 dumper.py
中用 dumper.C()
创建对象时,print self.d
输出 <dumper.D object at 0x2af450>
:self.d
属性被Python自动进行了限定!所以,看起来 pickle
模块在上述良好的恢复行为中并没有起到作用。
因此,真正的问题是:为什么Python在第二种情况下将 D()
转换为完全限定的 dumper.D
?这个问题是否有文档说明?
2 个回答
当你的类在主模块中定义时,pickle会在这个地方寻找它们,特别是在你想要把它们从文件中读取出来的时候。在你的第一个例子中,类是在主模块中定义的,所以当loader运行时,loader就是主模块,而pickle找不到这些类。如果你查看obj.pickle
的内容,你会看到__main__
被用作你C和D类的命名空间。
在你的第二个例子中,dumper.py自己导入了自己。这样一来,你实际上就有了两组不同的C和D类:一组在__main__
命名空间中,另一组在dumper
命名空间中。你序列化的是在dumper
命名空间中的那一组(可以查看obj.pickle
来确认)。
如果找不到命名空间,pickle会尝试动态导入它,所以当loader.py运行时,pickle本身会导入dumper.py以及dumper.C和dumper.D类。
由于你有两个独立的脚本,dumper.py和loader.py,因此将它们共享的类定义在一个公共的导入模块中是很有意义的:
common.py
class D(object):
pass
class C(object):
def __init__(self):
self.d = D()
loader.py
import pickle
with open('obj.pickle','rb') as f:
obj = pickle.load(f)
print obj
dumper.py
import pickle
from common import C
with open('obj.pickle','wb') as f:
pickle.dump(C(),f)
需要注意的是,即使在这个例子中dumper.py导出了C()
,pickle也知道它是一个common.C
对象(查看obj.pickle
)。当loader.py运行时,它会动态导入common.py,并成功加载这个对象。
事情是这样的:当你在 dumper.py
文件里导入 dumper
(或者用 from dumper import C
)的时候,整个程序会被重新解析一遍(你可以通过在模块里插入一个打印语句来看到这一点)。这种行为是正常的,因为 dumper
这个模块并没有被提前加载(不过 __main__
是被认为已经加载了的)——它不在 sys.modules
里。
正如 Mark 的回答中所示,导入一个模块会让模块里定义的所有名称都被识别,所以当重新解析 dumper.py
文件时,self.d = D()
会被理解为是 dumper.D
类的实例(这和解析 common.py
是一样的,正如 Mark 的回答所说)。
因此,import dumper
(或者 from dumper import C
)的这个小技巧就解释清楚了,序列化不仅会完整地识别 C
类,还会识别 D
类。这使得通过外部程序反序列化变得更简单!
这也表明,在 dumper.py
里执行 import dumper
会让 Python 解释器解析程序两次,这样做既不高效也不优雅。因此,在一个程序里序列化类,然后在 另一个 程序里反序列化,最好还是按照 Mark 的回答中提到的方法来做:序列化的类应该放在一个单独的模块里。