pythonでopenstructを作成。

pythonを使っていて、rubyのopenstructのような振る舞いをするオブジェクトが欲しくなりました。
具体的には以下のように振る舞って欲しいのです。

os = OpenStruct(x=10)
os.x # => 10
os.y = 20

(os.x, os.y) # => (10, 20)

collections.namedtupleは近いのですが微妙に違います。コンストラクタに割り当てる段階では全ての値を渡すようにしくないのです。collections.defaultdictが使えるかと思ったのですが、値の取得が失敗した際のデフォルト値が決められるだけのようです。*1調べて見たところ、標準ライブラリには含まれていないみたいだったので、実装してみました。

code

class OpenStruct(dict):
    def __init__(self, **kw):
        for k, v in kw.items():
            self.__dict__[k] = v

    def __getattr__(self, name):
        return self.get(name, None)

    def __setattr__(self, name, v):
        self.__dict__[name] = v

    def __str__(self):
        return str(self.__dict__)

そんなに難しいことはしてません。dictを継承して、__getattr__と__setattr__を再定義しているだけです。
これで冒頭の例が動くようになります。

拡張

もう少し機能を付け足したいと思いました。例えばファイル名を取得することを考えて見てください。ベースとなるディレクトリのパスが分かっている状態で、ファイル名だけを指定することで絶対パスを取得したいという場合があると思います。そのような際に、関数を渡せると便利なのではないかと思いました。

以下のような動作を期待しています。

import os.path
ose = OpenStructExtend(name= lambda x : os.path.join("/foo/bar/",x))

ose.name = "spam.py"
ose.name # => "/foo/bar/spam.py"

値を格納する際の関数を付加できると便利かなと。コードは以下のようになりました。

code2

class OpenStructExtend(dict):
    def __init__(self, **kw):
        funD = {}
        for k, v in kw.items():
            if callable(v):
                funD[k] = v
            else:
                self.__dict__[k] = v
        self.__dict__['setter_hooks'] = funD

    def __getattr__(self, name):
        return self.get(name, None)

    def __setattr__(self, name, v):
        fun = self.__dict__['setter_hooks'].get(name, None)
        self.__dict__[name] = v if fun is None else fun(v)

    def __str__(self):
        return str(self.__dict__)

*1:本当は定義できるかもしれない。分からない。