私が歌川です

@utgwkk が書いている

Ruby の dig メソッドが便利なので Python で実装した

dig メソッドとは

instance method Hash#dig (Ruby 2.6.0)

たとえば各種 API を叩いて返ってきた JSON をパースした後の辞書型のような、データがいくらでもネストしてあるようなデータ構造があるとき、普通なら obj['path']['to']['destination'][2] とか obj.path.to.destination[2] のようにしてアクセスしなければなりません。

この dig メソッドを使うと、先述したどちらの場合でも dig(obj, 'path', 'to', 'destination', 2) のような形式で要素にアクセスすることができます。また、該当する要素がない場合の挙動(例外か、それとも None を返す*1か)もメソッド側で制御することができます*2

残念ながらまだ存在する、データ構造を掘り進めた先にあるものが辞書型なのかオブジェクトなのか分からないようなインターフェース、それを気にせずに我々は掘り進めるだけでよいというのが、この dig の実装のポイントです。

実装

def dig(obj, *keys, error=True):
    keys = list(keys)
    if isinstance(keys[0], list):
        return dig(obj, *keys[0], error=error)

    if isinstance(obj, dict) and keys[0] in obj or \
       isinstance(obj, list) and keys[0] < len(obj):
        if len(keys) == 1:
            return obj[keys[0]]
        return dig(obj[keys[0]], *keys[1:], error=error)

    if hasattr(obj, keys[0]):
        if len(keys) == 1:
            return getattr(obj, keys[0])
        return dig(getattr(obj, keys[0]), *keys[1:], error=error)

    if error:
        raise KeyError(keys[0])

    return None

キーを用いて要素にアクセスするデータ構造(listdict や、それに類似したインタフェースを持つオブジェクト)だけでなく、普通のオブジェクトにも用いることができます。

class Hoge:
    def __init__(self, p, q, r):
        self.p = p
        self.q = q
        self.r = r


if __name__ == '__main__':
    h = { 'foo': { 'bar': { 'baz': 1 } } }
    print(dig(h, 'foo', 'bar', 'baz')) # => 1

    try:
        print(dig(h, 'foo', 'zot', 'xyz')) # => KeyError
    except KeyError as e:
        print(e) # => 'zot'

    print(dig(h, 'foo', 'zot', 'xyz', error=False)) # => None

    g = { 'foo': [10, 11, 12] }
    print(dig(g, 'foo', 1)) # => 11

    k = Hoge(1, 2, Hoge(Hoge(1, 2, 3), 4, 5))
    print(dig(k, 'r', 'p', 'p')) # => 1

    try:
        print(dig(k, 'r', 's', 'p')) # => KeyError
    except KeyError as e:
        print(e) # => 's'

このようにして用いることができます。こうして我々は掘り進めるだけでよくなります。よかったですね。

ところで、この実装にはまだ解決されていない問題があり、

hoge.method_one().method_two().destination

のようなメソッドチェーンには対応しておりません。それはまた別の話になりそうです*3

*1:個人的にこの挙動は好ましくないと考えています。なぜなら、返ってきた None が実際の要素なのか、要素が存在しなかったときの None なのか区別することができないからです。

*2:どちらも try-except でくくれば解決することではありますが……。

*3:そもそも Pythonic なのか? という議論も出てきそうなので、この辺で止めておきます。