Blow Up by Black Swan

Python-関数プログラミング(高階関数・デコレータ・・・)

今回は、関数関連で苦戦したところがあったので、関数の性質から高階関数、デコレータへの一連の流れを一から学習し直しましたので、それをまとめようと思います。なんでもオブジェクトと取るpythonの特徴が掴める、面白い学習でした。

1. pythonの特徴の一つ

今回の一連の流れにおけるスタート地点は、pythonならでは特徴からです。それは、pythonでは関数もオブジェクトの一つとして認識されるということです。オブジェクトは、ある種、部品かモジュールか、または要素などの一つの機能のまとまりで、それ自体何らかの機能を持ちつつ、他のものとも連携できる、そういったものです。そして、pythonではオブジェクトにオブジェクトIDというものが振られているため、変数に値を格納するのと同じように、オブジェクト機能をある変数に格納することができます。

下記がコード例です。関数に括弧がついている場合、関数機能の実行を表しますが、括弧がついていない場合、オブジェクトとしての関数を表します。

#example1->組み込み型

answer1 = int(3.14)

xxx = int  #xxxという変数にintを格納
answer2 = xxx(3.14)  #変数xxxがintの働きをしている

#確認
print(answer1, answer2)

print(id(answer1) == id(answer2))

戻り値

3 3
True

上記で変数がint関数の働きをしているのがわかると思います。また、オブジェクトIDを表示するidメソッドで取得したidが等しく、この点からもオブジェクトとしての関数が引き継がれていることがわかります。これは、自作の関数でも同様に機能します。

#example2->自分で定義した関数の場合

def my_func(args):
    return int(args ** 2 / args + 1)

answer1 = my_func(12)

yyy = my_func
answer2 = yyy(12)

print(answer1, answer2)
print(id(answer1) == id(answer2))

戻り値

13 13
True

このように、pythonでは、関数もオブジェクトのため、柔軟な対応が可能になります。私は詳しくないのですが、C言語などの関数ポインタと似ているようですが厳密には関数ポインタとは違うらしいです。

2. 高階関数

ここからは高階関数についてです。高階関数については、関数を引数でとったり、戻り値で返したりする関数のことです。以下のように3つに場合分けできるので、それぞれコード例を作ってみました。また、代表的な高階関数についても最後にコードを作ってみました。

  1. 関数を引数にとる場合
  2. 関数を戻り値で返す場合
  3. 関数を引数に取り、関数を戻り値で返す場合

2-1. 関数を引数にとる場合

まずは、関数を引数にとる場合です。

#(2)高階関数->1.関数を引数にとる

ex_list = [2, 5, 10]

answer1 = sum(ex_list) ** 2  #検証用コード

def high_func(func, args):  #関数を引数にとる関数(func)
    return func(args)  ** 2
answer2 = high_func(sum, ex_list)

print(answer1, answer2)
answer1 == answer2

戻り値

289 289
Out[0]: True

high_funcが高階関数になります。引数のfuncに関数が入ります。ここで引数に挿入されるのは、関数オブジェクトで、1章で触れたように括弧がない形で引数に入れられています。上記のコードでは、sum関数オブジェクトが指定されています。

2-2. 関数を戻り値で返す場合

次は、関数を戻り値で返す場合の高階関数です。

#(2)高階関数->2.戻り値で関数を返す
ex_list = [2, 5, 10]

def inner(list):  #一般的な関数を定義
    return sum_num(list) ** 2 
#inner(ex_list) -> 289

def high_func():  #戻り値を関数で返す関数定義(high_func関数を実行するとinner関数が戻り値として返ってくる)
    return inner

newfunc = high_func()  #変数newfuncに関数オブジェクトinnerを格納
newfunc(ex_list)  #inner(list)と同義

戻り値

289

ここでもhigh_func関数が高階関数になります。高階関数high_funcが実行されたことによる戻り値inner関数オブジェクトが、変数newfuncに格納されます。ここで、high_funcは括弧がついているので、関数機能が実行されinnerが戻り値として返されているのに対し、inner自体は括弧がついていないので、関数オブジェクトとして渡されていることがわかります。そして、newfunc(ex_list)と実行文を記載することで、実質的にinner(ex_list)を実行したことになります。このコードは一般的には以下のように記載されます。この記載の仕方が後で触れるデコレータなどの理解を難しくしている点だと個人的に思います。

ex_list = [2, 5, 10]

def high_func():  #highfuncは、関数を返す高階関数で、その関数をさらに内部で定義している
    def inner(list):
        return sum(list) ** 2
    return inner  #high_func関数の戻り値がinner関数になっている


newfunc = high_func()  #変数newfuncには関数オブジェクトinnerを格納
newfunc(ex_list)  #inner(list)と同義

以上が関数を戻り値として返す場合の高階関数です。

2-3. 関数を引数に取り、関数を戻り値で返す場合

そして、最後は上記の2つを合わせた、関数を引数に取り、関数を戻り値で返す高階関数です。このコード例は以下になります。

