Q1次のコードを実行したときの出力はどれですか?def greet():
print("Hi")
f = greet
f()
高階関数 — 関数を引数や戻り値として扱う
Python の高階関数を基礎から解説します。関数を変数・引数・戻り値として扱う書き方と、コールバック・クロージャの実用例まで一通り図解で押さえます。
前回の yield では値を 1 つずつ作る関数を見ました。今回は関数まわりの応用として、関数そのものを「値」として扱う仕組みである高階関数を整理します。
変数への代入、引数として渡すコールバック、戻り値として返すパターンの 3 つをおさえましょう。
高階関数とは — 関数も「値」として扱える
Python では、整数や文字列と同じように関数も値として扱えます。変数に入れたり、別の関数の引数として渡したり、戻り値として返したりできます。
関数を引数で受け取る、または関数を戻り値で返す関数を高階関数(higher-order function)と呼びます。print() や len() のように単に値を扱う関数とは違って、高階関数は処理そのものを部品として組み合わせます。
関数を変数に代入する — 関数も参照される値
def で関数を定義すると、関数の本体がメモリ上に作られて、その場所を関数名が指します。a = print のように代入すれば、a も同じ関数本体を指すようになり、a("HELLO") と print("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 になります。
関数を引数として渡す — コールバック
高階関数の最も典型的な使い方がコールバックです。「他の関数に渡されて、特定のタイミングで呼び出される関数」のことで、「処理の中身は呼び出し側が決めます」。
たとえば「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 の本体はループだけで、名前をどう挨拶文に変換するかは引数 formatter に任せます。用途別コールバックで分岐する
コールバックは 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 は奇数です
関数を戻り値として返す — クロージャの実用
高階関数のもう 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] 処理が完了しました
- info = make_logger("INFO") — INFO を覚えた関数を取得
- error = make_logger("ERROR") — ERROR を覚えた別の関数を取得
prefix = "INFO"を保持- 中で定義した
logをreturn
- 呼び出されるたびに
[INFO] ...を表示
prefix = "ERROR"を保持- 別の
log関数をreturn
infoとは独立した別の関数オブジェクト
理解度チェック
まずは1問ずつ答えてみましょう。
Q2次のコードのうち、コールバックとして say_hi を渡しているのはどれですか?
Q3次のコードを実行したときの info("OK") の出力はどれですか?def make_logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
info = make_logger("INFO")