Q1次のコードを実行したときの出力はどれですか?def greet():
print("Hi")
f = greet
f()
高階関数 — 関数を引数や戻り値として扱う
関数も値として変数代入・引数渡し・戻り値返しができる。a = printの別名、greet_all(names, formatter)のコールバック、成功/失敗で呼び分ける分岐、設定済み関数を返すmake_loggerを扱います。
前回の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")