関数 — 引数パターン
デフォルト値、*args/**kwargs、positional-only(/)、keyword-only(*) まで、関数シグネチャを表現力豊かに書くすべての道具を整理します。
第4章 コレクションと内包表記 でデータを扱う道具を見たなら、本章は そのデータを受けて処理する関数 のシグネチャ表現法です。Python の関数は 引数の表記が豊富です。 利点ですが、最初は迷いやすい部分でもあります。一箇所にまとめて整理します。
本章の引数表記は第2部の第12章 デコレータパターン と第3部の第20章 typing 高度 — Variance, ParamSpec, Self, overload で再度登場します。ParamSpec のような高度な型ツールは結局、本章の *args / **kwargs の形を型システムにそのまま移したものなので、本章を終える時点で表記パターンをはっきり押さえておくと後が楽になります。
もっとも単純な関数 #
def greet(name: str) -> None:
print(f"hi, {name}!")
greet("カーティス") # hi, カーティス!def キーワード、コロン後にインデント。第2章 で見た型ヒント (: str、-> None) を毎関数に書くのがモダンPythonの原則であり、本書もその原則に従います。
return と複数返し
#
def split_name(full: str) -> tuple[str, str]:
first, last = full.split(" ", 1)
return first, last
first, last = split_name("カーティス 金")
print(first, last) # カーティス 金return a, b は実は return (a, b) です。受け取る側で unpack して解きます。JavaScript のオブジェクト分解に似た効用を tuple で実現します。
return がないと関数は None を返します。 明示しても、しなくても結果は同じです。
デフォルト値 #
def greet(name: str, greeting: str = "hi") -> None:
print(f"{greeting}, {name}!")
greet("カーティス") # hi, カーティス!
greet("カーティス", "こんにちは") # こんにちは, カーティス!デフォルト値のある引数は デフォルト値のないものの後ろに 来る必要があります。
def bad(x: int = 0, y: int) -> int: ...
# SyntaxError: non-default argument follows default argument落とし穴 — 可変デフォルト値 #
これは Python でもっとも有名な落とし穴です。
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← ?!
print(append_to(3)) # [1, 2, 3]デフォルト値 [] が 関数が定義された時に一度だけ作られ、すべての呼び出しで同じオブジェクトを共有します。呼び出すたびに新しい空リストが生まれるわけではありません。
解決: None をデフォルト値にし、関数の中で新しく作ります。
def append_to(item, target: list[int] | None = None):
if target is None:
target = []
target.append(item)
return targetこのパターンはよく目にします。覚えておくと便利です。
Positional と Keyword の呼び出し #
呼び出すときに引数を 2 つの方法で渡せます。
def create_user(name: str, age: int, role: str = "member") -> dict:
return {"name": name, "age": age, "role": role}
# positional — 位置で
create_user("カーティス", 30)
create_user("カーティス", 30, "admin")
# keyword — 名前で
create_user(name="カーティス", age=30)
create_user(age=30, name="カーティス") # 順序を変えても OK
# 混ぜて使う (positional が先)
create_user("カーティス", age=30, role="admin")呼び出しコードだけで意図が伝わるようにしたいなら keyword 呼び出し が強力です。create_user("カーティス", 30, True, False, "admin") のようなシグネチャは読みにくいですが、create_user(name="カーティス", age=30, is_active=True, is_admin=False, role="admin") は一目で頭に入ります。
*args — 可変位置引数
#
引数の前に * を付けると「残りの位置引数をすべて tuple で受ける」という意味になります。
def add(*nums: int) -> int:
total = 0
for n in nums:
total += n
return total
print(add(1, 2, 3)) # 6
print(add(1, 2, 3, 4, 5)) # 15
print(add()) # 0名前は慣習的に args ですが、*nums のように意味のある名前を付けてもかまいません。
展開して渡す #
* は 呼び出す側 でも使えます。シーケンスを展開して位置引数として渡します。
def add(a: int, b: int, c: int) -> int:
return a + b + c
nums = [1, 2, 3]
print(add(*nums)) # 1, 2, 3 に展開されて入る → 6**kwargs — 可変キーワード引数
#
** を付けると「残りのキーワード引数をすべて dict で受ける」になります。
def configure(**options: str) -> None:
for key, value in options.items():
print(f"{key} = {value}")
configure(host="localhost", port="5432", db="myapp")
# host = localhost
# port = 5432
# db = myapp展開して渡す #
呼び出し側の ** は dict を展開して keyword 引数として渡します。
def create_user(name: str, age: int, role: str) -> dict:
return {"name": name, "age": age, "role": role}
data = {"name": "カーティス", "age": 30, "role": "admin"}
print(create_user(**data))API レスポンスを関数引数にそのまま流す場合によく使います。
すべて合わせた形 #
def fn(a: int, b: int, *args: int, **kwargs: str) -> None:
print(a, b, args, kwargs)
fn(1, 2, 3, 4, 5, name="カーティス", role="admin")
# 1 2 (3, 4, 5) {'name': 'カーティス', 'role': 'admin'}順序: 通常引数 → *args → 通常引数(keyword-only) → **kwargs です。
Keyword-only — * 単独の使用
#
* だけを単独で置くと、その後ろの引数は 必ず keyword でしか受け取れなくなります。
def create(name: str, *, role: str = "member", active: bool = True) -> dict:
return {"name": name, "role": role, "active": active}
create("カーティス") # OK
create("カーティス", role="admin") # OK
create("カーティス", "admin") # ✗ TypeError
# create() takes 1 positional argument but 2 were given呼び出し側が create("カーティス", "admin", True) のような形で書けないように防ぎます。ブール値フラグが複数あるとき に、呼び出しが意味のある見た目になるよう強制できます。
Positional-only — / 単独の使用 (3.8+)
#
逆に / の前は 位置でしか 受け取れないように強制します。
def power(base: float, exp: float = 2, /) -> float:
return base ** exp
power(3) # 9
power(2, 3) # 8
power(base=3) # ✗ TypeError — base は keyword 不可いつ使うのか。引数名を将来変更する自由を残しておきたいとき。 呼び出し側が名前で縛られていなければ、ライブラリのメンテナが名前だけリネームしても呼び出し側コードは壊れません。組み込み関数(abs(x)、len(seq)) はほとんどこの形です。
三つ全部を一緒に #
def f(pos1: int, pos2: int, /, normal: int, *, kw1: int, kw2: int) -> None:
print(pos1, pos2, normal, kw1, kw2)
# pos1, pos2 → 位置のみ
# normal → 位置または keyword
# kw1, kw2 → keyword のみ
f(1, 2, 3, kw1=4, kw2=5) # OK
f(1, 2, normal=3, kw1=4, kw2=5) # OK
f(pos1=1, pos2=2, ...) # ✗ pos1, pos2 は keyword 不可
f(1, 2, 3, 4, 5) # ✗ kw1, kw2 は位置不可読み方:
/の前 = positional-only/と*の間 = 通常*の後ろ = keyword-only
ラムダ — 一行関数 #
lambda は匿名関数です。一つの式だけ 持てます。
square = lambda x: x ** 2
print(square(5)) # 25
# sorted の key のようなコールバック役によく使う
people = [("カーティス", 30), ("スミス", 25), ("ジョン", 40)]
people.sort(key=lambda p: p[1])
print(people)
# [('スミス', 25), ('カーティス', 30), ('ジョン', 40)]lambda を変数に代入して使うパターン (square = lambda x: ...) は 推奨されません。 def square(x): ... と書いてください。lambda は名前が要らない場合だけ使うのが慣例です。
nonlocal と global — スコープを少しだけ
#
関数の中から外の変数を 修正 するには明示が必要です。
def outer():
count = 0
def inner():
nonlocal count # outer の count を修正すると宣言
count += 1
inner()
inner()
print(count) # 2
outer()nonlocal なしで count += 1 すると 新しいローカル変数 が inner の中に作られ、outer のものは変わりません (正確には UnboundLocalError になります — 読む前に書いているので) 。
グローバル変数を関数の中で修正するときは global を使います。ただし、可能なら グローバル変数自体を使わない 方がよいです。
関数も一級オブジェクト #
関数は変数に入れられ、引数として渡せ、戻り値にもできます。
def add(a: int, b: int) -> int:
return a + b
op = add # 関数を変数に入れる
print(op(2, 3)) # 5
def apply(fn, x: int, y: int) -> int:
return fn(x, y)
print(apply(add, 2, 3)) # 5この性質が第12章 デコレータパターン の土台です。関数を受けて関数を返す一行関数がデコレータの本質です。
Callable 型 — 関数を受ける関数の型
#
関数を引数として受け取るときにそのシグネチャを書きたいなら Callable を使います。
from collections.abc import Callable
def apply(fn: Callable[[int, int], int], x: int, y: int) -> int:
return fn(x, y)
apply(lambda a, b: a + b, 1, 2) # 3Callable[[引数の型], 戻り値の型] の形です。引数の型をリストで 書くのが最初は不慣れです。
Callable の引数シグネチャ自体を型変数として扱う ParamSpec は第20章 typing 高度 で扱います。
docstring — 文書文字列 #
関数の最初の行の文字列は docstring になります。
def add(a: int, b: int) -> int:
"""二つの整数を足して返す。"""
return a + b
print(add.__doc__)
# 二つの整数を足して返す。
help(add)型ヒントが「何を受けて何を返すか」を埋めてくれるので、docstring は 「なぜ / どう使うか」 を書く部分になります。短く端的な一行がもっともよいです。
練習問題 #
- 可変デフォルト値の落とし穴を直接再現してみてください。
def collect(item, bucket=[]):のシグネチャの関数に同じ引数で 3 回呼び出して結果を確認した後、同じ関数をbucket: list | None = None+ 中で新しいリストを作る形に直してから、同じ呼び出しを再度行ってください。 def request(url: str, *, method: str = "GET", timeout: float = 5.0, retries: int = 0) -> str:のシグネチャを書いてください。method、timeout、retriesは keyword-only に強制されます。request("https://example.com", "POST")の呼び出しが失敗することと、request("https://example.com", method="POST")が動作することを直接確認します。from collections.abc import Callableを import し、def apply_n_times(fn: Callable[[int], int], x: int, n: int) -> int:を書いてください。fnをxにn回適用した結果を返します。apply_n_times(lambda x: x + 1, 0, 5)→ 5、apply_n_times(lambda x: x * 2, 1, 10)→ 1024 になるかを確認します。
一行まとめ: 関数の引数表記は通常 / デフォルト値 /
*args/**kwargs/ keyword-only(*) / positional-only(/) の組み合わせ。可変デフォルト値はNone+ 関数内生成、ブール値が複数なら keyword-only を強制。呼び出し側の*seq/**dictは展開。関数は値なのでCallable[[...], ...]で型ヒント。
次の章 #
次の 第6章 エラーと例外処理 では、try/except/else/finally とユーザー定義例外、そして Python 3.11 で入った except* (exception group) まで扱います。本章の関数が投げる例外を呼び出し側がどう受けるかが第6章の出発点です。