argparseはデフォルト引数を受け取ったのか？【改訂版】

記事内容を全面的に書き直しました。
## 概要
`argparse`を使ったことはあるでしょうか？python標準で組み込まれているコマンドラインパーサですが、今回は解りにくい`argparse`をさらにわかりにくく使ってみる実験です。

### まずそのままパースしてみる
まず、今回の例に使う基本のコードを示します。コマンドラインからオプション引数`-A`と`-B`を受け取るだけの単純なコードです。
```python
import argparse

perser = parser = argparse.ArgumentParser()
parser.add_argument('-A', default='A') # -A のデフォルトは A
parser.add_argument('-B', default='B') # -B のデフォルトは B

ns = parser.parse_args()
print(ns)
```
特におかしなことはなく、引数があれば`Namespace`に反映して、なければデフォルト値を返します。
```sh
$ python parse1.py -A X -B Y # 指定した引数が表示される
Namespace(A='X', B='Y')
$ python parse1.py # 引数がないのでデフォルト値が設定される
Namespace(A='A', B='B')
$ python parse1.py -A A -B B # デフォルト値と同じ引数
Namespace(A='A', B='B')
```
3回目の実行では引数がデフォルト値と同じなので、`parse_args`が返した結果からは実際に引数が渡されていたのか判別できません。当たり前です。それを **知る必要がある値ならばデフォルト値を持つべきではないでしょう。**
しかし、それでもそれを知りたいケースが出てきたので実験してみました。

### Actionの挙動から判別する
`Action`クラスを自作して、`Action.__call__`の呼出しから引数の有無を出力させてみました。
```python
import argparse
# カスタムアクションを定義
class ActionTester(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        print(f'call {self.dest}:{values}') # __call__が呼ばれたことを知らせる
        setattr(namespace, self.dest, values)

perser = parser = argparse.ArgumentParser()
parser.add_argument('-A', default='A', action=ActionTester) # -A のデフォルトは A
parser.add_argument('-B', default='B', action=ActionTester) # -B のデフォルトは B

ns = parser.parse_args()
print(ns)
```
引数`-A`だけを与えたところ、`-A`に対する`__call__`のみが呼び出されました。これで引数が渡されない場合は`Action.__call__`が呼ばれないことが解りました。[^init]
```sh
$ python parse2.py -A A
call A:A
Namespace(A='A', B='B')
```
### 2回パースしてしまう
しかし、`Action.__call__`内では判別した結果を残しづらく、まだ他の引数のパースも終わっていません。もっと解りやすい方法を考えた結果、2回パースする方法に辿り着きました。
```python
import argparse

# -Bの存在を先に確認する
parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument('-B', nargs='?') # 値が不正でもエラーは起こさない
ns, other = parser1.parse_known_args()

# -B が指定されたかここで判る
print(ns)

# もう一度同じ引数をパースする
parser2 = argparse.ArgumentParser() 
parser2.add_argument('-A', default='A') # -A のデフォルトは A
parser2.add_argument('-B', default='B') # -B のデフォルトは B

ns = parser2.parse_args()
print(ns)
```
1回目の`parse_known_args`で`-B`にデフォルト値を指定せず。ここで実際に指定されたかどうかが解ります。`-B`が指定されていない場合は続く`parse_args`でデフォルト値が設定されます。以下のようにパースの途中で`-B`の存在が確認されます。
```sh
$ python parse4.py -A A
Namespace(B=None)
Namespace(A='A', B='B')
```
若干注意が要るのは、`parse_known_args`時点でパースエラーを起こしてしまうとエラーメッセージが後続の`parse_args`と違うものになってしまうので、パーサを`parse_args`に使う時より緩く設定します。さらに、`parse_known_args`が返した`Namespace`を`parse_args`に引き継いでしまうとデフォルト値が設定されないので、新しい`Namespace`を受け取るようにします。これで実験としては成功と言えるでしょう。

### これが必要になった経緯
この時書いていたプログラムは、プログラムの動作設定をコマンドライン以外にも設定ファイルから取得する。という動作がありました。
- コマンドラインで直接指定された値が最優先
- 設定ファイルに書き込まれた内容で値を補完する
- コマンドラインにも設定ファイルにも書かれていない値はデフォルト値を適用する
という、ややこしいものでしたが実装に挑戦してみたわけです。

```python
import argparse

# -Bの存在を先に確認する
parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument('-B', nargs='?') # 値が不正でもエラーは起こさない
ns, other = parser1.parse_known_args()

# B が X なら A のデフォルト値はXXXにする
ns2 = argparse.Namespace()
if ns.B is not None and ns.B == 'X':
    # ここで設定ファイルから値を取り込む（イメージ）
    setattr(ns2, 'A', 'XXX')

# もう一度同じ引数をパースする
parser2 = argparse.ArgumentParser() 
parser2.add_argument('-A', default='A') # -A のデフォルトは A
parser2.add_argument('-B', default='B') # -B のデフォルトは B

ns2 = parser2.parse_args(namespace=ns2)
print(ns2)
```
実行するとこうなります。
```sh
$ python parse5.py
Namespace(A='A', B='B')
$ python parse5.py  -A A -B B
Namespace(A='A', B='B')
 python parse5.py  -A AAA
Namespace(A='AAA', B='B')
$ python parse5.py  -B X # この場合だけAを書き換える
Namespace(A='XXX', B='X')
```
`Namespace`を複数作り直しているのがポイントです。これで目的は達成できたのですが、 `argparse`に頼り過ぎたせいで遠回りなコードになっている感は否めません。

[^init]: なお`Action.__init__`は常に呼び出されるようです。