[Python]キーワード引数の指定が要るとき、要らないとき

# 前書き
本記事は初版において誤った記述をしていた内容を書き直したものになります。
ご指摘頂いた@LouiS0616さんに感謝申し上げます🙏

# そもそも引数ってなに？
いや……聞かれなくても分かってるよって感じの質問ですけど、私は分かってなかったから表題のことが判別できなかったので、改めて整理してみます。

Python3で扱う「引数」というものには「実引数」と「仮引数」の2種類が存在します。[^1]
[実引数と仮引数の違いは何ですか?](https://docs.python.org/ja/3.5/faq/programming.html#faq-argument-vs-parameter)

[^1]: 大半のプログラミング言語に実引数と仮引数の概念が存在すると思いますけど、よく調べるのは大変だし本題から外れるので、とりあえずPython3だけに絞って話します。

先のリファレンスの項目を雑にまとめるとこんな感じになると思います。

| - | 実引数 | 仮引数 |
|:--------------  |:-----------------|:---------------------|
| どこに現れる？   | 関数を呼び出す側   | 関数を定義する側      |
| 何の情報を持つ？ | 具体的な引数の値   | 受け取れる引数の値の型 |
| 引数の名前は？   | 呼び出し側で決める | 定義側で決まっている   |

ちなみに、それぞれを英訳した場合、実引数はargument、仮引数はparameterという単語で表されるようです。日本語だと似てますけど、英語だと全然違う単語なんですね。

[argument](https://docs.python.org/ja/3/glossary.html#term-argument)

[parameter](https://docs.python.org/ja/3/glossary.html#term-parameter)

# 実引数の種類

上記リファレンス「argument」の項目を引用すると、下記2種類の渡し方をする引数のことを**キーワード引数**と呼ぶようです。

- キーワード指定を行って渡す引数
    - `complex(real=3, imag=5)` という渡し方。`3`および`5`がキーワード引数に該当。
-  `**`に続けた辞書形式で渡す引数
    -  `complex(**{'real': 3, 'imag': 5})`という渡し方。こちらも同じく`3`および`5`がキーワード引数に該当。

後者の渡し方のこと全然知らなかった。

逆に、キーワード引数以外の渡し方をする引数のことを**位置引数**と呼んでいます。具体的には下記の2種類の渡し方があると解説されています。

- キーワードなしで渡している引数
    - `complex(3, 5)`という渡し方における`3`と`5`
-  `*`に続けたiterable形式で渡している引数
    -  `complex(*(3, 5))`という渡し方においても同じく、`3`と`5`

こっちも後者の渡し方知りませんでした。

そして、**この引数にはキーワード引数を渡してくださいね OR 位置引数を渡してくださいね、ということを決めるのは仮引数の側**となります。従って、仮引数の種類を理解していけば表題の答えに繋がっていくでしょう。

# 仮引数の種類
## 可変長引数について
さっそく上記リファレンスの「parameter」の項目を読み込んでいきたいところなんですが、「可変長引数」の話が出てきます。
先に可変長引数について知っておかないと混乱するので、そちらについて理解していきましょう。

[Pythonの可変長引数（*args, **kwargs）の使い方](https://note.nkmk.me/python-args-kwargs-usage/)

（関数を定義するときに）仮引数を定義するにあたって、引数の名前の先頭に`*`もしくは`**`をつけると、その仮引数には**可変長引数**として実引数を渡すことが出来るようになります。
**何個でも好きな個数だけ引数を渡せる**のが可変長引数ですね。渡された引数の数値を全部足し上げるとかそういう関数で使い所がありそうです。
よく`*args`や`**kwargs`という名前で定義されていることの多いこれらの仮引数ですが、`*`や`**`部分が重要みたいです。そのあとに続く名前部分は`*hoge`とか何でも良いんですね。自由に名前をつけられるという点は他の一般的な引数と変わりません。

じゃあ、**アスタリスク一個と二個の違いって何なの？**ってところが気になってきます。
上記のページでも解説されていますが、これの違いは明確で、**一個だと引数をタプルで受け取る引数（可変長位置引数）になる**のに対して、**二個だと引数をdictで受け取る引数（可変長キーワード引数）になる**という違いになってきます。

## キーワード引数として渡すもの、渡さないもの
可変長引数について理解できたところで、ようやく上記リファレンスの「[parameter](https://docs.python.org/ja/3/glossary.html#term-parameter)」の項目を読み込んでいきましょう。
それによると、仮引数には次の5つの種類が存在することが分かります。

| 定義方法 | キーワード引数として渡せる | 位置引数として渡せる |
|:-:|:-:|:-:|
| 1番 | ⭕ | ⭕ |
| 2番 | ❌ | ⭕ |
| 3番 | ⭕ | ❌ |
| 4番 | ❌ | ⭕ + 可変長 |
| 5番 | ⭕ + 可変長 | ❌ |

1. def func(foo, bar=None): ... 
2. def func(posonly1, posonly2, /, positional_or_keyword): ...：`/`の前に定義されている引数。**その他、いくつかの組み込み関数**
3. def func(arg, \*, kw_only1, kw_only2): ...：可変長引数として定義した仮引数の後か、もしくは裸の`*`の後に定義されている引数
4. def func(\*args, \*\*kwargs): ...：これの`*args`部分
5. def func(\*args, \*\*kwargs): ...：これの`**kwargs`部分

さきほど確認した可変長位置引数は、位置引数と名前に付いているだけあってキーワード引数として渡すことはできません。
同様に可変長キーワード引数も、キーワード引数と名前に付いているだけあって位置引数として渡すことはできません。

加えて**注意しなければならないのが2番3番の定義方法**になります。
1番の引数はどちらの方法で渡してもエラーとかにはならないので注意しなくていいわけですが、これら2番3番の引数は位置引数として、もしくはキーワード引数としては**渡すことができずにエラーとなる**ので一層注意しなければならないということですね。[^2]

[^2]: 2番の「キーワード引数としては渡せない」パターンはpython3.8以降に追加されたようです。

# 呼び出し側の制約
これら仮引数側の定義方法を踏まえた上で、呼び出し側には更に次の制約が決められています。

[4.7.2. キーワード引数](https://docs.python.org/ja/3/tutorial/controlflow.html#keyword-arguments)

この中で特に注意が必要そうなのは、「キーワード引数を指定した後ろに位置引数を指定する」のはダメというところぐらいでしょうか。
呼び出す時に気をつけようね、ぐらいのものです。
位置引数とキーワード引数の前後関係さえ満たしていれば、複数のキーワード引数の順序は自由に指定できます。

# まとめ
表題の件についてまとめると、次のようになるかと思います。

1. 仮引数が可変長位置引数として定義されている場合、キーワード引数として実引数を渡すことはできない（当然）
2. 仮引数が可変長キーワード引数として定義されている場合、位置引数として実引数を渡すことは出来ない（これも当然）
3. **可変長引数、もしくは裸の\*のあとに定義されている仮引数には、必ずキーワード引数として実引数を渡さなければならない**
4. 実引数をキーワード引数として渡す場合、必ず位置引数の後ろの位置で渡さなければならない
5. **一部の組み込み関数や、`/`よりも前に定義されている仮引数には、必ず位置引数として実引数を渡さなければならない**
6. その他はどっちでも良い（はず）

# おまけ：なんでこんなこと書いたの
`dict`型には`.get()`というメソッドがあります。
[get(key[,default])](https://docs.python.org/ja/3/library/stdtypes.html#dict.get)

「dictの中から`key`に相当する値を取得したい。でも`key`が存在するかどうかはわからない。もし存在しなかったら*default*の値ということにしたい。」

そんな感じのときに使う関数です。
私はこれを`my_dict.get('hoge', default='bar')`のように呼び出して使ってしまったんですね。
こうするとターミナルに`TypeError: get() takes no keyword arguments`というエラーが出てきてしまいます。
こんなしょーもないことでハマってしまいました。

私が今回ハマった「位置引数として実引数を渡さなければならない」パターンは、上述のまとめの番号で言うと5番の「組み込み関数」パターンとして解釈できそうです。
このパターンは**たぶんリファレンスを見ても「キーワード引数として渡していいのか？ダメなのか？」ということがわからない**と思うので、**組み込み関数には、キーワード引数として渡せることが確定していない限り位置引数として実引数を渡す**という方針にするのが良さそうかなと思いました。

リファレンスに`/`を書いてくれたらちゃんとわかるからそうしてほしい～:sob: