高度なパラメータとスコープ

関数の基本を学んだので、高度なテクニックを探ってみましょう。パラメータの書き方にはどのような方法があるでしょうか?「変数のスコープ」とは何でしょうか?再帰とは何でしょうか?これらの概念により、関数に対する理解が次のレベルに進みます。


1. デフォルトパラメータ

デフォルトパラメータを使うと、関数呼び出し時に引数を省略できます:

例:基本的なデフォルトパラメータ

PYTHON
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Alice")              # Hello, Alice!
greet("Bob", "Hi")          # Hi, Bob!
greet("Charlie", "Hey")     # Hey, Charlie!
▶ 試してみよう

⚠️ デフォルトパラメータの落とし穴

デフォルトパラメータの値は関数定義時に一度だけ評価され、関数が呼び出されるたびに評価されるわけではありません。デフォルトがミュータブルなオブジェクト(リストや辞書など)の場合、予期しない動作を引き起こす可能性があります:

例:ミュータブルなデフォルトの落とし穴

PYTHON
def add_item(item, items=[]):   # ❌ 危険!
    items.append(item)
    return items

print(add_item("apple"))     # ['apple']
print(add_item("banana"))    # ['apple', 'banana'] — ['banana'] ではない!
print(add_item("orange"))    # ['apple', 'banana', 'orange']
▶ 試してみよう

なぜ? 空のリスト items=[] は関数定義時に一度だけ作成されます。後続のすべての呼び出しは同じリストに追加します。

正しい方法: デフォルトに None を使用し、関数内で新しいリストを作成します:

例:安全なデフォルトパラメータ

PYTHON
def add_item(item, items=None):  # ✅ 安全
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("apple"))     # ['apple']
print(add_item("banana"))    # ['banana']
print(add_item("orange"))    # ['orange']
▶ 試してみよう
💡 黄金律: デフォルトパラメータは常にイミュータブルな値(None、数値、文字列、タプル)にすべきです。リストや辞書のようなミュータブルなオブジェクトをデフォルトとして決して使用しないでください。


2. キーワード引数

関数を呼び出すときに、パラメータ名で値を指定でき、順序を入れ替えて引数を渡せます:

例:キーワード引数

PYTHON
def create_user(name, age, city):
    print(f"User: {name}, {age} years old, from {city}")

# 位置引数 — 順序通りに指定する必要がある
create_user("Alice", 25, "Beijing")

# キーワード引数 — パラメータ名を指定、順序は問わない
create_user(age=30, city="Shanghai", name="Bob")
create_user(name="Charlie", city="Guangzhou", age=22)

# 混合 — 位置引数を先に、キーワード引数を後に
create_user("Diana", city="Shenzhen", age=28)
# create_user(name="Eve", 35, "Chengdu")   ❌ キーワード引数を位置引数の前に置けない
▶ 試してみよう

3. *args:可変長位置引数

関数がいくつの引数を受け取るかわからない場合は、*args を使います:

例:*args を使った合計関数

PYTHON
def sum_all(*numbers):
    """任意の数の数値を受け取り、その合計を返す"""
    total = 0
    for n in numbers:
        total += n
    return total

print(sum_all(1, 2))                # 3
print(sum_all(1, 2, 3, 4, 5))      # 15
print(sum_all())                    # 0 — 引数なしでも動作
▶ 試してみよう

*args はすべての位置引数をタプルにまとめます:

例:args の調査

PYTHON
def show_args(*args):
    print(f"Received {len(args)} arguments: {args}")
    for i, arg in enumerate(args, 1):
        print(f"  #{i}: {arg}")

show_args("apple", "banana", "orange")
▶ 試してみよう

4. **kwargs:可変長キーワード引数

**kwargs は任意の数のキーワード引数を受け取り、辞書にまとめます:

例:**kwargs を使ったユーザープロファイル作成

PYTHON
def create_profile(**info):
    """ユーザープロファイルを作成、任意の量の情報を受け付ける"""
    print("User Profile:")
    for key, value in info.items():
        print(f"  {key}: {value}")

create_profile(name="Alice", age=25, city="Beijing", job="Engineer")
▶ 試してみよう

出力:

TEXT
User Profile:
  name: Alice
  age: 25
  city: Beijing
  job: Engineer

例:*args と **kwargs の組み合わせ

PYTHON
def advanced_function(a, b, *args, **kwargs):
    print(f"Positional: a={a}, b={b}")
    print(f"Extra positional: {args}")
    print(f"Keyword: {kwargs}")

advanced_function(1, 2, 3, 4, 5, name="Alice", age=25)
# Positional: a=1, b=2
# Extra positional: (3, 4, 5)
# Keyword: {'name': 'Alice', 'age': 25}
▶ 試してみよう
💡 パラメータの順序は固定: 通常パラメータ → *args → デフォルトパラメータ → **kwargs


