文章来自《Python cookbook》.

翻译仅仅是为了个人学习,其它商业版权纠纷与此无关!

-- 218.25.65.60 [2004-09-30 20:35:59]

1. 描述,问题,讨论

Module: Versioned Backups

模块: 版本备份

Credit: Mitch Chapman

Before overwriting an existing file, it is often desirable to make a backup. Example 4-1 emulates the behavior of Emacs by saving versioned backups. It's also compatible with the marshal module, so you can use versioned output files for output in marshal format. If you find other file-writing modules that, like marshal, type-test rather than using file-like objects polymorphically, the class supplied here will stand you in good stead.

覆盖写入文件前备份文件经常会是良好的实践. 例子 4-1模拟了Emacs的版本备份行为。它的代码同模块marshal也是兼容的,因此可以将 版本备份出来的文件以marshal的模式输出。遇到其他模块,如marshal,检测类型而不是多态的使用类似文件的对象,都可以用这里的脚本类代替你自己的对象封装。 (???)

When Emacs saves a file foo.txt, it first checks to see if foo.txt already exists. If it does, the current file contents are backed up. Emacs can be configured to use versioned backup files, so, for example, foo.txt might be backed up to foo.txt.~1~. If other versioned backups of the file already exist, Emacs saves to the next available version. For example, if the largest existing version number is 19, Emacs will save the new version to foo.txt.~20~. Emacs can also prompt you to delete old versions of your files. For example, if you save a file that has six backups, Emacs can be configured to delete all but the three newest backups.

Emacs存文件foo.txt时,首先检查foo.txt是否存在。如果文件已经存在,那么Emacs备份当前的文件。可以配置Emacs使得它用版本来备份文件,比如,foo.txt可能备份成foo.txt.~1~。如果其他的版本已经存在,就使用下一个可以使用的版本号,比如,当前最大的版本号为19,那么新版本存为foo.txt.~20~。 Emacs也会提示你删除文件的旧版本。举个例子,如果一个文件已经有了6分备份,当你再次存储时,Emacs可以配置成保留最新的3个备份,删除其他的旧备份。

Example 4-1 emulates the versioning backup behavior of Emacs. It saves backups with version numbers (e.g., backing up foo.txt to foo.txt.~n~ when the largest existing backup number is n-1). It also lets you specify how many old versions of a file to save. A value that is less than zero means not to delete any old versions.

例子 4-1模拟了Emacs的版本备份机制。备份使用了版本号(备份文件foo.txt当前最大的版本号为n-1,使用foo.txt.~n~作为备份文件名),同时可以让你指定备份的最多数量。一个负数被认为是不删除任何旧的备份版本。

The marshal module lets you marshal an object to a file by way of the dump function, but dump insists that the file object you provide actually be a Python file object, rather than any arbitrary object that conforms to the file-object interface. The versioned output file shown in this recipe provides an asFile method for compatibility with marshal.dump. In many (but, alas, far from all) cases, you can use this approach to use wrapped objects when a module type-tests and thus needs the unwrapped object, solving (or at least ameliorating) the type-testing issue mentioned in Recipe 5.9. Note that Example 4-1 can be seen as one of many uses of the automatic-delegation idiom mentioned there.

模块marshal使用函数dump将对象编组到文件中,但是 dump要求输入的文件对象确实是一个Python文件对象,而不是一个实现了文件对象接口的任意的对象。食谱中的版本输出文件提供了一个与marshal.dump兼容的asFile方法。 许多(不过,啊!距离全部很远)情况下,当模块检测类型需要对象的封装还原时,可以对被包装的对象使用例子中的方法,解决(至少可以改善)食谱5.9中提到的type-testing问题。

The only true solution to the problem of modules using type tests rather than Python's smooth, seamless polymorphism is to change those errant modules, but this can be hard in the case of errant modules that you did not write (particularly ones in Python's standard library).

一些模块使用类型检测而不是Python提供的简单的,无缝的多态行为,这个问题的真正的解决是改变这些易出错的模块本身,但是,这些模块(Python库中的模块)不是你自己编写的,改动起来很困难。

