STEP 4

Python 関数 完全ガイド

def文による関数定義から、引数の種類、lambda式、クロージャ、デコレーター、ジェネレーターまで、Pythonの関数に関するすべてを解説します。

📖 中級者向け内容含む🎯 初心者〜中級者

1. 関数の基本(def文)

関数は処理のまとまりに名前をつけて再利用できるようにするものです。def キーワードで定義します。

# 最もシンプルな関数
def greet():
    print("こんにちは!")

greet()  # こんにちは!

# 引数を持つ関数
def greet_name(name):
    print(f"こんにちは、{name}さん!")

greet_name("Alice")  # こんにちは、Aliceさん!

# docstring(関数の説明)
def add(a, b):
    """
    2つの数値を加算して返す。

    Args:
        a: 1つ目の数値
        b: 2つ目の数値

    Returns:
        a と b の合計
    """
    return a + b

print(add(3, 5))        # 8
print(add.__doc__)      # docstringを表示
help(add)               # ヘルプを表示

2. 引数の種類

位置引数・キーワード引数

def profile(name, age, city):
    print(f"{name}, {age}歳, {city}在住")

# 位置引数(順番通りに渡す)
profile("Alice", 25, "Tokyo")

# キーワード引数(名前で渡す)
profile(age=30, city="Osaka", name="Bob")

# 混在(位置引数は先に)
profile("Carol", city="Kyoto", age=28)

デフォルト引数

def greet(name, greeting="こんにちは"):
    print(f"{greeting}、{name}さん!")

greet("Alice")                    # こんにちは、Aliceさん!
greet("Bob", "おはよう")          # おはよう、Bobさん!
greet("Carol", greeting="やあ")   # やあ、Carolさん!

# 注意:デフォルト引数にミュータブルを使ってはいけない!
# 悪い例(バグの元)
def bad_append(item, lst=[]):
    lst.append(item)
    return lst

print(bad_append(1))   # [1]
print(bad_append(2))   # [1, 2] ← 前の呼び出しの状態が残る!

# 正しい書き方
def good_append(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

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

# *args - 任意個数の位置引数をタプルで受け取る
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3))        # 6
print(sum_all(1, 2, 3, 4, 5))  # 15

# **kwargs - 任意個数のキーワード引数を辞書で受け取る
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=25, city="Tokyo")

# 組み合わせ(順番は: 通常引数, *args, キーワードのみ, **kwargs)
def complex_func(required, *args, keyword_only=None, **kwargs):
    print(f"required: {required}")
    print(f"args: {args}")
    print(f"keyword_only: {keyword_only}")
    print(f"kwargs: {kwargs}")

complex_func("必須", 1, 2, 3, keyword_only="KW", extra="追加")

# アンパックして渡す
numbers = [1, 2, 3]
print(sum_all(*numbers))    # リストをアンパック

params = {"name": "Alice", "age": 25}
print_info(**params)        # 辞書をアンパック

# / と * による引数の種類を強制
def strict(pos_only, /, normal, *, kw_only):
    """
    pos_only: 位置引数のみ(名前で渡せない)
    normal: どちらでも可
    kw_only: キーワード引数のみ(位置で渡せない)
    """
    print(pos_only, normal, kw_only)

strict(1, 2, kw_only=3)
strict(1, normal=2, kw_only=3)

3. 戻り値(return)

# 単一の戻り値
def square(x):
    return x ** 2

result = square(5)   # 25

# 複数の戻り値(実際はタプルを返している)
def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 4, 1, 5, 9])
print(lo, hi)   # 1 9

# 条件によって異なる値を返す
def absolute(x):
    if x >= 0:
        return x
    return -x    # return があれば else は不要

# 戻り値なし(None を返す)
def print_greeting(name):
    print(f"Hello, {name}!")
    # return None が暗黙的に実行される

result = print_greeting("Alice")
print(result)  # None

# 早期リターン(ガード節)
def divide(a, b):
    if b == 0:
        return None    # 早期リターン
    return a / b

print(divide(10, 2))   # 5.0
print(divide(10, 0))   # None

4. スコープ(変数の有効範囲)

Pythonのスコープは LEGB ルールに従います:Local → Enclosing → Global → Built-in

x = "global"   # グローバル変数

def outer():
    x = "enclosing"   # enclosing スコープ

    def inner():
        x = "local"   # ローカル変数
        print(x)      # local

    inner()
    print(x)          # enclosing

outer()
print(x)              # global

# global 宣言
count = 0

def increment():
    global count     # グローバル変数を変更する宣言
    count += 1

increment()
print(count)  # 1

