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