tkherox blog

データサイエンスおよびソフトウェア開発、たまに育児についての話を書いています

CLIアプリケーションのためのTyper

Typerとは

TyperはCLIアプリケーションを作成するためのライブラリです.

github.com

Typerの特徴は大きく以下になります.

  • 直感的なコーディング
  • 使いやすさ
  • 少ない量のコーディング
  • 導入の簡単さ
  • 拡張性

省コストと拡張性を備えたライブラリで,シンプルで使いやすいが故に複雑なカスタマイズができないのかと思いきや,必要に応じてオプション引数やサブコマンドなどの複雑さを備えたCLIアプリケーションを作成したい場合にも対応できるようになっています.
現在の最新バージョンは 0.3.2 となっており,Pythonは3.6以上が対応している様です.より詳細な情報は公式ドキュメントを参照してみてください.

typer.tiangolo.com

インストール

実行環境は以下となっています.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G6042

$ python -V
Python 3.9.1

ライブラリのインストールはpipで簡単に行えます.

$ pip install typer

使い方・実装方法

早速Typerの実装方法を説明していきたいと思います.

CLI Arguments

最初はAugmentsによる引数の利用法です.
Typerでは以下の様に実装していきます. typer.runメソッドで引数に実行したい関数を指定してあげるだけです.その際に,実行する関数(main)で定義した引数(name)がコマンドラインで実行する時の引数かつ必須パラメータとなります.ちなみにドキュメントではType Hintsにて引数の型を指定していますが,この記載方法でなくても動作します.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import typer

def main(name: str):
    print(f"Hello {name}")

if __name__ == "__main__":
    typer.run(main)

早速実行してみましょう.
main.py を実行する際に与えた引数が内部で利用できていることが見てとれます.また,上記の実装方法でTyperを利用した場合は引数指定が必須となるため,引数を指定しないで実行した場合はエラーとなります.

# 引数を指定して実行
$ python main.py Japan
Hello Japan

# 引数を指定せずに実行
$ python main.py 
Usage: main.py [OPTIONS] NAME
Try 'main.py --help' for help.

Error: Missing argument 'NAME'.

引数にデフォルト値を設定する場合は以下の様にします.
Argument クラスを用いてインスタンス化する際にデフォルト値を指定した変数を引数に与えることで実現できます.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import typer

def main(name: str = typer.Argument("World", help="出力する文字列")):
    print(f"Hello {name}")

if __name__ == "__main__":
    typer.run(main)

先ほどは引数を与えない場合はエラーとなっていましたが,Argument にてデフォルト値を指定した場合はエラーにはならずデフォルトで指定した文字列が利用されています.上記例ではデフォルト値として World が設定されているので Hello World と出力されていることが見て取れますね.

# 引数を指定して実行
$ python main.py Japan
Hello Japan

# 引数を指定して実行
$ python main.py 
Hello World

Augumentには --help オプションにてコマンドの利用方法を表示するhelpテキストを記載することも可能です.--help オプションを表示してみると引数のhelpテキストが表示されていることが確認できます.

$ python main.py --help
Usage: main.py [OPTIONS] [NAME]

Arguments:
  [NAME]  出力する文字列  [default: World]

Options:
  --install-completion [bash|zsh|fish|powershell|pwsh]
                                  Install completion for the specified shell.
  --show-completion [bash|zsh|fish|powershell|pwsh]
                                  Show completion for the specified shell, to
                                  copy it or customize the installation.

  --help                          Show this message and exit.

CLI Options

続いてオプションを利用する方法について記載します.
オプションと引数の違いですがコマンドのオプションは「プログラムの動作を指定するもの」で引数は「動作の対象を指定するもの」と把握しておきましょう.
実装は先ほどの引数の場合には Augment クラスを利用してましたが,オプションの場合には Option クラスを利用するだけです.Augument と同様にデフォルトのパラメータを指定すること,helpテキストを定義することが可能です.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import typer