# nonlocal 宣言(クロージャで使用)
def make_counter():
    count = 0
    def counter():
        nonlocal count   # 外側スコープの変数を変更
        count += 1
        return count
    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

5. lambda式(無名関数)

lambdaは名前のない小さな関数を1行で書く記法です。ソートのキー関数などシンプルな処理に使います。

# 基本構文: lambda 引数: 式
square = lambda x: x ** 2
print(square(5))   # 25

add = lambda a, b: a + b
print(add(3, 4))   # 7

# 条件式(三項演算子)を使う
classify = lambda x: "正" if x > 0 else "負" if x < 0 else "ゼロ"
print(classify(5))    # 正
print(classify(-3))   # 負
print(classify(0))    # ゼロ

# sorted のキー関数として使う
students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]

# 点数でソート
sorted_by_score = sorted(students, key=lambda s: s[1], reverse=True)
print(sorted_by_score)  # [('Bob', 92), ('Alice', 85), ('Carol', 78)]

# 辞書のリストをソート
people = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
    {"name": "Carol", "age": 28}
]
sorted_by_age = sorted(people, key=lambda p: p["age"])

# 複数条件でソート(タプルを返す)
data = [("Alice", 30, "B"), ("Bob", 25, "A"), ("Carol", 30, "A")]
# 年齢昇順、その後グレード昇順
sorted_data = sorted(data, key=lambda x: (x[1], x[2]))

6. 高階関数(map / filter / sorted / functools)

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map - 全要素に関数を適用
squares = list(map(lambda x: x**2, nums))
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# filter - 条件を満たす要素だけ残す
evens = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4, 6, 8, 10]

# 内包表記の方が Pythonic(推奨)
squares = [x**2 for x in nums]
evens = [x for x in nums if x % 2 == 0]

# functools.reduce - 要素を蓄積して1つの値に
from functools import reduce
product = reduce(lambda acc, x: acc * x, nums)  # 全要素の積
# 1*2*3*4*5*6*7*8*9*10 = 3628800

# functools.partial - 引数を部分的に固定した新しい関数を作る
from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(4))   # 16
print(cube(3))     # 27

# functools.lru_cache - キャッシュで再帰関数を高速化
from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(50))  # 瞬時に計算

7. クロージャ

クロージャは、外側のスコープの変数を「記憶」する内側の関数です。状態を持つ関数ファクトリーを作るのに使います。

# カウンターファクトリー
def make_counter(start=0, step=1):
    count = start
    def counter():
        nonlocal count
        current = count
        count += step
        return current
    return counter

c1 = make_counter()
c2 = make_counter(10, 2)

print(c1(), c1(), c1())    # 0 1 2
print(c2(), c2(), c2())    # 10 12 14

# 掛け算ファクトリー
def multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = multiplier(2)
triple = multiplier(3)
print(double(5))    # 10
print(triple(5))    # 15

# ロガーファクトリー
def make_logger(prefix):
    def log(message):
        print(f"[{prefix}] {message}")
    return log

info_log = make_logger("INFO")
error_log = make_logger("ERROR")

info_log("処理開始")     # [INFO] 処理開始
error_log("接続失敗")    # [ERROR] 接続失敗

8. デコレーター

デコレーターは関数を「ラップ」して機能を追加する仕組みです。@ 構文で使います。

import time
import functools

# シンプルなデコレーター(実行時間の計測)
def timer(func):
    @functools.wraps(func)   # 元の関数の情報を保持
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} の実行時間: {elapsed:.4f}秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.5)
    return "完了"

slow_function()  # slow_function の実行時間: 0.5xxx秒

# 引数を持つデコレーター
def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hello():
    print("Hello!")

say_hello()   # Hello! が3回出力

# ログデコレーター
def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"呼び出し: {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"戻り値: {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 5)
# 呼び出し: add(3, 5)
# 戻り値: 8

# クラスベースのデコレーター
class Retry:
    def __init__(self, max_attempts=3):
        self.max_attempts = max_attempts

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(self.max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"試行 {attempt+1}/{self.max_attempts} 失敗: {e}")
            raise RuntimeError("最大試行回数を超えました")
        return wrapper

@Retry(max_attempts=3)
def flaky_function():
    import random
    if random.random() < 0.7:
        raise ValueError("ランダムエラー")
    return "成功"

9. ジェネレーター

ジェネレーターは値を一つずつ生成する特殊な関数です。大量のデータを扱う際にメモリを節約できます。

# yield を使ったジェネレーター関数
def count_up(start, stop):
    current = start
    while current <= stop:
        yield current   # 値を返しつつ一時停止
        current += 1

