この記事はJij Inc. Advent Calendar 2023の15日目の記事です。
株式会社JijのNY57です。
本記事ではPythonの引数に関する持論を展開したいと考えています。よろしくお願いします。
TL:DR
- キーワード専用引数は変更・削除される可能性のある引数に使うと良い
前提: 関数への値の渡し方
以下のように定義された関数を考えます。
def func(arg1, arg2, arg3):
...
この関数を利用する時、引数への値の渡し方は2パターン存在しています。
func(1, 2, 3) # 位置引数で指定する
func(arg1=1, arg2=2, arg3=3) # キーワード引数で指定する
ひとつはfunc([値], ...)
で指定する方法。もうひとつはfunc([引数名]=[値], ...)
で指定する方法です。本記事では前者を位置引数指定と呼び、後者をキーワード引数指定と呼ぶことにします。
前提: 引数の定義の仕方
Pythonでは関数の引数の定義の仕方が3パターン存在しています。もっとも一般的な定義の仕方は以下の通りです。
def func(arg1, arg2, arg3):
...
この場合、引数に値を渡す方法は位置引数指定・キーワード引数指定のどちらも許されています。
func(1, 2, 3) # OK!
func(arg1=1, arg2=2, arg3=3) # OK!
一方で位置引数指定のみ・キーワード引数のみを許すような定義方法も存在しています。
位置引数指定のみを許すようにしたい場合、次のように定義します。
def func_pos(pos1, pos2, pos3, /): # /より前の引数は位置引数指定のみを許す
...
上記のように定義することで引数pos1
, pos2
, pos3
は位置引数指定のみが許されるようになります。本記事ではこのような位置引数指定のみが許されている引数を位置専用引数と呼ぶことにします。
位置専用引数にキーワード引数指定で値を渡すと以下のようなエラーが出ます。
func_pos(1, 2, pos3=3)
""" 実行結果
TypeError: func() got some positional-only arguments passed as keyword arguments: 'pos3'
"""
同様にキーワード引数指定のみを許すようにしたい場合、次のように定義します。
def func_kw(*, kw1, kw2, kw3): # *より後の引数はキーワード引数指定のみを許す
...
上記のように定義することで引数kw1
, kw2
, kw3
はキーワード引数指定のみが許されるようになります。本記事ではこのようなキーワード引数指定のみが許されている引数をキーワード専用引数と呼ぶことにします。
キーワード専用引数に位置引数指定で値を渡すと以下のようなエラーが出ます。
func_kw(1, kw2=2, kw3=3)
""" 実行結果
TypeError: func() takes 0 positional arguments but 1 positional argument (and 2 keyword-only argument) were given
"""
キーワード専用引数のメリット
さて、ここからが本題です。自分がキーワード専用引数をおすすめしたい理由は、以下のような変更が生じた時に対処しやすいからです。
- 引数の削除
- 引数名の変更
これらの変更は破壊的な変更でありユーザーに影響を与える変更です。ですので、ここでいう「対処しやすい」とは「ユーザー与える影響を抑えつつ変更する方法がある」ことを指すものとします。
メリット: 引数の削除に対処しやすい
まず、通常の引数定義の場合を考えてみましょう。以下の定義から引数arg2
が不要になったので削除したいとします。
def func(arg1, arg2, arg3):
...
### ↓ `arg2`を削除 ↓ ###
def func(arg1, arg3):
...
このとき、安易にarg2
を削除してしまうと関数func
のユーザーは
func(1, 2, 3)
""" 実行結果
TypeError: func() takes 2 positional arguments but 3 were given
"""
func(arg1=1, arg2=2, arg3=3)
""" 実行結果
TypeError: func() got an unexpected keyword argument 'arg2'
"""
のようなTypeErrorが発生し始めてしまい困ってしまいます。全てのユーザーがリリースノートを読んだりErrorを読むに抵抗がないとは限りませんから、こういった変更には警告(Warning)を出す期間を設けたいものです。では、そのようなWarningを出す処理を書き加えることができるでしょうか?
def func(arg1, arg2=None, arg3):
if arg2 is not None:
warnings.warn("`arg2` is deprecated")
...
例えば、こんな定義をしてみてはどうでしょうか? 一見するとarg2
に値が渡されればWarningが出て、そうでなければデフォルト値のNoneが設定されてWarningが出ないように見えます。が、この定義ではSyntaxErrorが発生してしまいます。
SyntaxError: non-default argument follows default argument
arg3
にデフォルト値を設定できればSyntaxErrorを解決できますが、必ずしも設定できるとは限らない為、どうやらこの実装方法ではダメそうです。では、デフォルト値を設定しない場合はどうでしょう?
def func(arg1, arg2, arg3):
if arg2 is not None:
warnings.warn("`arg2` is deprecated")
...
この実装では、Warningを見たユーザーが引数arg2
を削除してしまうとTypeErrorが発生してしまいます。これではユーザーのコードをarg2
削除後の形に移行することができません。
このように通常の引数の定義の仕方ではWarningを出しつつ移行できる状態を作ることができないのです。
一方でキーワード専用引数の場合を考えてみます。この場合、以下のように変更することでWarningを出しつつ移行できる状態を作ることができます。
def func(*, arg1, arg2=None, arg3):
if arg2 is not None:
warnings.warn("`arg2` is deprecated")
...
一見すると、デフォルト値が設定されているarg2
が設定されていないarg3
より先に来ている為、SyntaxErrorが起こりそうですが、キーワード専用引数で指定している場合は問題にはなりません。そして、arg2
を使っているユーザーはWarningを受け取り、arg2
を削除できる状態になっています。
func(arg1=1, arg2=2, arg3=3) # Warningが発生する
func(arg1=1, arg3=3) # OK!
この意味で通常の引数定義よりキーワード専用引数のほうが引数の削除に対処しやすいと言えるでしょう。
メリット: 引数名の変更に対処しやすい
次は引数名arg2
をnew_arg2
に変更することを考えてみましょう。通常の引数定義の場合に安直に引数名を変更すると
def func(arg1, arg2, arg3):
...
### ↓ `arg2`を`new_arg2`へ変更 ↓ ###
def func(arg1, new_arg2, arg3):
...
ユーザーはその利用パターンに応じてTypeErrorが発生したりしなかったりします。
func(1, 2, 3) # OK!
func(arg1=1, arg2=2, arg3=3)
""" 実行結果
TypeError: func() got an unexpected keyword argument 'arg2'
"""
このTypeErrorを発生させずに、arg2
を使用しているユーザーにWarningを出しつつ、arg2
をnew_arg2
に移行可能な状態にする方法はありません。
一方でキーワード引数限定の場合は以下のように変更すればWarningを出しつつ移行できる状態を作ることができます。
def func(*, arg1, new_arg2=None, arg3, arg2=None):
if arg2 is not None:
warnings.warn("Please use `new_arg2` instead of `arg2`")
new_arg2 = arg2
elif new_arg2 is None:
raise TypeError("Please set a value for `new_arg2`")
...
この実装の場合、引数arg2
に与えられた値はnew_arg2
へと代入されるので、関数内部の変数名はnew_arg2
に移行してしまって問題ありません。その上でarg2
を使っている場合はWarningが表示され、ユーザーがキーワード引数名を変更中にうっかりarg2
とnew_arg2
の両方を設定し忘れた場合にはTypeErrorが出るようにできます。勿論、キーワード専用引数としている為、デフォルト値の設定の有無と前後によるSyntaxErrorは出ません。
func(arg1=1, arg2=2, arg3=3) # Warningが発生する
func(arg1=1, arg3=3) # TypeErrorが発生する
func(arg1=1, new_arg2=2, arg3=3) # OK!
この意味で通常の引数定義よりキーワード専用引数のほうが引数名の変更に対処しやすいと言えるでしょう。
位置専用引数なら引数名の変更はいつでも可能です。削除されない引数であれば位置専用引数を選択するのも良い案でしょう。
まとめ
キーワード専用引数を使うメリットは上記以外にもあると考えていますが、少なくとも上記で提示したメリットから言えるのは「変更・削除の可能性がある引数にはキーワード専用引数を使うと良い」ということです。
キーワード専用引数のメリットを活用してユーザーに優しい開発をしていきましょう。
最後に
\Rustエンジニア・数理最適化エンジニア募集中!/
株式会社Jijでは、数学や物理学のバックグラウンドを活かし、量子計算と数理最適化のフロンティアで活躍するRustエンジニア、数理最適化エンジニアを募集しています!
詳細は下記のリンクからご覧ください。皆さんのご応募をお待ちしております!
Rustエンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/51062
数理最適化エンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/75132