5. スコープ:LEGB ルール

変数はどこからアクセスできるでしょうか?これはスコープによって決まります。Python には 4 つのスコープレベルがあります:

例:LEGB スコープのネスト

PYTHON
# グローバルスコープ
x = 10          # グローバル変数

def outer():
    # エンクロージング関数スコープ
    x = 20      # これは outer のローカル x。グローバル x とは異なる
    
    def inner():
        # ローカルスコープ
        x = 30  # これは inner のローカル x
        print(f"inner: {x}")     # 30
    
    inner()
    print(f"outer: {x}")         # 20

outer()
print(f"global: {x}")            # 10
▶ 試してみよう

LEGB の意味:

Python は変数を L → E → G → B の順で検索します。

global と nonlocal

関数内でグローバル変数を変更するには、global を使います:

例:global の使用

PYTHON
count = 0

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

increment()
increment()
print(count)         # 2
▶ 試してみよう
⚠️ global の使用は最小限に。 グローバル変数はコードの理解とデバッグを困難にします。関数はパラメータで入力を受け取り、return で出力を返すべきであり、グローバル変数を密かに変更すべきではありません。


6. 再帰の基本

再帰は関数が自分自身を呼び出すことです。核となる考え方:大きな問題をより小さな同様のサブ問題に分割します。

例:再帰的階乗

PYTHON
def factorial(n):
    """n!(n の階乗)を計算"""
    if n <= 1:
        return 1          # 終了条件
    return n * factorial(n - 1)   # 再帰呼び出し

print(factorial(5))        # 120 (5×4×3×2×1)
▶ 試してみよう

実行過程:

TEXT
factorial(5) = 5 * factorial(4)
            = 5 * 4 * factorial(3)
            = 5 * 4 * 3 * factorial(2)
            = 5 * 4 * 3 * 2 * factorial(1)
            = 5 * 4 * 3 * 2 * 1
            = 120

例:フィボナッチ数列(難易度 ⭐⭐⭐)

PYTHON
def fibonacci(n):
    """n 番目のフィボナッチ数を返す(0 から開始)"""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 最初の 10 個を出力
for i in range(10):
    print(fibonacci(i), end=" ")
# 出力:0 1 1 2 3 5 8 13 21 34
▶ 試してみよう
💡 再帰の 2 つの必須要素:終了条件(これがないと無限再帰→スタックオーバーフロー)。② 再帰条件(各呼び出しが終了条件に近づく)。すべての問題が再帰に適しているわけではありません——ループで解決できるならループを優先しましょう。再帰は木構造、分割統治アルゴリズムなどに最適です。


よくあるユースケース


❓ よくある質問

Q *args**kwargs の名前を変更できますか?
A はい。*argskwargs は命名規則に過ぎません——重要なのはアスタリスクです。*numbersoptions と書くこともできます。しかし、他の Python プログラマーがすぐに理解できるよう、慣習に従うことをお勧めします。
⚠️ Q:再帰 vs ループ——どちらが優れていますか? A:ループで問題が解決できるなら、ループを優先してください。ループはパフォーマンスが良く、スタックオーバーフローを起こさず、理解しやすいです。再帰は 2 つのシナリオに適しています:木構造の走査(ディレクトリ、JSON)と数学的に再帰的な定義(階乗、フィボナッチ)です。再帰はより簡潔ですが、理解が難しいです。

Q 関数内でグローバル変数を読み取れるのに、変更できないのはなぜですか?
A 読み取り時、Python は LEGB ルールに従ってグローバル変数を見つけます。しかし、代入時にはデフォルトで新しいローカル変数を作成します——関数内の x = 10 は「ローカル変数 x を作成する」と扱われます。本当にグローバルを変更する必要がある場合は global を使います。ただし、それは通常、設計を改善できることを意味します。

📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):関数 multiply(*nums) を書いてください。任意の数の数値を受け取り、その積を返します。引数がない場合は 0 を返します。

  2. 中級(難易度 ⭐⭐):関数 create_student(name, **scores) を書いてください。学生名と任意の数の科目スコア(キーワード引数)を受け取り、平均を計算して返します。呼び出し例:create_student("Alice", Chinese=85, Math=92, English=78)

  3. 挑戦(難易度 ⭐⭐⭐):再帰関数 sum_digits(n) を書いてください。正の整数の各位の和を計算します。例:sum_digits(123)1+2+3=6 を返します。ヒント:10 で割った余りが最後の桁。10 で割った整数部分がそれを除去。終了条件:n < 10。

100%