検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
effective_python

【 Effective Python, 2nd Edition 】keyword-only arguments (キーワード指定のみ引数) と positional-only arguments (位置指定のみ引数) を使いこなして、コードの読解性と将来的な拡張性を高めよう! 投稿一覧へ戻る

Published 2020年6月22日20:28 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Python では、関数を呼び出す際にキーワード引数 (keyword arguments) を利用することで、引数の役割が明確になりコードの読解性、メンテナンス性が高まったり、将来的に、関数側で新たにデフォルト値を設定したオプションパラメータを追加して下位互換性を維持したまま機能の拡張ができるようになったり、非常に多くの利点を享受することができます。


ここで、割り算の答えを返す単純な関数を考えてみましょう。


ただしこの関数では、設定によって、OverflowError 例外が発生した場合でも代わりに 0 を返し、
ZeroDivisionError 例外が発生した場合でも代わりに infinity (無限) を返すことができるようにしましょう。


def safe_division(dividend, divisor, ignore_overflow, ignore_zero_division):
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 例外を無視するようにこの関数を呼び出します。

result = safe_division(1.0, 10**500, True, False)
print(result) # 0



次は ZeroDivisionError 例外を無視するように呼び出してみます。

result = safe_division(1.0, 0, False, True)
print(result) # inf



期待通りの動作ですが、ちょっと問題があります。どちらの Boolean 引数がどちらの例外を無視するものなのか、を混同しやすい、ということです。


この例では、3番目と4番目の位置引数として単純に True や False を渡していますが、もし勘違いして値を入れ替えて渡してしまったとしてもプログラムは正常に動作してしまうでしょう。


この種のバグはとにかく見つけ辛いです。ゲッソリやつれます。


そこで、関数側でこの2つのパラメータにデフォルト値を設定し、オプションパラメータとして扱うことにしましょう。


def safer_division(dividend, divisor, ignore_overflow=False, ignore_zero_division=False):
try:
return dividend / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise



そして、呼び出す側では、無視したい例外を指定する場合にはキーワード引数で指定するようにします。


result = safer_division(1.0, 10**500, ignore_overflow=True)
print(result) # 0

result = safer_division(1.0, 0, ignore_zero_division=True)
print(result) # inf



一安心ですね、ホッ!


いえいえ、まだまだ安心はできません。


関数側でこのような実装をしたとしても、オプションパラメータに値を指定する場合はキーワード引数で、ということを呼び出し側に強制できるわけではありません。


今までどおり位置引数で渡すこともできちゃいます。


assert safer_division(1.0, 10**500, True, False) == 0



そこで、キーワード引数でなければこれらのオプションパラメータに値を渡せないようにしちゃいましょう、そうしましょう。


def safest_division(dividend, divisor, *, ignore_overflow=False, ignore_zero_division=False):
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番目の引数を位置引数として渡すことはできなくなります。


result = safest_division(1.0, 10**500, True, False)

# TypeError: safest_division() takes 2 positional arguments but 4 were given




もちろん、キーワード引数として渡した場合は期待通りに動作します。


result = safest_division(1.0, 0, ignore_zero_division=True)
assert result == float('inf')



該当すればちゃんと例外も投げてきます。


try:
result = safest_division(1.0, 0)
except ZeroDivisionError:
print('Woohoo!')

# Woohoo!




一件落着!?ところが、残念ながら話はまだ続きます。


呼び出し側で、最初の2つの引数、もしくは、どちらか一方をキーワード引数で渡していたとします。


assert safest_division(dividend=2, divisor=5) == 0.4
assert safest_division(divisor=5, dividend=2) == 0.4
assert safest_division(2, divisor=5) == 0.4



さて、この関数の作成者の気が変わって、パラメータの名前を変更しちゃいました。


def safest_division_changed(numerator, denominator, *, ignore_overflow=False, ignore_zero_division=False):
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 パラメータにキーワード引数で値を渡していた全ての呼び出し元を機能させなくする、という大問題を引き起こします。


safest_division_changed(dividend=2, divisor=5)

# TypeError: safest_division_changed() got an unexpected keyword argument 'dividend'




そこで、Python 3.8 では、将来的にこのような変更があることも考慮して positional-only arguments (位置指定のみ引数) という考えが採用されました。


これは先ほどまで見てきた keyword-only arguments (キーワード指定のみ引数) の対極に位置するものです。


def safest_division_changed_positional(numerator, denominator, /, *, ignore_overflow=False, ignore_zero_division=False):
try:
return numerator / denominator
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise



パラメータリスト中の / シンボルは、「ここまでの引数は位置引数としてしか値を渡せませんよ」ということを意味しています。


assert safest_division_changed_positional(2, 5) == 0.4



ですが、キーワード引数として値を渡そうとすると...


safest_division_changed_positional(2, denominator=5)

# TypeError: safest_division_changed_positional() got some positional-only arguments passed as keyword arguments: 'denominator'




ここまでで、前半2つのパラメータに対しては位置引数でしか値を渡せないので、将来的なパラメータ名の変更が可能となり、
後半2つのパラメータに対してはキーワード引数でしか値を渡せないので、コードの読解性を高め、引数の渡し間違いによるバグ発生を軽減できるようになりました。


最後にもう1つだけ。


パラメータリスト内の / シンボルと * シンボルの間に置かれたパラメータに対しては、Python のデフォルトの動作と同様に、位置引数としてもキーワード引数としても値を渡せます。


割り算の結果の小数点以下の桁数を指定するためのパラメータを追加してみましょう。


def safest_division_changed_positional_plus(numerator, denominator, /,
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 パラメータには、引数を省略することも、位置引数として渡すことも、キーワード引数として渡すこともできます。


result = safest_division_changed_positional_plus(22, 7)
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 におけるパラメータのデフォルト操作同様、
位置引数としてもキーワード引数としても値を渡すことが可能です。

この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Effective Python, 2nd Edition 】throw() メソッドを利用したジェネレータ ( generator ) 内部での状態遷移はなるだけ避けましょう。ネストが深くなってコードの読解性が落ちちゃいますよ!
【Python 雑談・雑学】 デコレータ (decorators) を理解しよう - デコレータ、オリジナル関数からの引数の渡し方、受け取り方
【Python 雑談・雑学】 関数への引数の渡し方・受け取り方 - *args、**kwargs を利用した引数の渡し方、パラメータの受け取り方、総復習 -
【 Effective Python, 2nd Edition 】__set_name__ デスクリプタ専用特殊関数 ( special method for descriptor ) を利用してデスクリプタインスタンスを割り当てたクラス変数名を取得し、コードの冗長性を排除しよう!
【 Effective Python, 2nd Edition 】クラスインスタンスを関数として利用可能にする __call__ 特殊関数を含んだクラスを定義してフック ( hook ) として利用することで、既存の API の機能拡張を計ろう!
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!
【 Effective Python, 2nd Edition 】内包表記に含める for 文や if 文の数は2つ位までに抑えておかないと読解性が極端に悪くなりますよ、という話