gen = count_up(1, 5)
print(next(gen))  # 1
print(next(gen))  # 2
for n in count_up(1, 5):
    print(n)       # 1, 2, 3, 4, 5

# フィボナッチ数列ジェネレーター(無限)
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# ファイルを行ごとに読む(大容量ファイルに有効)
def read_large_file(filepath):
    with open(filepath, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()

# yield from - 別のイテラブルに委譲
def chain(*iterables):
    for it in iterables:
        yield from it

for x in chain([1, 2], [3, 4], [5]):
    print(x)   # 1, 2, 3, 4, 5

# send() - 外部から値を送り込む
def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)          # 最初のyieldまで進める
print(acc.send(10))   # 10
print(acc.send(20))   # 30
print(acc.send(30))   # 60

10. クラスと特殊メソッド(概要)

クラスはデータ(属性)と処理(メソッド)をまとめたものです。関数と深く関わる重要な概念です。

class Circle:
    """円を表すクラス"""
    pi = 3.14159   # クラス変数

    def __init__(self, radius):   # コンストラクタ
        self.radius = radius      # インスタンス変数

    def area(self):               # インスタンスメソッド
        return Circle.pi * self.radius ** 2

    def perimeter(self):
        return 2 * Circle.pi * self.radius

    @classmethod
    def from_diameter(cls, diameter):  # クラスメソッド
        return cls(diameter / 2)

    @staticmethod
    def is_valid_radius(r):        # スタティックメソッド
        return r > 0

    def __str__(self):             # str() 変換
        return f"Circle(r={self.radius})"

    def __repr__(self):            # repr() 変換
        return f"Circle({self.radius!r})"

    def __eq__(self, other):       # == 比較
        return self.radius == other.radius

    def __lt__(self, other):       # < 比較
        return self.radius < other.radius

# 使用例
c = Circle(5)
print(c.area())         # 78.53975
print(c.perimeter())    # 31.4159
print(str(c))           # Circle(r=5)
c2 = Circle.from_diameter(10)
print(c == c2)          # True

# データクラス(Python 3.7+)
from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float
    z: float = 0.0   # デフォルト値

    def distance_from_origin(self):
        return (self.x**2 + self.y**2 + self.z**2) ** 0.5

p = Point(3.0, 4.0)
print(p)                         # Point(x=3.0, y=4.0, z=0.0)
print(p.distance_from_origin())  # 5.0

11. 関数設計の鉄則・実践ヒント

関数の文法を覚えた後は、「どう設計するか」が次の壁になります。読みやすく、再利用しやすく、テストしやすい関数の書き方には共通の原則があります。ここでは、実務で関数を書くときに意識するとコードの質が一段上がる指針をまとめます。

11.1 単一責任の原則 — 関数は「1 つのこと」だけする

1 つの関数が複数のことをやろうとすると、名前を付けにくく、テストしにくく、再利用しにくくなります。関数名に「と(and)」が入りそうになったら、分割の合図です。

# ❌ 複数のことをやっている
def fetch_and_save_user(user_id, filename):
    user = fetch_user(user_id)         # 取得
    user["fetched_at"] = now()         # 加工
    with open(filename, "w") as f:     # 保存
        json.dump(user, f)
    return user

# ✅ 役割ごとに分ける
def fetch_user(user_id):
    return api_call(f"/users/{user_id}")

def add_timestamp(user):
    return {**user, "fetched_at": now()}

def save_to_file(data, filename):
    with open(filename, "w") as f:
        json.dump(data, f)

# 呼び出し側で組み合わせる
user = save_to_file(add_timestamp(fetch_user(123)), "user.json")

11.2 引数は 3 つまでが理想・5 つを超えたら設計を見直す

引数が多い関数は、呼び出すときに引数の意味を覚えていられず、テストもしづらくなります。関連する引数をまとめてオブジェクト(dataclass や辞書)にするのが定石です。

# ❌ 引数が多すぎる
def send_email(to, subject, body, cc, bcc, attachments, smtp_host, smtp_port, user, password):
    ...

# ✅ 設定はオブジェクトに、メッセージは別に
from dataclasses import dataclass

@dataclass
class SMTPConfig:
    host: str
    port: int = 587
    user: str = ""
    password: str = ""

@dataclass
class Mail:
    to: str
    subject: str
    body: str
    cc: list = None
    bcc: list = None
    attachments: list = None

def send_email(mail: Mail, config: SMTPConfig):
    ...

11.3 型ヒント(Type Hints)を書く — 「動くドキュメント」になる

型ヒントは Python 3.5 以降の正式な機能で、関数の入出力をコードそのものに記述できます。実行時には型チェックされませんが、VSCode 等のエディタで補完が効くようになり、人間が読むときの理解速度が劇的に上がります。mypy などの型チェッカと組み合わせれば、実行前にバグを発見できます。

