Python – ファーストクラス関数 (First Class Function)

読了 6分

今回の講座では、Pythonのファーストクラス関数(First-class function)について学んでいきます。

ファーストクラス関数とは、プログラミング言語が関数 (function) を first-class citizen として扱うことを意味します。簡単に説明すると、関数自体を引数 (argument) として他の関数に渡したり、他の関数の戻り値として返したり、関数を変数に代入したり、データ構造の中に保存できる性質を指します。

少し難しいでしょうか。それでは実際にコードを書きながら説明していきましょう。

実習のため、お好きなディレクトリの中に first_class_function.py という名前のファイルを一つ作成して、次のコードを入力してください。

first_class_function.py
def square(x):
return x * x

print(square(5))

f = square

print(square)
print(f)

ファイルを保存したら、ファイルが保存されたディレクトリでターミナルやコマンドプロンプトを開き、次のコマンドでPythonファイルを実行してみましょう。

$ python first_class_function.py
25
<function square at 0x000001FE1433D1F0>
<function square at 0x000001FE1433D1F0>

上のコードでは、ごく簡単な関数「square」を定義して呼び出しました。続いて square 関数を f という変数に代入してから、squaref の値を出力してみました。どちらもメモリアドレス値である 0x1018dfe60 に保存された square 関数オブジェクトが代入されているのが分かります。それでは、f も本当に関数のように呼び出せるか見てみましょう。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
def square(x):
return x * x

f = square

print(f(5))
$ python first_class_function.py
25

f(5) という構文で square 関数を呼び出しているのが分かります。先ほど触れたように、プログラミング言語がファーストクラス関数をサポートしていれば、今やってみたように変数に関数を代入できるだけでなく、引数として他の関数に渡したり、関数の戻り値としても使えます。次の例を見ながら説明します。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
def square(x):
return x * x

def my_map(func, arg_list):
result = []
for i in arg_list:
result.append(func(i)) # square 関数を呼び出し, func == square
return result

num_list = [1, 2, 3, 4, 5]

squares = my_map(square, num_list)

print(squares)
$ python first_class_function.py
[1, 4, 9, 16, 25]

my_map 関数に square 関数を引数として渡してから、for ループの中で square 関数を呼び出しているのが分かります。ところで、下のように simple_square 関数一つで問題を解決すればいいのではないか、と考える方もいらっしゃるでしょう。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
def square(x):
return x * x

num_list = [1, 2, 3, 4, 5]

def simple_square(arg_list):
result = []
for i in arg_list:
result.append(i * i)
return result

simple_squares = simple_square(num_list)

print(simple_squares)
$ python first_class_function.py
[1, 4, 9, 16, 25]

おや?もっと簡単なコードで同じ結果が出ました。そうです、単純に関数一つだけを実行したいときは、simple_square のような通常の関数を使って同じ結果を出すこともできます。しかし、ファーストクラス関数を使うと、すでに定義された複数の関数を簡単に再利用できるという利点があります。下の例をもう一度見てみましょう。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
def square(x):
return x * x

def cube(x):
return x * x * x

def quad(x):
return x * x * x * x

def my_map(func, arg_list):
result = []
for i in arg_list:
result.append(func(i))  # square 関数を呼び出し, func == square
return result

num_list = [1, 2, 3, 4, 5]

squares = my_map(square, num_list)
cubes = my_map(cube, num_list)
quads = my_map(quad, num_list)

print(squares)
print(cubes)
print(quads)
$ python first_class_function.py
[1, 4, 9, 16, 25]
[1, 8, 27, 64, 125]
[1, 16, 81, 256, 625]

上の例のように、すでに定義されている squarecubequad のような複数の関数があるとき、my_map のような wrapper 関数を一つだけ定義しておけば、既存の関数を修正することなく便利に使えるのです。

