Python 3.8 の概要 (その2) - Positional-only parameters
Python 3.0 以降では、関数を定義するときに、キーワード専用引数 を指定できるようになりました。
def func(a, b, *, c=1, d=2): return a+b+c+d
こんなのですね。引数のリストに *
がある関数を呼び出すとき、*
の後ろにある引数の値は、かならずキーワード引数として指定しなければいけません。
↑の関数だと、引数 c
はキーワード引数で指定すればちゃんと動きます。
>>> func(1, 2, c=10) 15
しかし、キーワードなしで呼び出すとエラーになります。
>>> func(1, 2, 10) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: func() takes 2 positional arguments but 3 were given
「この関数呼び出すとき、この引数はかならず argname=value
の形で指定してね。そうじゃないと読みにくくなっちゃうから」という、関数の開発者の気配りです。
で、キーワード専用引数はずいぶん前に実現してたんですが、その逆、キーワードなし専用引数 は存在しませんでした。不公平ですね。
しかし、キーワード専用引数に遅れること11年、Python 3.8でついに PEP 570 -- Python Positional-Only Parameters として実現することになりました。
PEP 570 -- Python Positional-Only Parameters
関数の引数のリストに /
があると、/
より前にある引数はすべて 位置専用パラメータ (Positional-Only Parameters) となり、呼び出すときに値をキーワードでは指定できなくなります。
def func(a, b, /, c): return a+b+c
この例では、引数 c
は /
の後ろにありますから、キーワードで指定できます。
>>> func(1, 2, c=3) 6
しかし、a
と b
は/
の前にありますので、キーワードは使えません。
>>> func(1, b=2, c=3) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: func() got some positional-only arguments passed as keyword arguments: 'b'
古くて新しい機能
じつは、位置専用パラメータは完全に新しい機能というわけではなく、Python 3.8以前にも存在していました。しかし、そういう関数はPython言語では作成できず、組み込みモジュールとしてC/C++言語などでしか作成できませんでした。C言語でPythonの関数を定義するときは、キーワードを無効化したほうが簡単でパフォーマンスも良かったりします。
昔からあるシンプルな組み込み関数などはだいたいそういうパターンで作られていて、例えば range()
や sum()
などは、キーワードとして引数を指定できません。
>>> sum(itearble=[1, 2, 3]) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: sum() takes no keyword arguments
この sum()
と同じインターフェースの関数は、これまでPythonの構文としてはサポートされていなかったのですが、位置専用パラメータを利用すれば
def sum(iterable, start=0, /): ...
と定義できるようになります。
ところで、まだ位置専用パラメータがサポートされていない、Python3.7で sum()
関数を調べてみましょう。
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43) >>> help(sum) Help on built-in function sum in module builtins: sum(iterable, start=0, /) Return the sum of a 'start' value (default: 0) plus an iterable of numbers ...
ここで表示される関数の定義をよく見てみると、helpドキュメントにはすでに
sum(iterable, start=0, /)
と、位置専用パラメータの構文で表示されてますね。実はこの構文、あたらしく作られたのではなく、Pythonの組み込み関数の開発に使われる ツール ですでに採用されていた書き方なのです。
これ、なにが嬉しいの?
例えば、こんな関数を考えてみましょう。
def set_attrs(obj, **kwargs): for k, v in kwargs.items(): setattr(obj, k, v)
set_attrs()
は、指定したオブジェクトに、キーワード引数に指定した属性を設定します。
# target.foo = 1, target.bar=2 とする >>> set_attrs(target, foo=1, bar=2) >>> target.foo 1 >>> target.bar 2
いい感じですが、この実装には欠点があります。obj
という名前の属性を設定できないのです。
>>> set_attrs(target, obj=1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: set_attrs() got multiple values for argument 'obj'
obj
という名前の仮引数は、更新対象のオブジェクトを指定するために使われていますので、もう一度同じ obj
という名前を使うことはできません。
この問題、これまでは、仮引数 obj
の名前を _target___obj
などのあまり使われなさそうな名前にして、問題が発生しないことを祈る、などが主な対策でした。
しかし、位置専用パラメータを利用して
def set_attrs(obj, /, **kwargs): for k, v in kwargs.items(): setattr(obj, k, v)
と定義すれば、位置パラメータしてしか指定できない obj
という仮引数名は、なかった ものとして扱われ、キーワード引数として obj
を指定できるようになります。
>>> set_attrs(target, obj=1) >>> target.obj 1
他には?
PEP 570 -- Python Positional-Only Parameters には他にも想定される使い道が示されているのですが、あまり私のハートには刺さりませんでした… 😁
たとえば、
def div_obj(left, right): return left/right
という、left/right
を計算する関数があったとき、ふつうは
>>> div_obj(10, 2) 5
という感じに使うと思います。しかし、キーワード引数を使って
>>> div_obj(right=2, left=10) 5
と書かれてしまうと、これはとても気持ち悪いコードになってしまいます。
そこで、PEP 570ではこの関数の定義を
def div_obj(left, right, /): return left/right
としてキーワード引数を使えなくしてしまえば、こういった気持ちの悪い使い方を禁止できますよ、と紹介しています。
しかし、そのためにわざわざキーワード引数を使えなくしてしまうのは、ちょっと余計なお世話じゃないかなっていう感じがして、たぶん自分ではこういう使い方はしないかなーと思います。