🐈

【Python/C++】オブジェクトの状態をまるっとSAVEしたい!【シリアライズ】

2021/12/20に公開

シリアライズって何?

ゲームを中断したい場合って、セーブコマンドを選んでゲームの状態を保存して、次に遊びたいときは、ロードコマンドを選んで途中から再開できますよね。

┏━━━━━━━━┓
┃ → SAVE ┃
┃   LOAD ┃
┗━━━━━━━━┛

それ以外にも、復活の呪文をチラシの裏に書き留めておいて、次に遊びたいときに、その呪文を入力して続きを遊ぶというのもありますね!

ゆうて いみや おうきむ
こうほ りいゆ うじとり
やまあ きらぺ ぺぺぺぺ
ぺぺぺ ぺぺぺ ぺぺぺぺ
ぺぺぺ ぺぺぺ ぺぺぺぺ ぺぺ

同じことを、PythonやC++のクラスのオブジェクトに対してできると、計算した結果を保存しといて、また後で使うとか、他のマシンに持っていって使うなど、色々と便利です。このような、動作しているプログラムのデータをまるっと、ファイルに保存したり、ネットワークで送信したりする形式に変換することを、シリアライズ(Serialize)といいます。反対に、そのような形式になっているデータをプログラムで使用できるデータに復元することをデシリアライズ(Deserialize)といいます。

Pythonの場合は?

Pythonの場合は、pickle というモジュールを使って、酢漬けにして保存します。

今回は、このようなキャラクターのクラスを使います。

character.py
class Character:
    def __init__(self, name, lv, hp, mp):
        self.name = name
	self.lv = lv
	self.hp = hp
	self.mp = mp

    def showStatus(self):
        print(f"NAME:{self.name} / LV:{self.lv} / HP:{self.hp} / MP:{self.mp}")

シリアライズするには、pickle.dumpというメソッドを使います。
キャラクターの名前などを設定したオブジェクトを作り、そのオブジェクトをpickle.dumpを使ってファイルに保存します。

serialize.py
import pickle
from character import Character

chara = Character('もょもと', 48, 229, 0)

with open('pickled_chara.dump', 'wb') as f:
    pickle.dump(chara, f)

デシリアライズするには、pickle.loadというメソッドを使います。
さっきのファイルをpickle.loadを使って読み込み、もょもとオブジェクトをプログラム上で使えるように復元します。

deserialize.py
import pickle
from character import Character

with open('pickled_chara.dump', 'rb') as f:
    chara = pickle.load(f)

chara.showStatus()

pybind11の場合は?

pybind11は、C++とPythonの橋渡しをするライブラリで、C++で作ったコードをPythonでかんたんに動かすことができるようになります。くわしくは前に書いた記事を見てください。
https://zenn.dev/t_ibe/articles/400aa60a12434d

pybind11を使って、C++のクラスをPython上でシリアライズしたい場合は、このように書きます。

mychara.cpp
#include <iostream>
#include <string>
#include <pybind11/pybind11.h>
namespace py = pybind11;

class Character {
public:
    Character(const std::string& name, int lv, int hp, int mp)
        : name_{ name }, lv_{ lv }, hp_{ hp }, mp_{ mp } {}
    void showStatus() const {
        std::cout << "NAME:" << name_ << " / ";
        std::cout << "LV:" << lv_ << " / ";
        std::cout << "HP:" << hp_ << " / ";
        std::cout << "MP:" << mp_ << std::endl;
    }

    // シリアライズ
    py::tuple _getstate(const Character& chara) const {
        return py::make_tuple(chara.name_, chara.lv_, chara.hp_, chara.mp_);
    }

    // デシリアライズ
    Character _setstate(const py::tuple& params) {
        return Character(params[0].cast<std::string>(), params[1].cast<int>(),
            params[2].cast<int>(), params[3].cast<int>());
    }

private:
    std::string name_;
    int lv_;
    int hp_;
    int mp_;
};

PYBIND11_MODULE(mychara, m) {
    py::class_<Character>(m, "Character")
        .def(py::init<const std::string&, int, int, int>())
        .def("showStatus", &Character::showStatus)
        .def(py::pickle(&Character::_getstate, &Character::_setstate))
        ;
}

C++の場合は?

C++でシリアライズしたい場合は、いくつかライブラリがあります。

  • boost::serialization
    Boost c++ Libraries に含まれているシリアライズ ライブラリです。使用するにはライブラリをビルドする必要があります。2004年頃からあり、古いC++コンパイラでも使えます。
  • cereal
    C++11準拠の比較的新しいライブラリなので、とても使いやすいです。ヘッダーファイルのみで、インクルードすれば使用できます。バイナリ/XML/JSONなどに対応しています。

cerealの使い方

cerealの使い方について、かんたんに説明します。
対象のクラスに以下のような、serializeというメンバーテンプレート関数を作ります。関数内でシリアライズしたいメンバー変数をar(hogehoge)のように書いていくだけです。

character.hpp
#include <iostream>
#include <string>
#include <cereal/cereal.hpp>
#include <cereal/types/string.hpp>

class Character {
public:
    Character()
        : name_{}, lv_{ 0 }, hp_{ 0 }, mp_{ 0 } {}
    Character(const std::string& name, int lv, int hp, int mp)
        : name_{ name }, lv_{ lv }, hp_{ hp }, mp_{ mp } {}
    void showStatus() const {
        std::cout << "NAME:" << name_ << " / ";
        std::cout << "LV:" << lv_ << " / ";
        std::cout << "HP:" << hp_ << " / ";
        std::cout << "MP:" << mp_ << std::endl;
    }
    
    // シリアライズ/デシリアライズ
    template <class Archive>
    void serialize(Archive& ar) {
        ar(name_, lv_, hp_, mp_);
    }
    
private:
    std::string name_;
    int lv_;
    int hp_;
    int mp_;
};

シリアライズの例は↓こちらです。出力ファイルストリームをBinaryOutputArchiveのコンストラクタの引数にして、それのoperator()の引数に対象のオブジェクトを渡すだけです。例ではBinaryにしましたが、XMLやJSONの形式で出力することも出来ます。

serialize.cpp
#include "character.hpp"
#include <fstream>
#include <cereal/archives/binary.hpp>

int main() {
    Character chara("もょもと", 48, 229, 0);
    std::ofstream ofs("chara.bin", std::ios::binary);
    cereal::BinaryOutputArchive archive(ofs);
    archive(chara);
    return 0;
}

デシリアライズの例は↓こちらです。シリアライズの場合と同様に、入力ファイルストリームをBinaryInputArchiveのコンストラクタの引数にして、それのoperator()の引数に対象のオブジェクトを渡すだけです。

deserialize.cpp
#include "character.hpp"
#include <fstream>
#include <cereal/archives/binary.hpp>

int main() {
    std::ifstream ifs("chara.bin", std::ios::binary);
    cereal::BinaryInputArchive archive(ifs);
    Character chara;
    archive(chara);
    chara.showStatus();
    return 0;
}

おわりに

今回は、シリアライズ(pickle🥒とcereal🥣)について説明しました!

メンバー募集中です
アダコテックは上記のような画像処理技術を使って、大手メーカーの検査ラインを自動化するソフトウェアを開発している会社です。
機械学習や画像処理の内部ロジックに興味がある方、ご連絡下さい!
我々と一緒にモノづくりに革新を起こしましょう!

https://adacotech.co.jp/recruit/

Discussion