3.

Python 関数定義完全ガイド(def / 引数 / 戻り値 / Type Hint / lambda / yield)

編集
この記事の要点
  • Python の関数定義def 関数名(引数): ...。インデントでブロックを表現
  • 引数は位置引数 / デフォルト引数 / *args / **kwargs / キーワード専用 / 位置専用の 6 種類を組み合わせ可能
  • 戻り値は return。複数返すときはタプル(実質マルチリターン)
  • Type Hint: def f(x: int) -> str:。実行時には強制されないが mypy / pyright で静的解析可能
  • 関数は一級オブジェクト: 変数代入・引数渡し・戻り値で返却可能。デコレータ / 高階関数 / クロージャの基盤

基本構文: def

def greet(name: str) -> str:
    """挨拶文字列を返す(docstring)"""
    return f"Hello, {name}!"

print(greet("taro"))   # Hello, taro!

# 引数なし、戻り値なし
def show():
    print("Hello")

show()

引数の 6 種類

def func(
    pos_only,           # 通常位置引数
    /,                  # ← この前は位置専用 (Python 3.8+)
    normal,             # 位置でもキーワードでも可
    *,                  # ← この後はキーワード専用
    kw_only,            # キーワード専用
    default_kw="x",     # デフォルト付きキーワード専用
):
    ...

# 呼び出し
func(1, 2, kw_only=3)               # OK
func(1, normal=2, kw_only=3)        # OK (normal はキーワードも可)
# func(pos_only=1, normal=2, kw_only=3)  # ❌ pos_only は位置専用
# func(1, 2, 3)                          # ❌ kw_only はキーワード必須

デフォルト引数の落とし穴

デフォルト引数の値は関数定義時に 1 回だけ評価されます。可変オブジェクト(list / dict)をデフォルトにすると、呼び出しごとに同じインスタンスが使い回されます。

# ❌ 危険: 可変オブジェクトをデフォルトに
def append_item(item, items=[]):
    items.append(item)
    return items

print(append_item(1))  # [1]
print(append_item(2))  # [1, 2]  ← 共有されている!
print(append_item(3))  # [1, 2, 3]

# ✅ 正しいパターン
def append_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

*args と **kwargs(可変長引数)

def func(*args, **kwargs):
    print(args)    # tuple
    print(kwargs)  # dict

func(1, 2, 3, name="taro", age=30)
# args   = (1, 2, 3)
# kwargs = {'name': 'taro', 'age': 30}

# アンパック呼び出し
def add(a, b, c):
    return a + b + c

nums = [1, 2, 3]
print(add(*nums))            # 6

params = {"a": 1, "b": 2, "c": 3}
print(add(**params))         # 6

戻り値: return と複数値

def divmod_(a, b):
    """商と余りをタプルで返す"""
    return a // b, a % b      # 実はタプル

q, r = divmod_(10, 3)         # アンパック
print(q, r)                   # 3 1

# return を書かないと None
def show(x):
    print(x)

result = show(1)              # 1
print(result)                 # None

# 途中 return で早期リターン
def first_positive(nums):
    for n in nums:
        if n > 0:
            return n
    return None

Type Hint(型ヒント)

from typing import Optional, Union, Callable

def parse(s: str) -> int:
    return int(s)

def find(name: str) -> Optional[dict]:
    # None もありうる
    return None

# Python 3.10+ の Union 短縮記法
def fetch(id: int) -> dict | None:
    ...

# 関数型
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

# 実行時に強制はされない(型違いでも動く)
parse(123)   # 動くが mypy は警告

ラムダ式(無名関数)

square = lambda x: x ** 2
print(square(5))  # 25

# 高階関数と組み合わせ
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(nums, key=lambda x: -x))         # 降順
print(list(filter(lambda x: x % 2 == 0, nums)))
print(list(map(lambda x: x * 10, nums)))

# 注意: ラムダは式 1 つのみ。文 (if / for / try) は書けない
# 複雑なら def を使う

ジェネレータ関数(yield)

yield を含む関数はジェネレータになり、呼び出し時に即実行せずイテレータを返す。メモリ効率がよい。

def count_up(n):
    i = 0
    while i < n:
        yield i           # 値を 1 つ返して中断、次の next() で再開
        i += 1

for x in count_up(3):
    print(x)              # 0, 1, 2

# 無限ストリームも可
def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

from itertools import islice
print(list(islice(fib(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

async def(非同期関数)

import asyncio
import aiohttp

async def fetch(url: str) -> str:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

async def main():
    urls = ["https://example.com", "https://example.org"]
    results = await asyncio.gather(*(fetch(u) for u in urls))
    print(len(results))

asyncio.run(main())

デコレータ

import time
from functools import wraps

def measure(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        t0 = time.time()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__}: {time.time() - t0:.3f}s")
        return result
    return wrapper

@measure
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)
# slow_sum: 0.234s

関数の一級オブジェクト性

# 変数に代入
f = print
f("Hello")                    # Hello

# 引数として渡す
def apply(fn, x):
    return fn(x)

apply(str.upper, "hello")     # 'HELLO'

# 戻り値として返す(クロージャ)
def make_adder(n):
    def adder(x):
        return x + n
    return adder

add5 = make_adder(5)
print(add5(10))               # 15
print(add5(20))               # 25

docstring と help()

def factorial(n: int) -> int:
    """n の階乗を計算する。

    Args:
        n: 0 以上の整数

    Returns:
        n! の値

    Raises:
        ValueError: n が負のとき
    """
    if n < 0:
        raise ValueError("n must be >= 0")
    return 1 if n <= 1 else n * factorial(n - 1)

help(factorial)   # docstring が表示される
print(factorial.__doc__)

FAQ

Q: 関数内で外側の変数を書き換えたい
A: global x でグローバル、nonlocal x で 1 つ外の関数スコープ。

Q: 再帰の深さ制限はある?
A: デフォルト 1000。sys.setrecursionlimit(10000) で変更可。Python は末尾再帰最適化を持たないためループに書き換えるのが原則。

Q: 関数の引数を強制的に型チェックしたい
A: 標準では型ヒントは強制されません。実行時チェックには pydantic.validate_call / typeguard / beartype を使います。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. クラスの定義
  2. initメソッド
  3. 関数の定義
  4. クラス変数とインスタンス変数

最近更新/作成されたページ