/dev/null

脳みそのL1キャッシュ

Python の仮引数で指定できる / と * の意味

はじめに

以下は valid な Python コードなのですが、仮引数リストにある/*の役割わかりますか? 僕はわかりません。さっぱり動作が想像できなかったので調べてみました。

def func(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)

TL;DR

def func(a, b, /, c, d, *, e, f):
         ----     -----    -----
           \         \        \_____ キーワード実引数しか受け取らない仮引数
            \         \_____________ 位置実引数またはキーワード実引数が受け取れる仮引数
             \______________________ 位置実引数しか受け取らない仮引数

引数の種類

仮引数と実引数

Python の引数(Python に限りませんが)には 2 種類あり、引数(parameter)と引数(argument)があります。

仮引数は以下のように、関数定義においてその関数が受け取れる実引数を指定します。

def func(a, b, c=None):
    print(a, b, c)

対して、実引数は以下のように、関数呼び出し時に関数に渡す値のことです。

func(1, 2, c=3) # 1 2 3

位置引数とキーワード引数

更に細かく分けると Python の引数には、位置(仮/実)引数(Positional argument/parameter)キーワード(仮/実)引数(Keyword argument/parameter) があります。

実引数

実引数のほうが簡単なので、実引数の方から説明します。以下の関数呼び出しにおいて、1, 2が位置実引数で、c=3がキーワード実引数です。

def func(a, b, c=None):
    print(a, b, c)

func(1, 2, c=3)
func(1, c=3, 2) # SyntaxError: positional argument follows keyword argument

位置実引数はその位置から、実際にどの仮引数に渡されるかが決定されます。これに比べて、キーワード実引数は=の前について識別子によって、どの仮引数に渡されるかが決定されます。また、キーワード引数の後ろに位置引数を渡すことはできません。

仮引数

ここがややこしい。仮引数にも位置仮引数とキーワード仮引数があります。僕は今まで以下のように勘違いしていました。

def func(a, b, c=None):  # a, b は位置仮引数、c はキーワード仮引数
    print(a, b, c)

違うんですよねー。上記の仮引数はすべて、位置またはキーワード仮引数(Positional-or-keyword parameter) です。つまり、以下のように位置/キーワード実引数の両方を受け取れるようになってるんですよね。

func(1, 2, 3)
func(1, 2, c=3)
func(1, b=2, c=3)
func(a=1, b=2, c=3)

じゃあ、位置引数しか受け取らない位置引数はどの定義すればいいのかというと、以下のように/の前に仮引数を書くとその仮引数は位置実引数しか受け取らないようになります。

def func(a, b, /):
    print(a, b)
    
func(1, 2) # 1 2
func(1, b=2) # TypeError: func() got some positional-only arguments passed as keyword arguments: 'b'

逆に、キーワード実引数しか受け取らないキーワード仮引数は、以下のように*の後ろに仮引数を書けば定義できます。

def func(*, c, d):
    print(c, d)
    
func(c=1, d=2) # 1 2
func(1, d=2) # TypeError: func() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given

そして、/*の間の仮引数が 位置またはキーワード仮引数(Positional-or-keyword parameter) になります。

def func(a, b, /, c, d, *, e, f):
    print(a, b, c, d, e, f)
    
func(1, 2, 3, d=4, e=5, f=6) # 1 2 3 4 5 6
func(1, 2, c=3, d=4, e=5, f=6) # 1 2 3 4 5 6
func(1, 2, 3, 4, e=5, f=6) # 1 2 3 4 5 6
func(1, b=2, c=3, d=4, e=5, f=6) # TypeError: func() got some positional-only arguments passed as keyword arguments: 'b'
func(1, 2, 3, 4, 5, f=6) # TypeError: func() takes 4 positional arguments but 5 positional arguments (and 1 keyword-only argument) were given

まとめると

def func(a, b, /, c, d, *, e, f):
         ----     -----    -----
           \         \        \_____ キーワード実引数しか受け取らない仮引数
            \         \_____________ 位置実引数またはキーワード実引数が受け取れる仮引数
             \______________________ 位置実引数しか受け取らない仮引数

おわりに

長年の勘違いが解けた瞬間だった。

参考文献

docs.python.org