from typing import Optional

# ❌ 型ヒントなし — 何を渡せばいいか不明
def fetch(user_id, retry=3):
    ...

# ✅ 型ヒントあり — 関数だけ見て意図が読み取れる
def fetch(user_id: int, retry: int = 3) -> Optional[dict]:
    """ユーザー情報を取得。見つからなければ None を返す。"""
    ...

# Python 3.10 以降は | を使った Union が書ける
def parse(value: str) -> int | float:
    if "." in value:
        return float(value)
    return int(value)

11.4 docstring で「使い方」を書く — help() で読める

関数の最初に書く三重引用符の文字列が docstring です。help(関数名) や IDE のホバー表示で参照され、関数の「使い方説明書」になります。可能なら引数・戻り値・例外・使用例まで書くと、半年後の自分が救われます。

def calculate_bmi(weight_kg: float, height_m: float) -> float:
    """BMI(Body Mass Index)を計算する。

    引数:
        weight_kg: 体重(キログラム)
        height_m: 身長(メートル)

    戻り値:
        BMI 値(小数 1 位まで)

    例外:
        ValueError: 身長が 0 以下のとき

    使用例:
        >>> calculate_bmi(60, 1.7)
        20.8
    """
    if height_m <= 0:
        raise ValueError("身長は 0 より大きい値を指定してください")
    return round(weight_kg / (height_m ** 2), 1)

11.5 デコレーターは「横断的関心事」に使う

ログ出力・実行時間計測・キャッシュ・認証チェックなど、複数の関数で同じ前処理/後処理を行いたいときがデコレーターの出番です。functools モジュールには実用的なデコレーターが揃っており、自作する前にまずチェックする習慣をつけましょう。

from functools import lru_cache, wraps
import time

# 結果をキャッシュ(同じ引数なら再計算しない)
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 自作デコレーター: 実行時間を計測
def timed(func):
    @wraps(func)            # 元の関数名・docstring を保持
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__}: {elapsed:.3f}秒")
        return result
    return wrapper

@timed
def slow_task():
    time.sleep(1)

11.6 ジェネレーターは「巨大データの逐次処理」に強い

リストはすべての要素をメモリに保持しますが、ジェネレーターは必要なときに 1 つずつ値を生成します。1 GB のログファイルや、終わりのない無限ストリームでもメモリを使わずに処理できます。「全件をリストに溜める前に、1 件ずつ処理できるか」を考える習慣が、効率的なコードへの近道です。

# ❌ 全行をメモリに読み込む(巨大ファイルでメモリ不足)
def read_errors(path):
    with open(path) as f:
        lines = f.readlines()                     # 全行メモリへ
    return [line for line in lines if "ERROR" in line]

# ✅ ジェネレーター: 1 行ずつ処理しメモリ消費を一定に
def read_errors(path):
    with open(path) as f:
        for line in f:                            # 1 行ずつ読む
            if "ERROR" in line:
                yield line.rstrip("\n")

# 呼び出し側でも 1 件ずつ処理できる
for err in read_errors("huge.log"):
    print(err)

11.7 lambda vs def の使い分け — 1 行 1 用途なら lambda

lambda は「その場で 1 回だけ使う、1 行で書ける関数」のためのものです。名前を付けたい・複数行になる・複雑な処理を含むなら def を使うのが基本ルールです。lambda に名前を付けて変数に代入するくらいなら、最初から def で書きましょう。

# ✅ lambda の正しい使い方: sorted の key にその場で渡す
users = [{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}]
users.sort(key=lambda u: u["age"])

# ❌ lambda を変数に代入するのは PEP 8 で非推奨
square = lambda x: x ** 2          # こうするくらいなら
def square(x):                     # def の方が読みやすい
    return x ** 2
💡
良い関数を書く力は「他人のコードを読む」ことで身につく

関数設計のセンスは、書く量よりも「読んだ良いコードの量」に比例します。本サイトのサンプルアプリ 200 本では、ここで紹介した原則を踏まえた関数設計が多数登場します。気になるアプリを選んで、関数の分け方・命名・型ヒント・docstring を眺めるだけでも勉強になります。

🎉
関数をマスター!いよいよGUIアプリ開発へ

Pythonの関数・クラスを学びました。いよいよ実際に動くGUIアプリを作ってみましょう!

📚
関数の理解をさらに深めるには

関数設計やPythonらしい書き方を突き詰めたい方には、おすすめ書籍【2026年版】で紹介している Effective Python 第3版(ベストプラクティス125項目)が定番です。