順番に読み進めながら学べます

高階関数 — 関数を引数や戻り値として扱う

Python の高階関数を基礎から解説します。関数を変数・引数・戻り値として扱う書き方と、コールバック・クロージャの実用例まで一通り図解で押さえます。

前回yield では値を 1 つずつ作る関数を見ました。今回は関数まわりの応用として、関数そのものを「値」として扱う仕組みである高階関数を整理します。

変数への代入、引数として渡すコールバック、戻り値として返すパターンの 3 つをおさえましょう。

高階関数とは — 関数も「値」として扱える

Python では、整数や文字列と同じように関数も値として扱えます。変数に入れたり、別の関数の引数として渡したり、戻り値として返したりできます。

関数を引数で受け取る、または関数を戻り値で返す関数を高階関数(higher-order function)と呼びます。print()len() のように単に値を扱う関数とは違って、高階関数は処理そのものを部品として組み合わせます

高階関数が扱う 3 つのパターン
①関数を変数に代入f = printf("HELLO")②関数を引数で渡すafter(2, greet)→ コールバック③関数を戻り値で返すmake_greeter("太郎")→ 関数を返す
関数は値として扱えるため、変数代入・引数渡し・戻り値返しの 3 通りに使えます。

関数を変数に代入する — 関数も参照される値

def で関数を定義すると、関数の本体がメモリ上に作られて、その場所を関数名が指します。a = print のように代入すれば、a も同じ関数本体を指すようになり、a("HELLO")print("HELLO") がまったく同じ動きをします。

関数名の後ろに ()付けないのがポイントです。() を付けると関数を実行してしまい、戻り値が代入されてしまいます。

関数名は関数オブジェクトを指す名前
print(組み込み名)関数オブジェクト(本体)実行結果HELLOa(自前の別名)参照同じ参照print("HELLO")a("HELLO")
a = print              # () を付けないので「関数本体」を a に代入
a("HELLO")            # HELLO  ← print("HELLO") と同じ

print(id(print))       # たとえば 4395020128
print(id(a))           # 同じ番地が表示される

# 自分で定義した関数も同じ
def greet():
    print("こんにちは")

say = greet            # 別名 say を作る
say()                  # こんにちは

() を付けるか付けないかで意味が変わる

a = print関数を渡す書き方、a = print("HELLO")関数を呼んで結果を渡す書き方です。後者の場合、print("HELLO") の戻り値(None)が a に入るので、a() を呼ぶと TypeError: 'NoneType' object is not callable になります。

関数も値として扱えることを、自分の関数と組み込み関数で体感します。

def greet(): を定義し、本体に print("こんにちは") を書いてください。

say = greet で別名を作り、say() を呼んで greet() と同じ結果になることを確認してください。

f = len のように組み込みの len も別名にして、f("Python") の結果を print(...) で表示してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

関数を引数として渡す — コールバック

高階関数の最も典型的な使い方がコールバックです。「他の関数に渡されて、特定のタイミングで呼び出される関数」のことで、「処理の中身は呼び出し側が決めます」。

たとえば「3 件の名前リストにあいさつを表示する」処理で、あいさつの形式だけを呼び出し側が差し替えたいとします。本体は名前を 1 件ずつ取り出すループだけにして、フォーマット部分を関数として受け取れば、呼び出し側はあいさつの定型を変えるだけで再利用できます。

def greet_all(names, formatter):
    for name in names:
        print(formatter(name))

def formal(name):
    return f"{name} 様、いつもありがとうございます。"

def casual(name):
    return f"よっ、{name}!"

greet_all(["太郎", "花子", "次郎"], formal)
# 太郎 様、いつもありがとうございます。
# 花子 様、いつもありがとうございます。
# 次郎 様、いつもありがとうございます。

greet_all(["太郎", "花子", "次郎"], casual)
# よっ、太郎!
# よっ、花子!
# よっ、次郎!
コールバックは「中の処理を渡す」しくみ
呼び出し側greet_all(names, formal)高階関数greet_all(names, formatter)name ごとにformatter(name) を呼ぶコールバックformal(name)「太郎 様、…」3 件分の出力関数を渡す本体差し替え可能結果
greet_all の本体はループだけで、名前をどう挨拶文に変換するかは引数 formatter に任せます。

ユーザー名のリストに対して、通知メッセージの作り方を呼び出し側が差し替えられる高階関数を作ります。

def notify_all(users, formatter): を定義し、for user in users: で各ユーザーに対して print(formatter(user)) を呼んでください。

def login_alert(name): を作り、return f"[ログイン] {name} さんが入室しました" を返してください。

