係数やしきい値だけが違う関数をいくつも書く代わりに、functools.partialで一部の引数を先に指定できる。元の関数は一つに保ち、条件付きの呼び出し口を作る方法である。ただし、キーワードで固定した値は、呼び出し時に上書きできる。絶対に変更させたくない制約とは区別して使おう。
処理と条件の違いを分ける
例えば測定値を係数倍してオフセットを加える計算で、単位換算ごとに同じ式を複製すると、後から式を修正する場所が増える。計算関数は一つにし、使う係数だけを前もって指定しておけば、各処理の差が分かりやすくなる。
partialが返すものは呼び出し可能なオブジェクトである。呼び出すときに新しく渡した引数と、保存した引数を組み合わせて元の関数を実行する。計算結果を保存するキャッシュではないので、呼ぶたびに元の処理が動く。
係数を指定した呼び出し口を作る
example.py
from functools import partial
def convert(value, *, factor, offset=0):
return value * factor + offset
to_milli = partial(convert, factor=1000)
print(to_milli(1.5))
print(to_milli(1.5, factor=1))
print(list(map(to_milli, [1, 2, 3])))
assert to_milli(1.5) == 1500.0
assert to_milli(1.5, factor=1) == 1.5
assert to_milli.func is convert
assert to_milli.keywords == {"factor": 1000}
fixed_input = partial(convert, 1.5, factor=2)
assert fixed_input() == 3.0
try:
fixed_input(value=4)
except TypeError:
print("TypeError")
else:
raise AssertionError("The positional input was specified twice")
実行結果
1500.0
1.5
[1000, 2000, 3000]
TypeError
to_milliはfactor=1000を既定の条件として持つ。数値を一つ渡すだけで換算できるため、mapやコールバックなど、関数を別の処理へ渡す場面でも使いやすい。offsetは元の関数の既定値がそのまま使われる。
キーワードの固定は上書き可能である
to_milli(1.5, factor=1)では、呼び出し時のfactorが優先される。例では比較のために上書きしているが、to_milliという名前に対して通常とは違う換算になるため、利用者へ自由に渡すAPIでは注意したい。条件を変更させたくないなら、明示的なラッパーで受け入れる引数を限定する方法もある。
partialは権限や妥当性を強制する仕組みではない。保存したキーワードの辞書も確認できるため、内部状態を触れられない保護境界としては扱わない。よく使う条件を短く指定する道具として考えると、用途を誤りにくい。
位置引数は後から追加される
位置引数を先に渡した場合、呼び出し時の位置引数はその後ろへ付く。例のfixed_inputはconvertの最初のvalueを1.5で埋めている。その状態でvalueをキーワード指定すると、同じ引数への二重指定になりTypeErrorになる。
Python 3.12では、先頭の位置引数から順に指定する基本的なpartialを使う。新しいPythonで追加された機能を前提にせず、後ろの条件を指定したい場合はキーワードで渡せる関数設計にするとよい。キーワード専用引数との組合せも分かりやすい。
引数のオブジェクトは自動コピーされない
リストや辞書を先に渡すと、そのオブジェクトへの参照が保存される。後から元を変更した場合、部分適用した関数が見る内容も変わる。作成した瞬間のスナップショットが必要なら、必要な深さのコピーをしてから渡す。
同じ意味で、partialをループ中に作る方法はlambdaの遅延束縛を避ける助けになるが、可変オブジェクトの中身まで固定するものではない。保存される引数と、それが指すデータの寿命を分けて考えることが重要である。
名前と説明は必要に応じて補う
partialにはfunc、args、keywordsという属性があり、どの関数へ何を渡すかを確認できる。一方、通常のPython関数と同じように__name__や__doc__が自動で元の説明へそろうとは限らない。ログやヘルプで名前が必要なら、用途が伝わる名前を付けたラッパーも選択肢になる。
単に条件を数個指定するだけならpartialは簡潔である。入力検査、単位変換、ログ、例外処理などが増えてきたら、普通の関数へ戻す方が読みやすい場合もある。短い記法を維持することより、呼び出し側から処理の意味が分かることを優先しよう。
正常な呼び出し、キーワードの上書き、位置引数の二重指定を確かめれば、固定という言葉の範囲を理解しやすい。元関数を一つに保ちながら、どの条件を便利な既定値として提供するかを決めるために使うとよい。
動作確認と参考資料
掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。
関連するTips
- 引数の渡し間違いを防ぐ:キーワード専用引数で関数を設計する
- *argsと**kwargsで処理を包む:引数の転送と重複指定の落とし穴
- ループで作ったlambdaが全部同じ値を返す原因と直し方
- 同じ計算を繰り返さない:lru_cacheとキャッシュの消しどき
