はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pythonのネストされたdictに「a.b.c.d」のようなJavaScript風のアクセスを行う小品のスニペット例 2nd

下記の記事の続きです。自分でも続きがあったのかと思っておりますが、今回は、前回と同じ要領で、あるdictについて、特定のプロパティ(複数可能)を抜き出したdictを得るというミニDSL風の関数を作成しました(以下 hoge2.py)。

itdepends.hateblo.jp

なお、hoge2.pyを作成してて、もう一声加えるとPython 3.9で導入されるネストされたdictのマージ演算子相当に近づけられそうだったので、これも自分メモ・活動の記録としておまけでつけてあります(以下hoge_merge.py)。

■(参考リンク)Python3.9のdictに関するマージ演算子「|」

www.python.jp

抜き出し

■hoge2.py(Pythonです)

selectfieldsというのが該当のもの。 なお、selectfieldsの第2引数で、抜き出したいプロパティを示すリストを指定するが、お行儀の良いものがお行儀の良い順で指定されることとする。例えば、「a.b.c」の後に「a.b」が指定される場合は、「a.b」が抜き出されてしまうことから、「a.b.f」などが成り行き保持されてしまい、「a.b.c」のみ選択したいという指定は機能しない。

dx = {
    "a": {"b": {"c": "あああ", "d": ["いいい"], "f": "えええ"}},
    "x": "ううう"
}


def selectfields(origobj, _paths):
    paths = copy.deepcopy(_paths) #初出時「_pathsを破壊的に動作する挙動」であったが、破壊しないように見直し(2020/12/1)
    obj_ = {}

    def _setobj(obj, path, up=[]):
        c = path.pop(0)
        p = up + [c]
        if len(path) == 0:
            #findfield関数は前回の記事のものと同じ
            if _ := findfield(origobj, p):
                obj[c] = _
            return
        if c not in obj.keys():
            obj[c] = {}
        _setobj(obj[c], path, p)

    for path in paths:
        _setobj(obj_, path)

    return obj_

# dxのa.b.d、a.b.fを抜き出す。ちなみに、「a.x」は存在しないので、空振りする。
dx_ = selectfields(
    dx, ['a.x'.split('.'), 'a.b.d'.split('.'), 'a.b.f'.split('.')])
print(dx_)

■hoge2.pyの実行結果

{'a': {'b': {'d': ['いいい'], 'f': 'えええ'}}}

マージ

getpathsで、あるdictのすべてのプロパティを「a.b.c〜」のリストで取得し、先述の「_setobj」を使って、マージ先のdictにコピーする(merge関数)。

hoge_merge.py



def getpaths(obj):
    pathslist = []

    def _getpaths(obj, up=[]):
        for k in obj.keys():
            p = up + [k]
            if isinstance(obj[k], dict):
                # dictなら掘り下げる
                _getpaths(obj[k], p)
            else:
                # 行き止まりであれば、pathを表すリストを出力する
                pathslist.append(p)

    _getpaths(obj)

    return pathslist


print("getpathsの挙動")
print(getpaths(dx))


def merge(obj1, obj2):
    obj_ = copy.deepcopy(obj1)
    origobj = obj2

    # この_setobjは上述の「hoge2.py」のものと同じ
    def _setobj(obj, path, up=[]):
        c = path.pop(0)
        p = up + [c]
        if len(path) == 0:
            if _ := findfield(origobj, p):
                obj[c] = _
            return
        if c not in obj.keys():

            obj[c] = {}
        _setobj(obj[c], path, p)

    for path in getpaths(obj2):
        _setobj(obj_, path)

    return obj_



dy = {"a": {"y": "わい"}, "z": "ぜっと"}

dx = {
    "a": {"b": {"c": "あああ", "d": ["いいい"], "f": "えええ", "g": {"h": "えいち"}}},
    "x": "ううう"
}

print("merge(dy, dx)の挙動")
print(merge(dy, dx))

hoge_merge.pyの実行結果

getpathsの挙動
[['a', 'b', 'c'], ['a', 'b', 'd'], ['a', 'b', 'f'], ['a', 'b', 'g', 'h'], ['x']]
merge(dy, dx)の挙動
{'a': {'y': 'わい', 'b': {'c': 'あああ', 'd': ['いいい'], 'f': 'えええ', 'g': {'h': 'えいち'}}}, 'z': 'ぜっと', 'x': 'ううう'}

テストパターンが少ないので見逃しがあるかもしれない*1。 また、今回やりたかったことについては、問題をうまく分解できた気がしていますが、一方、効率的かというとそうではないかもしれません。

*1:もともと割り切り仕様になっているところはありますし...