
【 Effective Python, 2nd Edition 】keyword-only arguments (キーワード指定のみ引数) と positional-only arguments (位置指定のみ引数) を使いこなして、コードの読解性と将来的な拡張性を高めよう! 投稿一覧へ戻る
Published 2020年6月22日20:28 by T.Tsuyoshi
ここで、割り算の答えを返す単純な関数を考えてみましょう。
ただしこの関数では、設定によって、OverflowError 例外が発生した場合でも代わりに 0 を返し、
ZeroDivisionError 例外が発生した場合でも代わりに infinity (無限) を返すことができるようにしましょう。
try:
return dividend / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
OverflowError 例外を無視するようにこの関数を呼び出します。
print(result) # 0
次は ZeroDivisionError 例外を無視するように呼び出してみます。
print(result) # inf
期待通りの動作ですが、ちょっと問題があります。どちらの Boolean 引数がどちらの例外を無視するものなのか、を混同しやすい、ということです。
この例では、3番目と4番目の位置引数として単純に True や False を渡していますが、もし勘違いして値を入れ替えて渡してしまったとしてもプログラムは正常に動作してしまうでしょう。
この種のバグはとにかく見つけ辛いです。ゲッソリやつれます。
そこで、関数側でこの2つのパラメータにデフォルト値を設定し、オプションパラメータとして扱うことにしましょう。
try:
return dividend / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
そして、呼び出す側では、無視したい例外を指定する場合にはキーワード引数で指定するようにします。
print(result) # 0
result = safer_division(1.0, 0, ignore_zero_division=True)
print(result) # inf
一安心ですね、ホッ!
いえいえ、まだまだ安心はできません。
関数側でこのような実装をしたとしても、オプションパラメータに値を指定する場合はキーワード引数で、ということを呼び出し側に強制できるわけではありません。
今までどおり位置引数で渡すこともできちゃいます。
そこで、キーワード引数でなければこれらのオプションパラメータに値を渡せないようにしちゃいましょう、そうしましょう。
try:
return dividend / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
パラメータリストに * を追加しました。
パラメータリスト中の * シンボルは、「これ以降のパラメータはキーワード引数のみ受け付けます」という境界線を作り出します。
ですから、3番目と4番目の引数を位置引数として渡すことはできなくなります。
# TypeError: safest_division() takes 2 positional arguments but 4 were given
もちろん、キーワード引数として渡した場合は期待通りに動作します。
assert result == float('inf')
該当すればちゃんと例外も投げてきます。
result = safest_division(1.0, 0)
except ZeroDivisionError:
print('Woohoo!')
# Woohoo!
一件落着!?ところが、残念ながら話はまだ続きます。
呼び出し側で、最初の2つの引数、もしくは、どちらか一方をキーワード引数で渡していたとします。
assert safest_division(divisor=5, dividend=2) == 0.4
assert safest_division(2, divisor=5) == 0.4
さて、この関数の作成者の気が変わって、パラメータの名前を変更しちゃいました。
try:
return numerator / denominator
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
このちょっとした表面的な変更は、それまでの division、divisor パラメータにキーワード引数で値を渡していた全ての呼び出し元を機能させなくする、という大問題を引き起こします。
# TypeError: safest_division_changed() got an unexpected keyword argument 'dividend'
そこで、Python 3.8 では、将来的にこのような変更があることも考慮して positional-only arguments (位置指定のみ引数) という考えが採用されました。
これは先ほどまで見てきた keyword-only arguments (キーワード指定のみ引数) の対極に位置するものです。
try:
return numerator / denominator
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
パラメータリスト中の / シンボルは、「ここまでの引数は位置引数としてしか値を渡せませんよ」ということを意味しています。
ですが、キーワード引数として値を渡そうとすると...
# TypeError: safest_division_changed_positional() got some positional-only arguments passed as keyword arguments: 'denominator'
ここまでで、前半2つのパラメータに対しては位置引数でしか値を渡せないので、将来的なパラメータ名の変更が可能となり、
後半2つのパラメータに対してはキーワード引数でしか値を渡せないので、コードの読解性を高め、引数の渡し間違いによるバグ発生を軽減できるようになりました。
最後にもう1つだけ。
パラメータリスト内の / シンボルと * シンボルの間に置かれたパラメータに対しては、Python のデフォルトの動作と同様に、位置引数としてもキーワード引数としても値を渡せます。
割り算の結果の小数点以下の桁数を指定するためのパラメータを追加してみましょう。
ndigits=10, *,
ignore_overflow=False, ignore_zero_division=False):
try:
fraction = numerator / denominator
return round(fraction, ndigits)
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
ndigits パラメータには、引数を省略することも、位置引数として渡すことも、キーワード引数として渡すこともできます。
print(result) # 3.1428571429
result = safest_division_changed_positional_plus(22, 7, 5)
print(result) # 3.14286
result = safest_division_changed_positional_plus(22, 7, ndigits=2)
print(result) # 3.14
まとめ:
1: Keyword-only arguments (キーワード指定のみ引数) は引数の意味合いを明示し、コードの読解性を高めます。
パラメータリスト中の * シンボルに続くパラメータは全て keyword-only arguments です。
2: Positional-only arguments (位置指定のみ引数) はパラメータ名による束縛を軽減し、将来的な名前変更を担保することで、
コード全体に一貫性を与えるチャンスを残します。
パラメータリスト中の / シンボルより前のパラメータは全て positional-only arguments です。
3: パラメータリスト中の / シンボルと * シンボルの間におかれたパラメータは、Python におけるパラメータのデフォルト操作同様、
位置引数としてもキーワード引数としても値を渡すことが可能です。
こちらの投稿にも興味があるかもしれません...
- 【 Effective Python, 2nd Edition 】スレッド ( thread ) とコルーチン ( coroutine ) を混在させながら、asyncio を利用した非同期プログラムへ段階的に移行させよう!
- 【 Effective Python, 2nd Edition 】ブロッキング I/O ( blocking I/O ) とスレッド ( thread ) を利用しているプログラムを、asyncio 組み込みモジュールを利用してコルーチン ( coroutine ) と非同期 I/O ( asyncronous I/O ) を利用したプログラムにリファクタリング ( refactoring ) しよう!
- 【 Effective Python, 2nd Edition 】プログラムを並列処理 ( concurrency ) パターンへ移行するタイミングとツールを考えるシリーズ 第 6 回 - コルーチン ( coroutines ) を利用して数多くのブロッキング I/O を並列処理する fan-out、fan-in パターンを実現しよう、の巻
- 【 Effective Python, 2nd Edition 】プログラムを並列処理 ( concurrency ) パターンへ移行するタイミングとツールを考えるシリーズ 第 5 回 - 並列処理 ( concurrency ) のためにスレッド ( thread ) を利用する場合は concurrent.futures モジュールの ThreadPoolExecutor の導入を検討しましょう、の巻
0 comments
コメントはまだありません。
コメントを追加する(不適切と思われるコメントは削除する場合があります)