#example3->③関数を引数に取り、戻り値で関数を返す
ex_list = [2, 5, 10]

def high_func(func):  #引数で関数を指定
    def inner(args):  #内部で戻り値となる関数を定義
        return func(args) ** 2
    return inner

newfunc = high_func(sum)
  #変数newfuncに関数innerを格納
  #但し、inner関数は「return func(args) ** 2」->「return sum(args) ** 2」となっている

newfunc(ex_list)  #関数inner(list)と同義

引数にfuncが指定されている文、inner関数オブジェクトの仕様が若干変更されますが、基本的な処理の流れは上記2つと全く同じになります。そして、これがデコレータに繋がります。

2-4. 代表的な高階関数

デコレターの前にまず、代表的な高階関数についてまとめたいと思います。

・ map関数

map関数はシーケンスの各要素に対して、指定した処理を実行してくれる関数です。基本仕様が高階関数の形をとります。

map(func, シーケンス)

シーケンスはリストやタプルなどの複数のデータの塊を指します。以下がコード例です。

test_map = map(lambda s: s*2, prime_list)
print(tuple(test_map))
type(test_map)

ちなみにここで利用しているprime_listは、任意の範囲の素数を求める、私自作のコードで求められるリストです。以下がそのコードになりますので、興味あれば利用してみていただければと思います。

prime_list = []
#10000までの数字で素数を求めるプログラム
for i in range(2, 10001):
    k = 0
    for j in range(1,i+1):
        if i % j != 0:
            pass
        else:
            k += 1
            pass
    if k == 2:
        prime_list.append(i)
    else:
        continue

def high_func(func, list):
    return func(list)

戻り値はデータ量が大きいので、割愛していますが、各要素が2乗されたタプルが返されます。関数には無名関数のlambdaを利用していますが、自作関数をしてあげることもできます。注意点としては、map関数の戻り値はmapオブジェクトになり、そのままアクセスしてもシーケンス自体を見ることができないため、タプルやリストなどを用いて可視化できるようにする必要があります。

・ filter関数

filter関数はシーケンスの各要素の中で指定した条件を満たす要素だけを返す関数です。その名の通り、filterです。

def check_nine(args):
    if (args-1)%9==0:
        return True
    else:
        return False

test_filter = filter(check_nine, prime_list)
print(list(test_filter))

上記のコードでは、1ひくと、9で割り切れる素数のリストを返してくれます。以上が、代表的な高階関数です。高階関数自体を理解すれば、その扱いが非常に簡単なことがわかります。

3. デコレータ

次は、デコレータについてです。デコレータの説明は、どのサイトでもまず「関数を修飾する」という表現を使います。その通りですが、その機能の理解は、2-3.で説明したように実行過程を分解できれば、簡単に理解できました。以下がコード例です。

ex_list = [2, 5, 10]
def high_func(func):  #引数で関数を指定
    def inner(args):  #内部で戻り値となる関数を定義
        return func(args) ** 2
    return inner

@high_func  #high_func関数でsum_num関数をデコレート
def sum_num(list):  #sum関数と同じものを自作関数として作成(組み込み型オブジェクトにでコレータできない?)
    num = 0
    for i in list:
        num += i
    return num

sum_num(ex_list)

ここで自作関数sum_numが新たに定義されていますが、これはsum関数と全く同じ機能を持ったものですデコレータは基本的に自作関数にくっつけるものですので、新たに作っただけです。ここで、sum_num関数をhigh_func関数がデコレートしていますが、これは2-3で説明したのと全く同じ流れになります。

 ・sum_num(ex_list)が実行される
 ->high_func(sum_num)が実行され、innerが返される
 ->inner(ex_list)が実行される
 ->「sum_num(ex_list) ** 2」が戻り値として返される

2-3.であったnewfuncという仲介変数がなくなっただけですが、実行過程は全く同じになります。これが、デコレータの仕組みであり、自作関数に付帯させることで、新たな関数や機能を生み出すことができます。

4. コールバック関数

最後にコールバック関数です。最初のところで説明したように、コールバックという言葉自体が様々な使われ方をしており、コールバック関数自体についてもpythonについてはどうも高階関数を指しているようです。公式ドキュメントにもおそらく正式な説明は無さそうです。様々なを記事を読む限り、高階関数の中で2-1に当たる、関数を引数にとるタイプを指しているようです。また、そこにスコープの扱いの問題が絡んでくるという印象です。この点は、引き続き調べていきたいと思います。

5. まとめ

今回は、デコレータの理解に向けた、関数の一連の理解についてまとめました。今まで、デコレータなどはなんとなくで使っていたのですが、再度学び直したことで、自分でも利用できる自信がつきました。利用シーンについて、まだイメージできていないので、実用場面を多く作っていきたいと思います。また、コールバック関数と言われていることについても引き続き追求していきたいと思います。

読んで頂いた方、ありがとうございました。

6. 参考サイト

今回の記事の参考サイトは以下になります。