それでは今度は、関数の戻り値としてまた別の関数をreturnする方法を見ていきましょう。とても簡単なロギング関数を作ってみます。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
def logger(msg):
def log_message():  # 1
print('Log: ', msg)

    return log_message

log_hi = logger('Hi')
print(log_hi)  # log_message オブジェクトが出力されます。
log_hi()  # "Log: Hi" が出力されます。
$ python first_class_function.py
<function logger.<locals>.log_message at 0x0000022AB43EAA60>
Log:  Hi

上の #1 で定義された log_message という関数を logger 関数の戻り値として返し、log_hi という変数に代入してから呼び出しているのが分かります。ところで、ここで一つ特異な点に気づきます。msg のような関数のローカル変数の値は、関数が呼び出された後にメモリ上から消えるので再び参照できないはずなのに、msg に代入されていた ‘Hi’ の値が、logger 関数が終了した後でも参照できているのです。このような log_message のような関数を「クロージャ (closure)」と呼び、クロージャは他の関数のローカル変数を、その関数が終了した後でも記憶することができます。log_message が本当に記憶しているのか、msg 変数をローカル変数として持つ logger 関数をグローバル名前空間から完全に消した後で、log_message を呼び出してみましょう。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
def logger(msg):
def log_message():  # 1
print('Log: ', msg)

    return log_message


log_hi = logger('Hi')
print(log_hi)  # log_message オブジェクトが出力されます。
log_hi()  # "Log: Hi" が出力されます。

del logger  # グローバル名前空間から logger オブジェクトを削除します。

# logger オブジェクトが削除されたことを確認します。
try:
print(logger)
except NameError:
print('NameError: logger は存在しません。')

log_hi()  # logger が削除された後でも "Log: Hi" が出力されます。
$ python first_class_function.py
<function logger.<locals>.log_message at 0x0000022EC0BBAAF0>
Log:  Hi
NameError: logger は存在しません。
Log:  Hi

logger が消された後でも log_hi() を実行して log_message が呼び出されているのが分かります。

logger 関数を完全に削除した後でも log_message 関数は ‘Hi’ を記憶していることを確認しました。このようにクロージャは色々と便利に使われる場面が多いのですが、クロージャについては別の講座で詳しく見ていきます。

それでは、もう少し実用的な例を見ていきましょう。

次のようにコードを修正して保存し、実行してください。

first_class_function.py
# シンプルな通常関数
def simple_html_tag(tag, msg):
print('<{0}>{1}<{0}>'.format(tag, msg))


simple_html_tag('h1', 'シンプルな見出しタイトル')

print('-' * 30)


# 関数を返す関数
def html_tag(tag):
def wrap_text(msg):
print('<{0}>{1}<{0}>'.format(tag, msg))

    return wrap_text


print_h1 = html_tag('h1')  # 1
print(print_h1)  # 2
print_h1('1番目の見出しタイトル')  # 3
print_h1('2番目の見出しタイトル')  # 4

print_p = html_tag('p')
print_p('これはパラグラフです。')
$ python first_class_function.py
<h1>シンプルな見出しタイトル<h1>
------------------------------
<function html_tag.<locals>.wrap_text at 0x00000272C3CFAAF0>
<h1>1番目の見出しタイトル<h1>
<h1>2番目の見出しタイトル<h1>
<p>これはパラグラフです。<p>

19行目#1で html_tag 関数を print_h1 変数に代入し、20行目#2で変数の値を出力すると、wrap_text 関数オブジェクトが代入されていることが確認できます。21行目#3と22行目#4では文字列を渡して wrap_text 関数を呼び出しています。ここで「wrapper関数を作らずに simple_html_tag のような通常の関数を使えば良いのではないか」と疑問に思うかもしれません。しかし、html_tag のような高階関数(higher-order function)の動作を理解しておくことで、後続のクロージャ(closure)、デコレータ(decorator)、ジェネレータ(generator)といった概念を自然に理解できるようになります。

次回はクロージャを取り上げます。

X