不要なimportをlazyにしたい(ついでにちょっとしたsetupも呼びたい)

不要なimportをlazyにしたい場合がある。例えばdictknife.loadではフォーマットとしてjson,yaml,tomlをサポートしているのだけれど。 jsonだけで十分の処理でyamlやtomlをimportを行いたくない。

ふつうのlazy import

ふつうのlazy importとして関数の中でimportする方法がある。

foo.py

def use_json(data):
    import json
    return json.dumps(data)

例えばこういうコード(foo.py)は import foo されたタイミングではjsonがimportされずに、use_json()が使われたタイミングでimportされる。

reifyが便利

とは言え、関数の中で個別にimportしまくるというのも何だかそれはそれで微妙なのでもうちょっと楽な方法がないかなみたいな事を考えてみた。 reifyを使うと良い。

reifyというのはeval onceなpropertyのようなもの。一度実行した結果をキャッシュし続ける。

class reify(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped
        try:
            self.__doc__ = wrapped.__doc__
        except:
            pass

    def __get__(self, inst, objtype=None):
        if inst is None:
            return self
        val = self.wrapped(inst)
        setattr(inst, self.wrapped.__name__, val)
        return val

pyramidというweb frameworkで見つけてから、とても便利で気に入っている。なので個人的に至る所で使っている(とは言えさすがにreifyのためだけにpyramidに依存したくないという気持ちは強い)。

reifyを使ってlazy module

reifyを使ったlazy importは、仮想的なmoduleを作って上げる感じ。moduleと言っても単なるsingleton object。

class Loading:
    @reify
    def json(self):
        import json
        return json

    @reify
    def yaml(self):
        import yaml
        return yaml

    @reify
    def toml(self):
        import toml
        return toml


m = Loading()  # singleton

このようにしてあげると、m.jsonm.yaml とアクセスしたときにはじめてmoduleがimportされる。

利用例

例えば、以下の様なコードにすると、tomlを使った場合にはyaml,jsonがimportされない。

00lazy.py

def run(loading):
    import sys
    data = {"person": {"name": "foo", "age": 20}}
    loading.dump(data, sys.stdout)
    sys.stdout.write("\n")
    sys.stdout.flush()


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--format", default="json", choices=["json", "yaml", "toml"])

    args = parser.parse_args()
    loading = getattr(m, args.format)

    run(loading)

こういう感じ。

$ python 00lazy.py --format toml
[person]
age = 20
name = "foo"

補足1: setupも一緒に書けるので便利

例えば現状のyamlのoutputは以下の様になっている。

$ python 00lazy.py --format yaml
person: {age: 20, name: foo}

これを以下の様なコードを追加した後に実行する

--- 00lazy.py    2017-08-05 20:09:37.000000000 +0900
+++ 01lazy.py 2017-08-05 20:22:13.000000000 +0900
@@ -23,6 +23,13 @@
     @reify
     def yaml(self):
         import yaml
+        from functools import partial
+
+        class Dumper(yaml.Dumper):
+            def represent_mapping(self, tag, mapping, flow_style=False):
+                return super().represent_mapping(tag, mapping, flow_style=flow_style)
+
+        yaml.dump = partial(yaml.dump, Dumper=Dumper)
         return yaml
 
     @reify

defaultがblock styleになる。

$ python 01lazy.py --format yaml
person:
  age: 20
  name: foo

補足2: 本当に他のmoduleがimportされていないの?

importされているモジュールが何か確認する方法は幾つかある。例えば起動時に -v オプションをつけて結果をgrepしてみるだとか。

$ python -v 00lazy.py --format=toml 2>&1 | grep import | grep -P 'json|yaml|toml'
import 'toml' # <_frozen_importlib_external.SourceFileLoader object at 0x103210438>

個人的に作っているmoduleknifeというので可視化するのもありかも?

$ modulegraph --outfile=withtoml.dot 00lazy.py --format=toml
$ dot -Tpng withtoml.dot > withtoml.png

withtoml