5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jij Inc.Advent Calendar 2023

Day 15

[Python] キーワード専用引数をおすすめしたい

Last updated at Posted at 2023-12-15

この記事は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!

この意味で通常の引数定義よりキーワード専用引数のほうが引数の削除に対処しやすいと言えるでしょう。

メリット: 引数名の変更に対処しやすい

次は引数名arg2new_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を出しつつ、arg2new_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が表示され、ユーザーがキーワード引数名を変更中にうっかりarg2new_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

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?