def logout_alert(name): を作り、return f"[ログアウト] {name} さんが退室しました" を返してください。

users = ["太郎", "花子"] を用意し、notify_all(users, login_alert)notify_all(users, logout_alert) を呼び出してください。

Python エディタ

コードを実行してください

用途別コールバックで分岐する

コールバックは 1 つだけ渡せるとは限りません。「成功したらこっちの関数、失敗したらあっちの関数」のように、複数のコールバックを使い分けたい場面でもコールバックは使えます。

たとえば数値が偶数か奇数かで処理を切り替えたいとき、判定する側の関数は if で分岐するだけにしておき、実際の処理は呼び出し側が用意した 2 つの関数に任せる、という書き方ができます。

def process_number(number, even_callback, odd_callback):
    if number % 2 == 0:
        even_callback(number)
    else:
        odd_callback(number)

def handle_even(n):
    print(f"{n} は偶数です")

def handle_odd(n):
    print(f"{n} は奇数です")

process_number(4, handle_even, handle_odd)   # 4 は偶数です
process_number(7, handle_even, handle_odd)   # 7 は奇数です
条件で呼ぶコールバックを切り替える
process_number(7,handle_even,handle_odd)n % 2 == 0?even_callback(n)→ handle_evenodd_callback(n)→ handle_odd判定TrueFalse

決済処理を成功時/失敗時で別のコールバックに振り分ける高階関数を作ります。

def process_payment(amount, on_success, on_failure): を定義し、amount > 0 なら on_success(amount)、そうでなければ on_failure(amount) を呼んでください。

def notify_success(amount): を作り、print(f"{amount} 円の決済が完了しました") を表示してください。

def notify_failure(amount): を作り、print(f"金額 {amount} は不正です") を表示してください。

process_payment(1500, notify_success, notify_failure)process_payment(0, notify_success, notify_failure) をそれぞれ呼び出してください。

Python エディタ

コードを実行してください

関数を戻り値として返す — クロージャの実用

高階関数のもう 1 つのパターンが「関数を戻り値として返す」です。return で内部関数を返す書き方はクロージャで扱いましたが、ここでは「設定値を覚えた関数を呼び出し側に渡す」という活用方法に焦点を当てて整理します。

たとえば毎回呼び出すたびに同じプレフィックス(接頭辞)を付けたいログ関数を量産したい場合、make_logger("INFO")INFO 用ロガーmake_logger("ERROR")ERROR 用ロガーを作っておけば、使う側は info("処理を開始") のように短い呼び出しで済むようになります。

def make_logger(prefix):
    def log(message):
        print(f"[{prefix}] {message}")
    return log               # 関数自体を返す

info = make_logger("INFO")
error = make_logger("ERROR")

info("処理を開始しました")    # [INFO] 処理を開始しました
error("接続に失敗しました")   # [ERROR] 接続に失敗しました
info("処理が完了しました")    # [INFO] 処理が完了しました
make_logger が「設定済みの関数」を返す
モジュール(グローバル名前空間)
  • info = make_logger("INFO") — INFO を覚えた関数を取得
  • error = make_logger("ERROR") — ERROR を覚えた別の関数を取得
make_logger("INFO") のフレーム
  • prefix = "INFO" を保持
  • 中で定義した logreturn
log(INFO を覚えたまま)
  • 呼び出されるたびに [INFO] ... を表示
make_logger("ERROR") のフレーム
  • prefix = "ERROR" を保持
  • 別の log 関数を return
log(ERROR を覚えたまま)
  • info とは独立した別の関数オブジェクト
make_logger を呼ぶたびに、prefix を覚えた別個の log 関数が作られて戻り値として返される。呼び出し側は info() / error() のように短い名前で使える。

プレフィックスを覚えたタグ付け関数を返す高階関数を作ります。

def make_tagger(tag): を定義し、その中に def tag_text(text): を作って return f"<{tag}>{text}</{tag}>" を返してください。

② 最後に return tag_text で内部関数を返してください。

b = make_tagger("b")i = make_tagger("i") で 2 つの関数を作ってください。

print(b("重要"))print(i("強調")) を呼び出して、それぞれ <b>重要</b> / <i>強調</i> が表示されることを確認してください。

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のコードを実行したときの出力はどれですか?
def greet():
print("Hi")
f = greet
f()

Q2次のコードのうち、コールバックとして say_hi を渡しているのはどれですか?

Q3次のコードを実行したときの info("OK") の出力はどれですか?
def make_logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
info = make_logger("INFO")