Example 4-1. Saving backups when writing files

   1 """ This module provides versioned output files. When you write to such
   2 a file, it saves a versioned backup of any existing file contents. """
   3 
   4 import sys, os, glob, string, marshal
   5 
   6 class VersionedOutputFile:
   7     """ Like a file object opened for output, but with versioned backups
   8     of anything it might otherwise overwrite """
   9 
  10     def _ _init_ _(self, pathname, numSavedVersions=3):
  11         """ Create a new output file. pathname is the name of the file to 
  12         [over]write. numSavedVersions tells how many of the most recent
  13         versions of pathname to save. """
  14         self._pathname = pathname
  15         self._tmpPathname = "%s.~new~" % self._pathname
  16         self._numSavedVersions = numSavedVersions
  17         self._outf = open(self._tmpPathname, "wb")
  18 
  19     def _ _del_ _(self):
  20         self.close(  )
  21 
  22     def close(self):
  23         if self._outf:
  24             self._outf.close(  )
  25             self._replaceCurrentFile(  )
  26             self._outf = None
  27 
  28     def asFile(self):
  29         """ Return self's shadowed file object, since marshal is
  30         pretty insistent on working with real file objects. """
  31         return self._outf
  32 
  33     def _ _getattr_ _(self, attr):
  34         """ Delegate most operations to self's open file object. """
  35         return getattr(self._outf, attr)
  36 
  37     def _replaceCurrentFile(self):
  38         """ Replace the current contents of self's named file. """
  39         self._backupCurrentFile(  )
  40         os.rename(self._tmpPathname, self._pathname)
  41 
  42     def _backupCurrentFile(self):
  43         """ Save a numbered backup of self's named file. """
  44         # If the file doesn't already exist, there's nothing to do
  45         if os.path.isfile(self._pathname):
  46             newName = self._versionedName(self._currentRevision(  ) + 1)
  47             os.rename(self._pathname, newName)
  48 
  49             # Maybe get rid of old versions
  50             if ((self._numSavedVersions is not None) and
  51                 (self._numSavedVersions > 0)):
  52                 self._deleteOldRevisions(  )
  53 
  54     def _versionedName(self, revision):
  55         """ Get self's pathname with a revision number appended. """
  56         return "%s.~%s~" % (self._pathname, revision)
  57 
  58     def _currentRevision(self):
  59         """ Get the revision number of self's largest existing backup. """
  60         revisions = [0] + self._revisions(  )
  61         return max(revisions)
  62 
  63     def _revisions(self):
  64         """ Get the revision numbers of all of self's backups. """
  65         revisions = []
  66         backupNames = glob.glob("%s.~[0-9]*~" % (self._pathname))
  67         for name in backupNames:
  68             try:
  69                 revision = int(string.split(name, "~")[-2])
  70                 revisions.append(revision)
  71             except ValueError:
  72                 # Some ~[0-9]*~ extensions may not be wholly numeric
  73                 pass
  74         revisions.sort(  )
  75         return revisions
  76 
  77     def _deleteOldRevisions(self):
  78         """ Delete old versions of self's file, so that at most
  79         self._numSavedVersions versions are retained. """
  80         revisions = self._revisions(  )
  81         revisionsToDelete = revisions[:-self._numSavedVersions]
  82         for revision in revisionsToDelete:
  83             pathname = self._versionedName(revision)
  84             if os.path.isfile(pathname):
  85                 os.remove(pathname)
  86 
  87 def main(  ):
  88     """ mainline module (for isolation testing) """
  89     basename = "TestFile.txt"
  90     if os.path.exists(basename):
  91         os.remove(basename)
  92     for i in range(10):
  93         outf = VersionedOutputFile(basename)
  94         outf.write("This is version %s.\n" % i)
  95         outf.close(  )
  96 
  97     # Now there should be just four versions of TestFile.txt:
  98     expectedSuffixes = ["", ".~7~", ".~8~", ".~9~"]
  99     expectedVersions = []
 100     for suffix in expectedSuffixes:
 101         expectedVersions.append("%s%s" % (basename, suffix))
 102     expectedVersions.sort(  )
 103     matchingFiles = glob.glob("%s*" % basename)
 104     matchingFiles.sort(  )
 105     for filename in matchingFiles:
 106         if filename not in expectedVersions:
 107             sys.stderr.write("Found unexpected file %s.\n" % filename)
 108         else:
 109             # Unit tests should clean up after themselves:
 110             os.remove(filename)
 111             expectedVersions.remove(filename)
 112     if expectedVersions:
 113         sys.stderr.write("Not found expected file")
 114         for ev in expectedVersions:
 115             sys.sdterr.write(' '+ev)
 116         sys.stderr.write('\n')
 117 
 118     # Finally, here's an example of how to use versioned
 119     # output files in concert with marshal:
 120     import marshal
 121 
 122     outf = VersionedOutputFile("marshal.dat")
 123     # Marshal out a sequence:
 124     marshal.dump([1, 2, 3], outf.asFile(  ))
 125     outf.close(  )
 126     os.remove("marshal.dat")
 127 
 128 if _ _name_ _ == "_ _main_ _":
 129     main(  )
 130 
 131 #!译注:好长,没看!

For a more lightweight, simpler approach to file versioning, see Recipe 4.26.

更轻量,简单的文件版本备份方法见食谱 4.26

1.1. 参考 See Also

Recipe 4.26 and Recipe 5.9;

documentation for the marshal module in the Library Reference.