不要な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.json
や m.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
argparseでsubcommandを作るためのユーティリティ
サブコマンドを作る時に何らかのライブラリに依存して良いならclickがオススメではあるけれど。 使いたくない場合もあったりする。そういう時にどうするとまだましになるかみたいな事を考えたりしていた。
argparseでのサブコマンド
argparseでのサブコマンドの定義の仕方は以下のような感じ。
def foo(): print("foo") def bar(): print("bar") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="subcommand") subparsers.required = True foo_parser = subparsers.add_parser("foo") foo_parser.set_defaults(fn=foo) bar_parser = subparsers.add_parser("bar") bar_parser.set_defaults(fn=bar) args = parser.parse_args() args.fn()
こうするとfooとbarが使える
$ python 00subcommands.py -h usage: 00subcommands.py [-h] {foo,bar} ... positional arguments: {foo,bar} optional arguments: -h, --help show this help message and exit
引数の数が違う場合
引数の数が違う場合にちょっと大変になる。
例えばこういう風にfooではx,yでbarにはx,y,zの引数が用意されていた場合など。
foo_parser = subparsers.add_parser("foo") foo_parser.add_argument("-x") foo_parser.add_argument("-y") foo_parser.set_defaults(fn=foo) bar_parser = subparsers.add_parser("bar") bar_parser.add_argument("-x") bar_parser.add_argument("-y") bar_parser.add_argument("-z") bar_parser.set_defaults(fn=bar)
if文とかで頑張らないとだめ。
if args.fn == foo: return foo(args.x, args.y) elif args.fn == bar: return bar(args.x, args.y, args.z)
ユーティリティ
以下の様なユーティリティを書くとマシかもしれない。面倒なのは parser.parse_args()
で手に入った Namespace
オブジェクトの取扱い。
add_argumentで返されるactionのdestの値がシステム名的なものなのでそれを利用する関数を登録する。
クラスなどにしてラップしても良いけれど。contextmanagerを使うのが楽な気がした。
import contextlib @contextlib.contextmanager def subparser(subparsers, fn, *args, **kwargs): parser = subparsers.add_parser(fn.__name__, *args, **kwargs) dests = [] arrived = set() def add_argument(*args, **kwargs): ac = parser.add_argument(*args, **kwargs) if ac.dest not in arrived: arrived.add(ac.dest) dests.append(ac.dest) return ac yield add_argument def run(args): return fn(**{name: getattr(args, name) for name in dests}) parser.set_defaults(fn=run)
こんな感じで使う。
def foo(*, x, y): print("foo", x, y) def bar(*, x, y, z): print("bar", x, y, z) def main(): import argparse parser = argparse.ArgumentParser() parser.add_subparsers parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="subcommand") subparsers.required = True with subparser(subparsers, foo) as add_argument: add_argument("-x", default=None) add_argument("-y", default=None) with subparser(subparsers, bar) as add_argument: add_argument("-x", default=None) add_argument("-y", default=None) add_argument("-z", default=None) args = parser.parse_args() return args.fn(args)
$ python 02* foo -h usage: 02withutil.py foo [-h] [-x X] [-y Y] optional arguments: -h, --help show this help message and exit -x X -y Y $ python 02* bar -h usage: 02withutil.py bar [-h] [-x X] [-y Y] [-z Z] optional arguments: -h, --help show this help message and exit -x X -y Y -z Z # 引数多すぎ $ python 02* foo -x 10 -y 20 -z 30 usage: 02withutil.py [-h] {foo,bar} ... 02withutil.py: error: unrecognized arguments: -z 30 # ok $ python 02* foo -x 10 -y 20 foo 10 20
補足
foo,barの引数の定義にkeyword only argumentsを使うと良い。引数の数が会わない時のエラーがわかりやすいので。
例えば、fooにも -z
オプションを追加してしまった場合には以下の様になる。
$ python 02* foo Traceback (most recent call last): File "02withutil.py", line 56, in <module> main() File "02withutil.py", line 52, in main return args.fn(args) File "02withutil.py", line 20, in run return fn(**{name: getattr(args, name) for name in dests}) TypeError: foo() got an unexpected keyword argument 'z'
あー。zオプションなんて定義していなかったと気付ける。あと引数の順序を気にする必要がなくなるし。