Python 3 Tutorial 第五堂(2)物件序列化


在〈Python 3 Tutorial 第二堂(1)Unicode 支援、基本 I/O〉中談過基本輸入輸出,除了利用基本 I/O,自行決定要保存的運算結果之外,如果想直接保存物件狀態,在下次重新執行程式時讀取以恢復運算時必要的資料,這類的技術稱為物件序列化(Object serialization),在 Python 中,提供標準模組 pickleshelve 等來進行這方面的支援。

使用 pickle

如果要序列化 Python 物件,使用 pickle 模組會是比較好的方式,pickle 會記錄已經序列化的物件,如果後續有物件參考到相同物件,才不會再度被序列化。pickle 可以序列化使用者自定義的類別及實例,在格式方面,pickle 格證向後相容於新的 Python 版本。

cPickle 模組則是用 C 實作的模組,介面上與 pickle 相同,速度在理想上可達 pickle 的 1000 倍。

來看看使用 pickle 的一些程式範例,這個範例也示範了實作永續機制時的一種模式,用來序列化 DVD 物件的狀態:

class DVD:
    def __init__(self, title, year=None,
        duration=None, director_id=None):
        self.title = title
        self.year = year
        self.duration = duration
        self.director_id = director_id
        self.filename = self.title.replace(' ', '_') + '.pkl'

    def check_filename(self, filename):
        if filename is not None:
            self.filename = filename

這個 DVD 物件有 titleyeardurationdirector_id 四個狀態,每個 DVD 物件會以 title 作主檔名,空白以底線取代,並加上 .pkl 副檔名進行儲存。接下來列出儲存物件的 save 方法:

def save(self, filename=None):
    self.check_filename(filename)
    fh = None
    try:
        data = (self.title, self.year, 
                self.duration, self.director_id)
        fh = open(self.filename, 'wb')
        pickle.dump(data, fh)
    except (EnvironmentError, pickle.PicklingError) as err:
        raise SaveError(str(err))
    finally:
        if fh is not None:
            fh.close()

最主要地,你要以 'wb' 模式開啟檔案,然後使用 pickle.dump 進行物件序列化。程式中也使用了〈Python 3 Tutorial 第五堂(1)Shit happens!〉中談到的 try...except...finally 等語法,作了一些例外處理與資源收尾的動作。

接下來列出載入檔案 load 方法定義:

def load(self, filename=None):
    self.check_filename(filename)
    fh = None
    try:
        fh = open(self.filename, 'rb')
        data = pickle.load(fh)
        (self.title, self.year, 
         self.duration, self.director_id) = data
    except (EnvironmentError, pickle.PicklingError) as err:
        raise LoadError(str(err))
    finally:
        ...

這次是讀取,因此你要用 'rb' 模式開啟檔案,然後使用 pickle.load 載入檔案。這個 DVD 物件可以這麼使用:

dvd1 = DVD('Python 3 Tutorial', 2016, 1, 'Justin Lin')
dvd1.save()

dvd2 = DVD('PyCon Tutorial')
dvd2.load()
print(dvd2)

使用 shelve

shelve 物件行為上像是字典的物件,值的部份可以是 pickle 模組可處理的 Python 物件。以下來看個實例,搭配 DAO 模式 來使用 shelve 模組的功能:

class DvdDao:
    def __init__(self, shelve_name):
        self.shelve_name = shelve_name

    def save(self, dvd):
        shelve_db = None
        try:
            shelve_db = shelve.open(self.shelve_name)
            shelve_db[dvd.title] = (dvd.year,
                dvd.duration, dvd.director_id)
            shelve_db.sync()
        finally:
            if shelve_db is not None:
                shelve_db.close()

save 方法中,主要是使用 shelve.open 來開啟永續化時的字典檔案,在指定鍵值之後,使用 sync 方法將資料從快取中寫回檔案。接下來列出的 DAO 方法實作也是類似的操作:

def all(self):
    shelve_db = None
    try:
        shelve_db = shelve.open(self.shelve_name)
        return [DVD(title, *shelve_db[title]) 
                for title in sorted(shelve_db, key=str.lower)]
    finally:
        if shelve_db is not None:
            shelve_db.close()
    return []

def load(self, title):
    shelve_db = None
    try:
        shelve_db = shelve.open(self.shelve_name)
        if title in shelve_db:
            return DVD(title, *shelve_db[title])
    finally:
        if shelve_db is not None:
            shelve_db.close()
    return None

def remove(self, title):
    shelve_db = None
    try:
        shelve_db = shelve.open(self.shelve_name)
        del shelve_db[title]
        shelve_db.sync()
    finally:
        if shelve_db is not None:
            shelve_db.close()

以下是個使用 DvdDao 的例子:

filename = 'dvd_library.slv'
dao = DvdDao(filename)
dvd1 = DVD('Python 2 Tutorial', 2013, 1, 'Justin Lin')
dvd2 = DVD('Python 3 Tutorial', 2016, 1, 'Justin Lin')
dao.save(dvd1)
dao.save(dvd2)
print(dao.all())
print(dao.load('Python 2 Tutorial'))
dao.remove('Python 3 Tutorial')
print dao.all()