argparse嫌いのあなたへ贈るパッケージ「NAAM」

この記事は #kosen10s Advent Calendar 2日目の記事である。 昨日の記事はAllajahの「2017年にあったKosen10'sの動きまとめ」だ。

argparseを使いたくない

PythonでCLIなユーティリティを作るとき、コマンド引数のパースをする一番標準的な方法は argparse を使うことである。

しかし、私はPythonを始めたときからずっとargparseがどうにも好きになれない。ほんの数十行のお手軽スクリプトのために、数行から場合によっては10行以上のパース定義を書かなければならないからだ。こっちは --hoge 1234 みたいな引数をいい感じにパースしてほしいだけなのに、 add_argument 関数を大量のキーワード引数と共に仰々しく呼ばなければならない。しかもこのキーワード引数は使い方に慣れないとよく意味がわからない。結局、毎回argparseを使うごとにGoogleで検索するのがいつもの流れだった。

怒られるかもしれないが、つまるところ、argparseはPythonicではないと思っている。

代替ライブラリもcementなどが知られているが、どれも便利さやサブコマンドを考慮していて学習コストが重い。もうちょっといい感じにできないものか。

NAAM - No Argparse Any More

そういうわけで、関数定義を元にめっちゃいい感じに引数をパースしてくれるライブラリを作ってみた。その名も「NAAM - No Argparse Any More」だ。

PyPIにも公開したので、 pip install naam でインストールできる。Python 3.6で書いていて現状だとそれ以前のPython 3でも動くはず。ただしこれからType hintsまわりの実装をするためそのうちPython 3.6以降のみ動作対象になるだろう。

NAAMはとにかく使い方がシンプルで、引数をパースして渡して欲しい関数を naam.bind_args() でデコレートするだけだ。 例えば以下のような関数があったとする。

def hello(first_name, last_name=None):
    msg = 'Hello world! My name is %s.'
    if last_name is None:
        print(msg % first_name)
    else:
        print(msg % '{} {}'.format(first_name, last_name))

これは first_name を入れたHello Worldを表示し、もし last_name がNoneでなければフルネームで表示するというものだ。この関数をCLIツール化してみよう。

変更はimport文とデコレータと関数呼び出しを追加するだけである。空行を除いてDiffは実に3行しかない。

from naam import bind_args

@bind_args
def hello(first_name, last_name=None):
    msg = 'Hello world! My name is %s.'
    if last_name is None:
        print(msg % first_name)
    else:
        print(msg % '{} {}'.format(first_name, last_name))

hello()

このスクリプト(example/optional.py)を引数無しで実行すると、以下のような出力が出る。

$ python optional.py
Usage: optional.py [-l LAST_NAME | --last_name LAST_NAME] FIRST_NAME

おわかりだろうか。このUsageは完全に自動生成されている。キーワード引数はdefault値があることからoptionalな引数となっていることがわかる。

では名前だけ渡してみたらどうなるだろうか?

$ python optional.py Miku
Hello world! My name is Miku.

名前だけのHello Worldが表示される。

さらに、optionalな引数である LAST_NAME を渡してみる。

$ python optional.py --last_name Hatsune Miku
Hello world! My name is Miku Hatsune.

これはもちろん、短縮名でも可能だ。

$ python optional.py -l Hatsune Miku
Hello world! My name is Miku Hatsune.

ちなみに、頭文字が同じで短縮名がカブる場合は衝突を避けるようになっている。下のような関数 (example/duplicated.py)があった場合、

from naam import bind_args

@bind_args
def fn(name, lip=None, lap=None, loop=None):
    pass

fn()

以下のように表示される。

$ python duplicated.py
Usage: ./duplicated.py [-l LIP | --lip LIP] [-L LAP | --lap LAP] [-L2 LOOP | --loop LOOP] NAME

小文字→大文字→大文字+数字 という風に避けているが、これは好みの分かれるところでもあるし、デコレータに引数を渡すなどして動作を変えられるようにするかもしれない。

どうやって実装したか

一番キモなのは inspect.py だ。実行時にPythonコードをメタに解析するのに使われる標準ライブラリである。デコレータに渡ってきた関数を inspect.signature() に渡すと、その名の通り関数のシグネチャ情報が手に入る(もちろんアノテーション情報付き!)。そのシグネチャから引数を取り出し、順序有り引数と順序なし(optional)引数に分けた上で、短縮名の解決を行うと引数情報が完成する。最後に、実際に渡ってきた引数とこれを照らし合わせて実行するというのが全体の流れだ。

現在のコードはノリと勢いで書いたものなので決してエレガントではないが、 __init__.py に100行ちょっと書いた程度で軽く読めると思うので読みたい方はぜひ。

これからの展望

とりあえず動いたという程度なので、もうちょっとまともにしたい。(typedmarshalなどもっと先にエンハンスするべきものもあるが…)

  • シンプルなUsageだけでなく、読みやすいverboseなUsageを出す
  • Docstringをパースして説明文付きの丁寧なUsageを出す
  • --help に対応する
  • Type annotationを読んで適切にcastさせる
  • 値なしargument(フラグを立てるだけ)に対応する
  • サブコマンドに対応する

次回の担当は我らがでなり。お楽しみに!

聲の形 inner silence 観てきた

おととい(2017年6月24日)、立川シネマシティで『映画「聲の形」 inner silence 極音上映』を鑑賞した。本記事はその感想である。

「シネマシティ恒例の極音上映か〜」と思いそうなタイトルだが、今回は内容が特殊だ。

  • 音声トラックはパイロット音源に差し替え
  • セリフなし
  • 効果音なし

ここでのパイロット音源が「innner silence」というタイトルである。制作初期にコンセプト的に制作された音源を再構成し、Blu-ray版ディスクに新たに収録したものだそうだ。 私は限定版Blu-rayを予約して買ったのだがinner silenceは未視聴で、立川で上映されると聞いてからあえてそのまま視聴せずに当日を迎えた。

続きを読む

ぼくは感想文が書けない

立川シネマシティで『映画「聲の形」 inner silence 極音上映』(音声をパイロット版に差し替え、セリフ無し・効果音無しで上映したもの)を観た帰りに、とにかく感じたことを猛烈に誰かに伝えたくて、感想をしたためることにした。 これはまさしく「作品の感想文」を書くことだな―と頭のなかをよぎった瞬間、ひどく弱ってしまった。というのも、私は「読書感想文」みたいな類が全く書けたことがないからだ。

とはいえ、散りばめられた思いは筋道の通った文にしないと人に伝わらないと思っているので、聲の形の「感想文」は絶対完成させたい。感想文、ねぇ…

思い返すと、何人かにブログの文章を「面白い」とか「アツく書かれているから読んでて楽しい」とか褒めて頂いた事があった。 なんだ、ある程度の文章なら書けるらしいじゃん自分。では、「感想文」と「ブログの文章」は何が違うのだろう。同じ事なのに、前者と後者ではやる気が雲泥の差だ。

昔を思い出して考えてみる。

小学生の回想

担任の先生が、何かの本の感想文―いわゆる読書感想文―を書いてきましょうと言ったので、原稿用紙を4枚ほど持って帰った。4枚書ききる気はさらさらないが。 家に帰ってとりあえず用紙を広げる。本を置く。まあすでに読んだことのある本だから、内容はわかる。

続きを読む