目次
5 章

関数 — 引数パターン

デフォルト値、*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 と複数返し #

複数返しは実は tuple
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("カーティス", "こんにちは")  # こんにちは, カーティス!

デフォルト値のある引数は デフォルト値のないものの後ろに 来る必要があります。

✗ NG
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 で受ける」という意味になります。

*args
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 で受ける」になります。

**kwargs
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 でしか受け取れなくなります。

keyword-only
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+) #

逆に / の前は 位置でしか 受け取れないように強制します。

positional-only
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)) はほとんどこの形です。

三つ全部を一緒に #

positional-only / 通常 / keyword-only
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 は匿名関数です。一つの式だけ 持てます。

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 は名前が要らない場合だけ使うのが慣例です。

nonlocalglobal — スコープを少しだけ #

関数の中から外の変数を 修正 するには明示が必要です。

nonlocal
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 を使います。

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)   # 3

Callable[[引数の型], 戻り値の型] の形です。引数の型をリストで 書くのが最初は不慣れです。

Callable の引数シグネチャ自体を型変数として扱う ParamSpec は第20章 typing 高度 で扱います。

docstring — 文書文字列 #

関数の最初の行の文字列は docstring になります。

docstring
def add(a: int, b: int) -> int:
    """二つの整数を足して返す。"""
    return a + b

print(add.__doc__)
# 二つの整数を足して返す。
help(add)

型ヒントが「何を受けて何を返すか」を埋めてくれるので、docstring は 「なぜ / どう使うか」 を書く部分になります。短く端的な一行がもっともよいです。

練習問題 #

  1. 可変デフォルト値の落とし穴を直接再現してみてください。def collect(item, bucket=[]): のシグネチャの関数に同じ引数で 3 回呼び出して結果を確認した後、同じ関数を bucket: list | None = None + 中で新しいリストを作る形に直してから、同じ呼び出しを再度行ってください。
  2. def request(url: str, *, method: str = "GET", timeout: float = 5.0, retries: int = 0) -> str: のシグネチャを書いてください。methodtimeoutretries は keyword-only に強制されます。request("https://example.com", "POST") の呼び出しが失敗することと、request("https://example.com", method="POST") が動作することを直接確認します。
  3. from collections.abc import Callable を import し、def apply_n_times(fn: Callable[[int], int], x: int, n: int) -> int: を書いてください。fnxn 回適用した結果を返します。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章の出発点です。

X