【Python】*argsと**kwargsで処理を包む:引数の転送と重複指定の落とし穴

PythonのTopに戻る


元の関数へ引数をそのまま渡しながら既定の条件を追加したいときは、*argsと**kwargsを使う。ただし、同じキーワードを明示指定と辞書展開の両方から渡すとTypeErrorになる。先にオプション辞書を一つへまとめ、どちらを優先するかを決めてから転送するのが分かりやすい。

受け取る展開と渡す展開を区別する

関数定義の*valuesは、余った位置引数をタプルとして受け取る。**optionsは、余ったキーワード引数を辞書として受け取る。一方、呼び出し側の*valuesは中身を位置引数へ、**optionsはキーと値をキーワード引数へ展開する。同じ記号でも置く場所によって役割が異なる。

ここでは平均値を文字列にするsummarizeへ、詳しい表示用の既定値を追加する。元の計算関数は変更せず、呼び出し口だけをdetailという関数に分ける。呼び出し側がprecisionを指定した場合は、そちらを優先する約束にする。

既定値と個別指定を一つの辞書へまとめる

example.py

def summarize(*values, precision=2, label="mean"):
    if not values:
        raise ValueError("At least one value is required")
    result = sum(values) / len(values)
    return f"{label}={result:.{precision}f}"


def detail(*args, **kwargs):
    options = {"precision": 3, "label": "mean"}
    options.update(kwargs)
    return summarize(*args, **options)


print(detail(1, 2))
print(detail(1, 2, precision=1, label="average"))
assert detail(1, 2) == "mean=1.500"
assert detail(1, 2, precision=1) == "mean=1.5"

for call in (
    lambda: summarize(1, precision=3, **{"precision": 1}),
    lambda: detail(1, unknown=True),
):
    try:
        call()
    except TypeError:
        print("TypeError")
    else:
        raise AssertionError("Invalid keyword arguments were accepted")
try:
    detail()
except ValueError:
    pass
else:
    raise AssertionError("Empty input must fail")

実行結果

mean=1.500
average=1.5
TypeError
TypeError

options.update(kwargs)によって、同じキーは呼び出し側の指定へ置き換わる。逆に、強制したい条件があるなら適用順を変えるか、そもそも上書きを許可しない設計にする。どちらがよいかは用途によるため、順番を偶然に任せないことが重要である。

二重指定は上書きではなくエラーになる

summarize(*values, precision=3, **kwargs)と書き、kwargsにもprecisionがあると、関数へ同じ引数を二度渡すことになる。辞書のupdateとは違い、後ろのものが自動で勝つわけではない。例ではこの状況を意図的に作り、TypeErrorになることを確かめている。

同じ問題は二つの辞書を別々に**展開した場合にも起こる。先に辞書を結合する操作と、関数呼び出しのキーワード引数として展開する操作は規則が違う。関数へ渡す前に一つの設定を作ると、重複の扱いを明示できる。

未知の引数を黙って捨てない

ラッパーが**kwargsを受け取れるからといって、元の関数も何でも受け取れるわけではない。例のdetailへunknown=Trueを渡すと、summarize側でTypeErrorになる。この失敗を残すことで、オプション名の入力ミスに気付きやすくなる。

不要なキーを無条件に捨てる処理を入れると、precisionの綴りを間違えても既定値のまま正常終了してしまう。受け入れる引数を限定したい場合は明示的なシグネチャにするか、未知のキーを検出して説明付きのエラーにする方が安心である。

値を加工するなら単位と順序もそろえる

転送の前に位置引数を変換することもできるが、その場合は元関数が期待する単位や並びと合っているかを確認する。数値を千倍しているのにラベルだけ任意に変えられる設計では、値と表示が食い違う可能性がある。ラッパーの役割を一つに絞ると検証しやすい。

元の関数が入力を書き換えるかどうかも、そのまま利用側へ影響する。*で展開したから要素が深くコピーされるわけではない。辞書やリストを引数として渡すときは、ラッパーと元関数のどちらが所有・変更するのかを明確にしておく。

薄いラッパーに保つ

例のdetailは、既定値を作る、個別指定を重ねる、元関数へ返す、という三段階だけにしている。計算処理まで複製すると、後からsummarizeを修正しても片方だけ古いままになる。条件の差と実際の処理を分けることが、ラッパーを作る利点である。

正常な上書きだけでなく、入力なし、二重指定、未知のキーワードも試しておくと、どこで失敗するかを把握できる。デコレーターとして使い、関数名や説明文も引き継ぎたい場合は、functools.wrapsを使う方法も合わせて確認したい。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る