モダンPython基礎 #5 関数 — 引数パターン

読了 7分

#4 コレクションと内包表記 でデータを扱う道具を見たなら、今回は そのデータを受け取って働く関数 のシグネチャの書き方です。Python の関数は 引数の表記がとても豊富 です。それが強みですが、最初は混乱するところでもあります。一箇所に集めて整理します。

最も単純な関数 #

基本
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("カーティス", "こんにちは")  # こんにちは, カーティス!

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

✗ ダメ
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 呼び出し #

呼び出し時に引数を二つの方法で渡せます。

二つの呼び出し方
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 は無名関数です。式を一つだけ 持てます。

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

この性質が旧講座のデコレータとクロージャの土台です。モダンシリーズでは 次のシリーズ (Python 中級) でデコレータを再び扱います。

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[[引数の型たち], 戻り値の型] の形です。引数の型をリストで 書くのが最初は違和感があります。

docstring — ドキュメンテーション文字列 #

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

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

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

型ヒントが「何を受け取って何を返すか」を埋めてくれるので、docstring は 「なぜ / どう使うか」 を書くところです。短く言い切る一行がベストです。

まとめ #

今回扱ったすべての引数パターン:

  • デフォルト値 — 可変オブジェクト (list/dict) を絶対にデフォルト値にせず、None + 関数内で生成
  • positional vs keyword 呼び出し — keyword 呼び出しは意図が伝わりやすい
  • *args — 可変位置引数、tuple で受け取る
  • **kwargs — 可変キーワード引数、dict で受け取る
  • 呼び出し側 *seq**dict — 開いて渡す
  • *, の後ろは keyword-only — ブールフラグが複数あるとき呼び出しの可読性を強制
  • /, の前は positional-only — 仮引数名の自由を保つ
  • lambda はコールバック位置で、変数代入はしない
  • nonlocal で外側スコープの変数を修正
  • 関数は第一級オブジェクト — Callable[[...], ...] で型ヒント
  • docstring は """...""" で最初の行に

次回 (#6 エラーと例外処理) では try/except/else/finally とユーザー定義例外、そして Python 3.11 で入った except* (exception group) まで扱います。

X