「米国株売買シミュレーションで学ぶPythonプログラミング」の2回目です。まずはターミナルなどからコマンドを叩けるところまで目指すのですが、その前に使用するモジュールを説明していきます。今回は、コマンド実行時の引数をパースできるargparseの使い方を紹介します。
※当連載で扱うアプリのソースはGitHub上に公開してあります。 https://github.com/yusukemurayama/ppytrading
argparseについて
argparseはコマンドラインでスクリプトを叩いたときの引数をパースし、オブジェクトに値を格納するモジュールです。あらかじめパターンを決めておけば、「python3 sample.py foo bar baz
」や「python3 sample.py -f FOO --bar BAR
」のような色々なパターンのコマンドライン引数を解析することができます。
※argparseと同じようなモジュールにoptparseがあります。ただし、こちらは廃止予定とのことなので使わない方がいいでしょう。
基本的な使い方
それではargparseの使い方を紹介します。スクリプトを叩いたときの引数にハイフンがあるかどうかで若干書き方が変わってくるので、その2つを分けて説明します。
ハイフン付きの引数を受け取る
例えばpython3 argparse_samples.py --mode=1 --verbose
のような形です。argparseを使って、「mode」と「verbose」の値を取得してみます。
argparseを使うためには、モジュールをインポートしてから以下のようにArgumentParserのインスタンスを生成します。
import argparse
parser = argparse.ArgumentParser()
次は、生成したインスタンスに対してパースのパターンを追加していきます。
parser.add_argument('-m', '--mode', type=int, required=True)
parser.add_argument('-v', '--verbose', action='store_true')
add_argumentメソッドを2回実行して、それぞれ「mode」と「verbose」のためのパターンを登録しています。
add_argumentの最初の「引数名=」がない引数で、どのような形式で引数を受け取るかを指定しています。1行目は「-m」と「--mode」が与えられているので、スクリプト実行時に「-m 1」・「-m=2」・「--mode 3」・「--mode=4」のような形で引数を渡せるようになります。
「type=」は受け取る引数の型を指定しています。1行目では「type=int」なので、受け取った引数はint型になります。
「required=」は必須かどうかを決めています。1行目は「required=True」が指定されているので、「-m」か「--mode」の指定がないとusageが表示されて終了します。なお、requiredのデフォルトはFalseです。
「action=」は引数のアクションを決定しています。デフォルトは「action='store'」で、引数の値を格納します。2行目の「action='store_true'」は、「-v」か「--verbose」が指定されればTrue、されていなければFalseが格納されます。
パターンを登録したらparse_argsメソッドでパースします。
args = parser.parse_args()
パース後は、以下のように値を取り出すことができます。
# 「python3 sample.py -m 1 -v」のように実行した場合
print('mode: {}'.format(args.mode)) # mode: 1
print('mode type: {}'.format(type(args.mode))) # mode type: <class 'int'>
print('verbose: {}'.format(args.verbose)) # verbose: True
ちなみに、「--mode」を必須にしているので、指定されていない場合は以下のようにusageが表示されます。
$ python3 sample.py
usage: sample.py [-h] -m MODE [-v]
ハイフンがない引数を受け取る
argparseはハイフンなしの引数にも対応することができます。例えばpython3 argparse_samples.py a b c
のような形です。
ハイフンなしの引数は、以下のようにパターンを登録しておきます。
parser.add_argument('strings', type=str, nargs='+', metavar='STR')
add_argumentの1番目の引数(strings)はパターンに名前を付けています。引数で与えられた値を取り出すときはこの名前を指定します。また、後述のmetavarを指定していない場合はusageでも使われます。
「nargs=」は受け取る引数の数を指定しています。ここでは「nargs='+'」なので、1個以上の引数を受け取っています。他には「nargs='*'」で0個以上の引数を受け取ったり、「nargs=1」で1個だけ受けるなど色々なパターンを指定できます。
「metavar=」はusageで使うラベルを設定してます。ここでは「metavar='STR'」としているので、usageで「STR」というラベルが使われます。なお、metavarの指定がない場合は名前(この例ではstrings)が使われます。
# metavar='STR'の場合
usage: foo.py [-h] STR [STR ...]
# metavarの指定がない場合
usage: foo.py [-h] strings [strings ...]
引数のパターンを登録したら、後はハイフンがある場合と同じように格納された値を参照することができます。
# 「python3 sample.py a b c」のように実行した場合
args = parser.parse_args()
print('strings: {}'.format(args.strings)) # strings: ['a', 'b', 'c']
スクリプト起動時の引数以外をパースする
argparseは、スクリプトを叩いたときの引数以外をパースすることができます。その場合は、parse_argsの引数にlist型オブジェクトを渡してあげます。
以下の例ではparse_argsの引数に「aaa」と「bbb」という文字列を渡しています。
li = ['aaa', 'bbb']
parser = argparse.ArgumentParser()
parser.add_argument('strings', type=str, nargs='*', metavar='STR')
args = parser.parse_args(li)
print('strings: {}'.format(args.strings)) # strings: ['aaa', 'bbb']
ハイフンあり・なし混在
ハイフンあり・なしが混在しているケースをパースしてみます。また、1つ目のハイフンなし引数をコマンド名として扱うようにします。
「基本的な使い方」と同様にパースする
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('command', type=str, nargs=1)
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-m', '--mode', type=int, required=True)
parser.add_argument('strings', type=str, nargs='*', metavar='STR')
args = parser.parse_args()
print('command: {}'.format(args.command[0]))
print('verbose: {}'.format(args.verbose))
print('mode: {}'.format(args.mode))
print('strings: {}'.format(args.strings))
この登録パターンでも大抵はうまく動作します。
$ python3 argparse_samples.py update_data foo bar -v -m 1
command: update_data
verbose: True
mode: 1
strings: ['foo', 'bar']
$ python3 argparse_samples.py -v update_data foo bar -m 1
command: update_data
verbose: True
mode: 1
strings: ['foo', 'bar']
ただ、以下のように、コマンド名の後にハイフンありの引数が来て、その後にハイフンなしの引数を指定した場合はうまくいきません。
$ python3 argparse_samples.py -v update_data -m 1 foo bar
usage: argparse_samples.py [-h] [-v] -m MODE command [STR [STR ...]]
最終形としてこの問題を解決します。
失敗したケースに対応する
先ほどの失敗したケースは、例えばargparseを2回使えば対応することができます。argparseを2回使う流れは以下のようになります。
- argparse(1回目)を使い、スクリプト実行時の引数からコマンド名を取得する。
- 同時に、(2回目のargparse用に)コマンド名を取り除いた引数を組み立てる。
- argparse(2回目)を使い、コマンド名を取り除いた引数をパースする。
それでは、1回目のargparseで「コマンド名」と「コマンド名を取り除いた引数」に分割してみます。コードは以下のようになります。
import sys
import argparse
command, new_args = None, []
parser = argparse.ArgumentParser()
parser.add_argument('command', type=str, nargs=1)
for args in parser.parse_known_args():
if hasattr(args, 'command'):
command = args.command[0]
continue
new_args.extend(args)
なお、上記コードでは今まで使っていたparse_argsの代わりにparse_known_argsを使っています。parse_known_argsを使うことで、未知の引数があった場合にエラーとならないようにしています。1回目ではコマンド名以外のパターンを登録していないので、それ以外はパターンに無い引数として扱われるので、対応しておく必要があります。
「コマンド名」と「コマンド名以外の引数」に分けたら、argparseを使って2回目のパースを実施します。2回目は今まで通りにパターンを登録し、parse_argsに「コマンド名以外の引数」を指定しています。
parser = argparse.ArgumentParser(prog='{} {}'.format(sys.argv[0], command))
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-m', '--mode', type=int, required=True)
parser.add_argument('strings', type=str, nargs='*', metavar='STR')
args = parser.parse_args(new_args)
なお、ArgumentParserのprogはusage表示のために指定しています。指定している文字列は「スクリプト名 コマンド名」です。これを指定しないと、引数にコマンド名がないので、usageでコマンド名が表示されなくなってしまいます。
これで、python3 argparse_samples.py -v update_data -m 1 foo bar
のような形式でも実行できるようになりました。
print('command: {}'.format(command)) # command: update_data
print('verbose: {}'.format(args.verbose)) # verbose: True
print('mode: {}'.format(args.mode)) # mode: 1
print('strings: {}'.format(args.strings)) # strings: ['foo', 'bar']
argparseのサンプル・テストケース
今回使ったサンプルと、そのテストケースを掲載しておきます。