def main(
    name: str = typer.Argument("World", help="出力する文字列"),
    status: str = typer.Option("Great", help="状態を表す文字列"),
    age: int = typer.Option(None, help="年齢を表す数字")
    ):
    if age:
        print(f"Hello {name}! I am {age}")
    else:
        print(f"Hello {name}! I am {status}")

if __name__ == "__main__":
    typer.run(main)

上記のコードを実行した結果を確認してみます.
オプションを指定せずに実行した場合はデフォルトのパラメータが利用されるので,条件文の if ageelse の処理シーケンスに入るので Hello World! I am Great が出力されます.オプションを指定した場合は age20 が指定され, if ageTrue となるためHello World! I am 20 が出力されています.

# 引数の指定なし
$ python main.py 
Hello World! I am Great

# 引数指定なしでオプションあり
python main.py --age 20
Hello World! I am 20

併せて オプション のhelpテキストも確認してみましょう.
オプションに statusage がちゃんと追加されているのが見れました.

$ python main.py --help
Usage: main.py [OPTIONS] [NAME]

Arguments:
  [NAME]  出力する文字列  [default: World]

Options:
  --status TEXT                   状態を表す文字列  [default: Great]
  --age INTEGER                   年齢を表す数字
  --install-completion [bash|zsh|fish|powershell|pwsh]
                                  Install completion for the specified shell.
  --show-completion [bash|zsh|fish|powershell|pwsh]
                                  Show completion for the specified shell, to
                                  copy it or customize the installation.

  --help                          Show this message and exit.

また,オプションでは Option クラスの引数にpromtパラメータを指定してあげると,エラーを表示する代わりに不足しているパラメータをコマンドライン上で対話形式で入力することができます.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import typer

def main(
    name: str = typer.Argument("World", help="出力する文字列"),
    status: str = typer.Option("Great", help="状態を表す文字列"),
    age: int = typer.Option(None, help="年齢を表す数字", prompt="Please tell me your age")
    ):
    if age:
        print(f"Hello {name}! I am {age}")
    else:
        print(f"Hello {name}! I am {status}")

if __name__ == "__main__":
    typer.run(main)

自身が作成したCLIアプリケーションを別の人が利用する際に,アプリケーション側で対話形式での入力機能を持っていると入力するべきパラメータが分かりやすいので非常に有効なツールになると思います.

$ python main.py 
Please tell me your age: 12
Hello World! I am 12

SubCommand

サブコマンドの利用もTyperで実装可能です.
これまでの引数やオプションと違って Typer クラスを用いて,生成したインスタンスを使ってサブコマンドに利用したい命名と同じ名前で定義したメソッドに対してデコレータを付与することでサブコマンドが利用できます.
以下では preprocesstrainpostprocess のサブコマンドを定義しています.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import typer

app = typer.Typer()

@app.command()
def preprocess(subcommand: str = typer.Argument('preprocess')):
    print(f"select command '{subcommand}'")

@app.command()
def train(subcommand: str = typer.Argument('train')):
    print(f"select command '{subcommand}'")

@app.command()
def postprocess(subcommand: str = typer.Argument('postprocess')):
    print(f"select command '{subcommand}'")

if __name__ == "__main__":
    app()

3つのサブコマンドが定義できて利用できていることを実行して確認します.
サブコマンドとして train を指定したら select command 'train'が表示されております.サブコマンドと引数の組み合わせも簡単に実装できてますね.

$ python main.py train
select command 'train'

$ python main.py train test
select command 'test'

まとめ

今回は『CLIアプリケーションのためのTyper』というタイトルで引数やコマンドラインオプションなどを簡易に実装できるツールとしてTyperを実装例とあわせて紹介しました.
機械学習アドホックに分析する際にはJupyter Notebookを利用するため,あまりCLIアプリケーションと関わりがない人もおられると思います.一方で,分析した後のシステムへのインプリの段階では多分に漏れなくCLIなどのPythonコードを記述することになると思います.その際にTyperを用いることでコマンドラインで学習や推論と行った処理を分けるプログラムを簡単に実装できるのはありがたいですね.今後も積極的に活用して行きたいと思います.

参考資料