こしあん
2022-03-26

argparseのハイフン置き換え仕様でハマった


3.5k{icon} {views}


argparseではハイフンのコマンドライン引数をアンダーバーに置き換える仕様があります。これはPythonの言語仕様の都合によるものですが、思わぬハマり方をしたので自分のメモ用に書いておきます。

argparseはハイフンをアンダーバーに置き換える

これが自分の半日を溶かしたPythonの💩仕様

こんなやり方でdictからargparseに値を代入させられるのは既に記事にしています。ここからが問題で、コマンドライン引数のオプションにハイフンが含まれると、Python側で勝手にアンダーバーに置き換える仕様があります。

参考:https://qiita.com/S-T/items/49febd9488a49efd8c9c

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--log-directory", type=str, default="log")
parser.add_argument("--data-directory", type=str, default="")

args = parser.parse_args()

print(args)

例えばこのようにコマンドライン引数では、ハイフンありでも、実行すれば

Namespace(batch_size=64, data_directory='', log_directory='log')

アンダーバーに置き換えられます。これはハイフンありの変数や属性をPythonが認識できないためです。

問題になるのは値の置き換え

これが問題になるのは値を置き換えるケースです。いくつかパターンがあります。

1つ目はローカルで完結するケース。argparseの値をもっと管理しやすいjsonやyamlで置き換えたい。

2つ目はAWSのSageMakerのようにクラウドとローカルで両方動かすケース。ローカルでテストして、クラウドで動かすようなケースです。SageMakerとローカルでは、データやログのディレクトリが異なり、それが環境変数に格納されている(SM_MODEL_DIR、SM_CHANNEL_TRAINING)ので、SageMaker(クラウド)特有の環境変数の面倒を見ないといけない場合です。このケースでは、何らかのローカルのデフォルト値をセットしておいて、クラウド特有の環境変数をtry~exceptでとってとれたらアップデートということをするのがまず考えられます。こっちのほうがハマったときに数倍面倒くさいです。

今回はわかりやすいように1番目のローカルで説明しましょう。

ダメなケース:dictのキーをハイフンにしてしまう

自分がハマったダメなケースです。コマンドライン引数にあわせてdictのキーをハイフンのままでアップデートしています。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--log-directory", type=str, default="log")
parser.add_argument("--data-directory", type=str, default="")

args = parser.parse_args()

update_values = {
    "batch-size": 128,
    "log-directory": "/opt/ml/output/log",
    "data-directory": "input_dir"
}

for k, v in update_values.items():
    args.__setattr__(k, v)

print(args)

珍妙なクリーチャーなDictが出来上がります。なんじゃこれ

Namespace(batch_size=64, data_directory='', log_directory='log', **{'batch-size': 128, 'data-directory': 'input_dir', 'log-directory': '/opt/ml/output/log'})

要するにアップデートしたのにアップデートされていないということです。これをSageMakerのTraining Jobの中でやられると大変デバッグに苦労します。

いいケース:可能ならコマンドライン引数を最初からアンダーバーにしてしまう解決法

dictのキーをアンダーバーにすればちゃんとアップデートできます。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--log-directory", type=str, default="log")
parser.add_argument("--data-directory", type=str, default="")

args = parser.parse_args()

update_values = {
    "batch_size": 128,
    "log_directory": "/opt/ml/output/log",
    "data_directory": "input_dir"
}

for k, v in update_values.items():
    args.__setattr__(k, v)

print(args)
# Namespace(batch_size=128, data_directory='input_dir', log_directory='/opt/ml/output/log')

ただこれだと紛らわしいですよね。全部自分で書いたコードで変更可能なら最初からアンダーバーに共通化してしまうのはどうでしょうか。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--batch_size", type=int, default=64)
parser.add_argument("--log_directory", type=str, default="log")
parser.add_argument("--data_directory", type=str, default="")

args = parser.parse_args()

update_values = {
    "batch_size": 128,
    "log_directory": "/opt/ml/output/log",
    "data_directory": "input_dir"
}

for k, v in update_values.items():
    args.__setattr__(k, v)

print(args)

個人的にはこれに落ち着きました。

結論

何が言いたかったかというと、argparseにこんな仕様があったのを知らずに、半日か1日溶かしたのが💩だったということです。



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です