【Django】コマンドライン引数sys.argvとは？テスト実行時にテスト用Database設定にする方法

# 概要
Pythonでコマンドライン引数とはどんなものなのか、djangoでどこで使われているか、また、Djangoの`settings.py`で、コマンドライン引数を使ってテスト用の`DATABASE`設定を行う方法も紹介します。

# サンプルコード
実際に動きを見てからの方が早いと思うので、まずは以下のサンプルコード。

```sample-test.py
import sys

if __name__ == "__main__":
    try:
        print(f"sys.argv: {sys.argv}")
        if len(sys.argv) >= 3:
            arg1 = sys.argv[1]
            arg2 = sys.argv[2]
            print(f"引数1: {arg1}")
            print(f"引数2: {arg2}")
        else:
            print("引数が少なくとも2つ必要です。")
    except Exception as ex:
        print(ex)
```

実行結果は以下の通り。

```
sample-test.py abc def
```

↓

```
sys.argv: ['sample-test.py', 'abc', 'def']
引数1: abc
引数2: def
```

上記から分かる通り、コマンドライン引数に期待通りの数指定されていれば、正常に出力されます。

### `sys.argv`とは？
`sys.argv`とは、標準ライブラリ`sys`モジュールに含まれる変数で、コマンドライン引数を取得するためのリストです。

上述の通り、Pythonスクリプトを実行するときに、`python script.py arg1 arg2`のように、スクリプト名の後にスペースで区切られた引数を追加された際、この引数のリストを`sys.argv`に格納することができます。

# 引数を一つにすると...？
では引数を一つにしたらどうなるでしょうか。

```
sample-test.py abc
```

↓

```
sys.argv: ['sample-test.py', 'abc']
引数が少なくとも2つ必要です。
```

一つしか連携されなかった場合は、`if len(sys.argv) >= 3:`が`False`になるので、`else`で指定したプリント文が出力されます。

なぜ`3`以上にしているか、というと、上記デバッグ結果を見て分かる通り、`sys.argv`リストにはスクリプト名も格納されているためです。スクリプト名、引数１、引数２が入っているかどうか、を確認するために、`3`以上かどうかを判定しています。

## `if sys.argv[1] and sys.argv[2]:`でも良いのか？
ちなみに、私は最初以下でも可能と思っていました。
一見、以下の`IF`文でも「引数が2つあるかどうか」をチェックしているので問題なさそうに見えます。

```py
import sys

if __name__ == "__main__":
    try:
        print(f"sys.argv: {sys.argv}")
        if sys.argv[1] and sys.argv[2]: # ここを変えています
            arg1 = sys.argv[1]
            arg2 = sys.argv[2]
            print(f"引数1: {arg1}")
            print(f"引数2: {arg2}")
        else:
            print("引数が少なくとも2つ必要です。")
    except Exception as ex:
        print(ex)
```

しかし、これでは、たとえ引数が一つでも`"引数が少なくとも2つ必要です。"`が表示されず、代わりに`Exception`が拾われて`list index out of range`と表示されます。インデックスエラーですね。

なぜそうなるか、というと、`if sys.argv[1] and sys.argv[2]:`の場合は、`sys.argv[1]`と`sys.argv[2]`が存在するかつ空でない場合に`True`になるからです。しかし、そもそもこの条件式で使う引数が足りていないので、その時点でプログラムの実行が中断され、`try`ブロックの例外処理が行われます。その結果、`list index out of range`エラーになってしまいます。

端的に言えば「条件式が評価される前にエラーが発生している」ということですね...。

## Djangoの`manage.py`でも使われている
例えばどこで使われているか？というと、Djangoで一番身近なところで言えば`manage.py`です。

```manage.py
def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()
```

`python manage.py runserver`を実行すると、`sys.argv`は`['manage.py', 'runserver']`のようなリストになります。そしてこの引数に応じて、異なる処理を実行するのです。`runserver`コマンドは開発サーバーの起動、`makemigrations`コマンドはデータベースのマイグレーションファイルの作成、といった感じです。上述の`test`もですね。

関数が具体的にどんなことをしているのか？という詳細は以下にコードがあります。
[Code source de django.core.management](https://docs.djangoproject.com/fr/1.8/_modules/django/core/management/)

## Djangoの`settings.py`でテスト用Databaseを指定する

最後に。`sys.argv`を使うことで、Djangoでテスト実行時のみ、テスト用のDatabaseを指定することができます。

Djangoでは通常、テストファイルを実行するときに`python manage.py test xxx`(`xxx`は実行したいアプリケーション)といった形で`tests.py`を実行させます。つまりテスト実行時のコマンドライン引数には必ず`test`が入っていることになるので、`test`を指定した際にはテスト用のデータベース設定を見るように指定すれば良い、というわけです。

```settings_local.py
import sys
if 'test' in sys.argv:
    DATABASES['default'] = DATABASES['test'];

DATABASES = {
    # 本番用
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': DATABASE,
        'USER': USER,
        'PASSWORD': PASSWORD,
        'HOST': HOST,
        'PORT': '3306',
        'OPTIONS': {
            'init_command':
                'SET character_set_connection=utf8,collation_connection=utf8_unicode_ci'
        }
    },
    # テスト用
    'test': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'test_db',
        'USER': 'root',
        'PASSWORD': 'root_password',
        'HOST': 'mysql',
        'PORT': '3306',
        'OPTIONS': {
            'connect_timeout': 500000,
            'init_command':
            'SET character_set_connection=utf8,collation_connection=utf8_unicode_ci'
        }
    }
}
```

上記により、`DATABASES['test']`に対応する値が`DATABASES['default']`にコピーされます。

これはあくまでテスト実施時にテスト用のDBを設ける場合、という想定です（本番とローカルのDBを分ける、という意味ではありません）。
ローカル用と本番用、という意味では、基本的には`settings.py`を`settings_local.py`としてローカル環境で別々に設定し、環境変数や環境ごとの設定ファイルを読み込む方法などが推奨されると思います。