公式ドキュメントで sorted() の説明を読んだとき、sorted(iterable, /, *, key=None, reverse=False) という書き方に戸惑ったことはありませんか?
引数の名前が並んでいるのはわかります。でも、間にはさまっている / と * は何なのでしょうか。掛け算でも割り算でもなさそうで、気になりますよね。
この2つの記号は、引数をどうやって渡すかを決めるためのものです。この記事では、* で作るキーワード専用引数と、/ で作る位置専用引数を順番に見ていきます。サンプルの実行結果は、手元のPython 3.11で確認したものです。
引数の渡し方には2種類ある¶
記号の話に入る前に、引数の渡し方をおさらいしておきましょう。Pythonでは、関数に値を渡す方法が大きく2つあります。
1つ目は、並べた順番で渡す位置引数です。2つ目は、引数の名前を書いて渡すキーワード引数です。
次のコードでは、同じ関数を2通りの方法で呼んでいます。
def greet(name, message):
print(f"{name}さん、{message}")
greet("佐藤", "こんにちは") # 位置引数で渡す
greet(message="こんにちは", name="佐藤") # キーワード引数で渡す
どちらも結果は同じで、佐藤さん、こんにちは と表示されます。普通に def で関数を作ると、引数はどちらの方法でも受け取れる状態になっています。
この自由さはとても便利です。ただ、自由すぎるせいで困る場面も出てきます。* と / は、その自由さにあえて制限をかけるための道具なのです。
可変長の引数を受け取る *args と **kwargs については、こちらの記事で詳しく解説しています。
【関連記事】Pythonのargsと*kwargsとは?
*より後ろはキーワード専用引数になる¶
まずは * から見ていきます。関数の定義に単独の * を書くと、それより後ろの引数は名前を書かないと渡せなくなります。
これをキーワード専用引数と呼びます。Python 3.0 で PEP 3102 として導入された仕組みです。
位置で渡すと何が困るのか¶
なぜわざわざ制限するのか、具体例で考えてみましょう。画像のサイズを変える関数を作ったとします。
def resize(image, width, height, keep_ratio=True):
...
resize("photo.png", 800, 600, False)
最後の False は、何を意味しているのでしょうか。関数の定義を見に行かないと、縦横比を保たない指定だとはわかりません。
こうした True や False だけがぽつんと並んだ呼び出しは、読む人にとってなぞなぞのようなものです。半年後の自分が読んでも、たぶん首をかしげます。
*を入れて名前の指定を強制する¶
そこで、keep_ratio の前に * を入れてみます。
def resize(image, width, height, *, keep_ratio=True):
return width, height, keep_ratio
print(resize("photo.png", 800, 600, keep_ratio=False))
# (800, 600, False)
resize("photo.png", 800, 600, False)
# TypeError: resize() takes 3 positional arguments but 4 were given
位置で渡そうとすると、位置引数は3つまでなのに4つ渡された、というエラーで止まります。keep_ratio=False と書いたときだけ受け付けてくれるわけです。
呼び出す側に名前を書かせることで、コードを読むだけで意味が伝わるようになります。これがキーワード専用引数のいちばんの効き目です。
デフォルト値がなければ必須の引数になる¶
キーワード専用引数には、デフォルト値を付けなくてもかまいません。その場合は、名前付きで必ず渡さなければいけない引数になります。
def connect(host, *, timeout):
print(f"{host} に {timeout} 秒で接続します")
connect("example.com", timeout=5)
connect("example.com")
# TypeError: connect() missing 1 required keyword-only argument: 'timeout'
エラーの中に keyword-only という言葉が出てきますね。英語のエラーメッセージも、仕組みを知っていれば落ち着いて読めるようになります。
ちなみに、*args を書いた場合もその後ろの引数はキーワード専用になります。単独の * は、余った位置引数を受け取らずに区切りだけを入れたいときの書き方だと考えると整理しやすいです。
/より前は位置専用引数になる¶
次は / です。こちらは * とちょうど反対の働きをします。
/ より前に書いた引数は、名前を書いて渡すことができなくなります。これを位置専用引数と呼び、Python 3.8 で PEP 570 として導入されました。
def area(width, height, /):
return width * height
print(area(3, 4)) # 12
area(width=3, height=4)
# TypeError: area() got some positional-only arguments passed as keyword arguments: 'width, height'
名前付きで渡すと、位置専用の引数がキーワードで渡されましたというエラーになります。名前を書いてくれたのに断るなんて、少し不親切に感じるかもしれません。
でも、これにはちゃんとした理由があります。名前で呼ばれないということは、その名前があとで変わっても誰も困らないということです。
たとえば width を w に変えたくなったとしましょう。普通の引数なら、width=3 と書いて呼んでいる人のコードがすべて壊れます。位置専用にしておけば、名前は関数の中だけの事情として自由に直せるのです。
組み込み関数はもともと位置専用だった¶
実は、位置専用引数は Python 3.8 より前から存在していました。C言語で書かれた組み込み関数の多くが、昔から名前での指定を受け付けていなかったのです。
手元で試すと、そのことがよくわかります。
len(obj=[1, 2, 3])
# TypeError: len() takes no keyword arguments
PEP 570 は、こうした組み込み関数と同じ形を、Pythonで書く関数でも表現できるようにする提案でした。そのおかげで、ドキュメントの書き方もそろえられるようになっています。
inspect.signature() を使うと、関数がどんな引数を受け取るのかを確認できます。いくつかの組み込み関数で試してみましょう。
import inspect
print(inspect.signature(sorted))
# (iterable, /, *, key=None, reverse=False)
print(inspect.signature(print))
# (*args, sep=' ', end='\n', file=None, flush=False)
print(inspect.signature(dict.get))
# (self, key, default=None, /)
冒頭の sorted() の謎が、ここで解けました。iterable は位置専用で、key と reverse はキーワード専用です。
つまり sorted(nums, lambda x: -x) のように key を位置で渡すことはできません。必ず key= と書く決まりになっています。
sorted() の key 引数の使い方は、こちらの記事でくわしく紹介しています。 【関連記事】Pythonのsorted()とは?
記号ごとの違いを表で整理する¶
ここまでの内容を、いったん表で整理しておきます。def f(a, /, b, *, c) という関数を例にすると、3つの引数はそれぞれ違うルールを持ちます。
| 引数 | 置かれた場所 | 位置で渡す | 名前で渡す | 呼び名 |
|---|---|---|---|---|
| a | / より前 | できる | できない | 位置専用引数 |
| b | / と * の間 | できる | できる | 普通の引数 |
| c | * より後ろ | できない | できる | キーワード専用引数 |
この関数に対して、いろいろな呼び方を試した結果も見てみましょう。実際に動かしたときの結果です。
| 呼び出し方 | 結果 |
|---|---|
| f(1, 2, c=3) | 成功 |
| f(1, b=2, c=3) | 成功 |
| f(1, 2, 3) | TypeError(位置引数が多すぎる) |
| f(a=1, b=2, c=3) | TypeError(a を名前で渡している) |
覚え方としては、/ の左は位置だけ、* の右は名前だけ、と考えるとすっきりします。真ん中にはさまれた引数だけが、昔ながらの自由な引数です。
位置専用にすると**kwargsと名前がぶつからない¶
位置専用引数には、もうひとつ便利な使い道があります。**kwargs で好きな名前の引数を受け取りたいとき、名前のぶつかりを避けられるのです。
次の関数は、最初の引数で名前を受け取り、残りを辞書として集めます。
def make_tag(name, /, **attrs):
return name, attrs
print(make_tag("input", name="email", type="text"))
# ('input', {'name': 'email', 'type': 'text'})
name が位置専用なので、呼び出し側の name="email" は **attrs のほうへ入ります。もし / がなければ、name に値が2回渡されたというエラーで止まってしまいます。
HTMLの属性のように、利用者がどんな名前を使うか予想できない場面で役に立つ書き方です。ライブラリのコードを読んでいると、ときどき見かけますよ。
dataclassでもキーワード専用にできる¶
キーワード専用の考え方は、関数だけのものではありません。データをまとめるクラスを作る dataclass でも、Python 3.10 から kw_only=True という指定が使えます。
from dataclasses import dataclass
@dataclass(kw_only=True)
class User:
name: str
age: int
print(User(name="佐藤", age=30))
# User(name='佐藤', age=30)
User("佐藤", 30)
# TypeError: User.__init__() takes 1 positional argument but 3 were given
項目が増えてくるクラスほど、順番で渡す書き方は事故のもとになります。最初から名前で渡す決まりにしておくと、あとから項目を足したときも安心です。
dataclass の基本的な使い方は、こちらの記事で解説しています。 【関連記事】Pythonのdataclassとは?
実務でどう使い分けるか¶
ここからは、実際の開発でどう使うかを考えていきます。使いどころをまとめると、次の表のようになります。
| 場面 | おすすめ | 理由 |
|---|---|---|
| True/False を切り替える引数 | キーワード専用 | 呼び出しを読むだけで意味がわかる |
| 省略できるオプションの引数 | キーワード専用 | あとから順番を入れ替えても壊れない |
| 意味が明らかな最初の1〜2個 | 位置専用か普通の引数 | len(x) のように名前は不要 |
| 他人に使ってもらうライブラリ | 位置専用も検討 | 引数名を変えても利用者が困らない |
私は10年ほどエンジニアとして開発に関わってきましたが、引数の順番が原因の不具合には何度も悩まされてきました。特に印象に残っているのは、同じ型の引数が並んだ関数で、開始日と終了日を逆に渡していたバグです。
エラーにならず、それらしい値が返ってくるので、気づくまでにかなり時間がかかりました。それ以来、取り違えると困る引数や真偽値の引数は、キーワード専用にするのを習慣にしています。
一方で、何でもかんでも * を付ければいいわけではありません。print() の中身や len() の対象のように、名前を書かなくても意味が明らかな引数まで縛ると、かえって呼び出しが長くなってしまいます。
迷ったときは、呼び出し側のコードを声に出して読んでみてください。意味がすっと伝わらない引数があれば、それがキーワード専用にする候補です。
古いPythonでは/が使えない点に注意¶
注意点として、/ を使った書き方は Python 3.8 より前のバージョンでは文法エラーになります。古い環境でも動かす必要があるコードでは、使えるかどうかを先に確認しておきましょう。
- によるキーワード専用引数は Python 3.0 からある機能なので、今の Python 3 ならまず心配いりません。
また、デフォルト値に関する落とし穴は、キーワード専用引数でもそのまま当てはまります。リストなどをデフォルト値にするときは気をつけてください。 【関連記事】Pythonのデフォルト引数の落とし穴
まとめ¶
関数定義の * と / は、引数の渡し方にルールを付けるための記号です。* より後ろはキーワード専用引数になり、/ より前は位置専用引数になります。
キーワード専用は、呼び出しを読みやすくして、順番の取り違えを防ぎます。位置専用は、引数の名前を外に見せないことで、あとから名前を変えられる自由を残します。
公式ドキュメントで / や * を見かけたら、渡し方のルールが書いてあると思い出してください。sorted() や print() の説明も、ぐっと読みやすくなるはずです。
まずは自分の関数で、True や False を受け取る引数に * を付けるところから試してみてはいかがでしょうか。
ここまでお読みいただきありがとうございました。