这几天在看 DrPython 的代码的时候,看到了 drPreferences.py 是用来保存参数配置信息的。 DrPython 中可以配置的参数非常多,因此它使用 drPreferences 类来进行保存,而且为了使用户的修改能够保存起来,那么需要将类的信息保存起来。这里我用类的信息并不精确,应该说是将 drPreferences 类的某个实例的成员属性保存起来。只需要保存属性值就可以了。而且在启动 DrPython 时,还需要将信息从保存的文件中读出来。 DrPython 使用了 drPrefsFile.py 模块来作这件事,不过代码好长,而且语句相似,因此我就想到有没有简单的方法实现这一功能。
一种是可以使用pickle或cpickle模块。 pickle 在英语中可以译为“腌制”,不过翻译成中文可不怎么好听,还是保留原样吧。它的作用就是将对象或模块保存到文件中,有些面向对象技术把这种处理叫作“serialization(序列化)”,而且可以从文件中恢复对象的值。cpickle与pickle功能基本一样,但cpickle是C语言实现的,速度要比pickle快1000倍(这句话不是我说的,是 Python 文档中说的),而且cpickle不支持子类化,而pickle可以。那么哪些数据可以被pickle呢?文档中说:
None
, True
, and False
- integers, long integers, floating point numbers(浮点数), complex numbers (复数)
- normal and Unicode strings (正常和Unicode字符串)
- tuples, lists, and dictionaries containing only picklable objects (仅包含可以pickle对象的元组,列表和字典)
- functions defined at the top level of a module (定义在模块顶层的函数)
- built-in functions defined at the top level of a module (定义在模块顶层的内置函数)
- classes that are defined at the top level of a module (定义在模块顶层的类)
- instances of such classes whose __dict__ or __setstate__() is picklable(这样的类实例,它们的__dict__ 或 __setstate__() 是可以pickle的)
这样,使用pickle模块提供的方法,dump()进行pickle操作,load()进行unpickle操作。还可以先生成Pickler或Unpickler类,再调用相应的dump()或load()方法进行pickle操作。保存的结果是用ascii表示的字符序列。有兴趣的可以看一看,乱七八糟的:)不过,虽然可以保存,但不直观。pickle还有一点要注意的是,在你从文件中恢复对象时,对象的类所在的模块应该是可以导入的,也就是说pickle在unpickle时,因为要生成原来类的对象,因此首先要找到类对象所在的模块,然后将其导入再生成类的实例。如果在unpickle时,类模块无法导入,unpickle会失败。
第二种就是自已来做。当然,如果数据类型很复杂,自已做的确麻烦。不过,就 DrPython 项目来说,保存到文件中的每个参数都不复杂,作起来不成问题。这里我们讨论一下最简单的情况。
如我们有一个类:
class a:
def __init__(self):
self.a=1
self.b=’a’
def pp(self):
print self.a, self.b
b=a()
枚举对象的所有成员变量
下面我分析一下对象b,因为a是类,只有b才有真正的数据,保存它才有意义。
>>> print b.__dict__
{‘a’: 1, ‘b’: ‘a’}
因此对象的__dict__中存放着所有的成员变量。这样,我们可以对__dict__进行循环处理,即可以把它们保存到文件中去。我们可以一个模块做这件事。
from types import*
def _defaultsavefunc(k, v):
if type(k) == IntType:
return “%s i %d\n” % (k, v)
elif type(k) == StringType:
return “%s s %s\n” % (k, v)
else:
raise SaveException()
def saveobj(obj, file, savefunc=_defaultsavefunc):
for k, v in obj.__dict__.items():
file.write(savefunc(k, v))
class SaveException(Exception):
pass
这样,我们使用saveobj就可以将一个对象保存到文件中去。例如:
if __name__ == ‘__main__’:
class a:
def __init__(self):
self.a=1
self.b=’a’
def pp(self):
print self.a, self.b
b=a()
saveobj(b, open(“mysave.p”, “w”))
当我们执行完毕后,查看文件mysave.p,会看到:
a i 1
b s a
一个变量占一行,变量名后面为一个空格,再后面是类型:i为整数,s为字符串,再后面是空格,然后是它的值。当然这个例子很简单,它只实现了两个类型:int和string。
保存做完后,下面就可以做恢复了。
def _defaultreadfunc(line):
k, t, v = line.split(‘ ‘, 2)
if t == ‘i’:
v=int(v)
return k, v
def readobj(file, readfunc=_defaultreadfunc):
obj=_emptyclass()
line=file.readline()[:-1]
while line:
k, v = readfunc(line)
setattr(obj, k, v)
line=file.readline()
return obj
class _emptyclass:
pass
这里为了返回一个对象,我们先建立了一个空类,用它生成一个对象,然后接收我们保存在文件中的数据。测试一下:
>>> c=readobj(open(“mysave.p”, “r”))
>>> print c.a, c.b
1 a
但这里面其实是存在一些问题的,我列在下面,大家可以思考一下:
- 为什么要用空类,不能直接生成a的对象吗?
- 可以使用c.pp()来输出吗?
回答一:使用空类是因为我们没有在文件中保存b对象的类信息,如:类a在哪个模块中,类名是什么。因些通过文件生成类a的对象是不可能的,因此只能用一个空类。这也就是为什么pickle需要能够导入对象类的原因,这样它就可以真正生成原来类的对象了,而不是象我们生成了另外一个类。
回答二:因为c是空类(_emptyclass)的对象因此不存在pp()函数,调用会失败。
如果想解决这些问题,首先要在保存文件中保存原来的类所在的模块和类名,使用类的__module__和__name__值即可。然后在生成类时,根据文件中的模块名和类名,将类导入,然后修改对象的__class__值即可。说起来容易作起来难。下面我从pickle中找到部分代码贴在下面:
def find_class(self, module, name):
# Subclasses may override this
__import__(module)
mod = sys.modules[module]
klass = getattr(mod, name)
return klass
这是根据模块名和类名找到真正的类对象,没错,是类这个对象,不是类的对象这个对象。糊涂了吗?类也是对象啊!
def _instantiate(self, klass, k):
args = tuple(self.stack[k+1:])
del self.stack[k:]
instantiated = 0
if (not args and
type(klass) is ClassType and
not hasattr(klass, “__getinitargs__”)):
try:
value = _EmptyClass()
value.__class__ = klass
instantiated = 1
except RuntimeError:
# In restricted execution, assignment to inst.__class__ is
# prohibited
pass
if not instantiated:
try:
value = klass(*args)
except TypeError, err:
raise TypeError, “in constructor for %s: %s” % (
klass.__name__, str(err)), sys.exc_info()[2]
self.append(value)
上面生成对象采用了两种方法,一种可以称为MixIn的方法(上面红色字),即先生成一个空类,然后根据导入的类将这个空类变成为导入的类,即把一个原来是类A的对象改成了类B的对象。有趣吧,体会到 Python 动态性的强大了吧。另一种是直接由导入的类生成实例(上面兰色字)。这两种生成策略是由环境不同造成的,这就不细说了。
因此要实现比较完善的保存机制的确还有一些工作要做,有兴趣的话,可以做一个。
那么我们生成的这个对象到底算什么呢?它只是将原来对象的数据恢复了过来。不过,我们还可以使用一个变通的方法使我们的恢复工作做得更好。我们可以这样设想,我们已经存在了一个类的对象,现在只是想将它上次的状态恢复,而不是重新生成一个新的对象,如果这样的话,可以做得简单一些。修改readobj函数:
def readobj(file, obj=None, readfunc=_defaultreadfunc):
if obj == None:
obj=_emptyclass()
line=file.readline()[:-1]
while line:
k, v = readfunc(line)
setattr(obj, k, v)
line=file.readline()
return obj
这样,我们将obj做为一个参数,如果传入一个对象了,就直接将值保存在对象中,否则再生成一个新的空对象。这样的效果要好一些。测试一下:
>>> c=a()
>>> readobj(open(“mysave.p”, “r”), c)
>>> c.pp()
1 a
看见了,一切OK。
只不过它的前提是我们已经有了一个对象了。
还有问题吗?我想可能就是对于复杂数据的表示,如tuple, list, dictionary,更复杂的如子对象,嗬嗬!有兴趣的自已实现吧。我想可能使用xml格式可能是一个好方法。