何気にPythonでつかっていた関数型プログラミング技法いろいろ ~ 高階関数・関数の再帰呼び出し、ネスト関数(closure)、遅延評価、カリー化(関数_部分適用)

  • 144
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

最近、いろいろな言語で続々、実装されていく関数型プログラミング技法。
今回は、Python2.7 に標準( 一部、import )に備わっていて、Python で いますぐにでもおこなえる 関数型プログラミング技法の実現手法 & 利用モジュール と メソッドについて、1ヶ所にまとめて整理してみた。 :blush:


( 目次 )

  • map() で 高階関数を実現
  • filter() で 高階関数を実現
  • reduce() で 高階関数を実現 & 再帰呼び出し関数
  • closure で 高階関数を実現
  • functools.partial() でカリー化(関数の部分適用)を実現
  • generator(ジェネレータ式)で遅延評価を実装
  • yield(イールド式)で遅延評価を実現
以下のコードは、この記事末尾に記載した数多くの参考ウェブサイトを参考、引用しています

map() は、関数を引数に受け取る「高階関数」(関数型言語)に相当

Python2.7
data_list = [1, 2, 3, 4, 5]

def square(x):
    return x**2

map(square, data_list)

p1.png

(ラムダ式(無名関数)で書くと1コード行で書ける)

Python2.7
map(lambda x: x**2, data_list)

p2.png

map()メソッドを使った上記の記法は、関数を引数で受け取る関数(=高階関数)を書いた以下のコードと等価です。

Python2.7
def func(x):
    return x**2

もしくは

Python2.7
func = lamda x: x**2


def map_func(func, data_list):
    result_list = []

    for x in data_list:
        result_list.append(func(x))
    return result_list

map_func(func, data_list)

p3.png


filter() も、関数を引数に受け取る「高階関数」(関数型言語)に相当

Python2.7
data_list = [1, 2, 3, 4, 5]

def is_odd(x):
    return x%2 == 1

filter(is_odd, data_list)

p4.png

(ラムダ式(無名関数)で書くと1コード行で書ける)

Python2.7
filter(lambda x:x%2==1, data_list)

p5.png

filter()メソッドを使った上記の記法は、関数を引数で受け取る関数(=高階関数)を書いた以下のコードと等価です。

Python2.7
def is_odd(x):
    return x%2 == 1

もしくは

Python2.7
is_odd = lambda x: x%2 == 1

def remove_if(is_odd, data_list):
    result_list = []
    for x in data_list:
        if is_odd(x): 
            result_list.append(x)
    return result_list

remove_if(is_odd, data_list)

p6.png


reduce() も同様です。

Python2.7
data_list = [1, 2, 3, 4, 5]

def multiply(x, y):
    return x*y

reduce(multiply, data_list)

p7.png

(ラムダ式(無名関数)で書くと1コード行で書ける)

Python2.7
reduce(lambda x, y:x * y, data_list)

p8.png

reduce()メソッドを使った上記の記法は、関数を引数で受け取る関数(=高階関数)を書いた以下のコードと等価です。

Python2.7
def multiply(x, y):
    return x * y

def fold(multilly, data_list, initial_data = 1):
    n = initial_data
    for x in data_list:
        n = multiply(n, x)

    return n

fold(multiply, data_list, initial_data = 1)

p9.png


reduce() はまた、関数内で自分自身の関数を呼び出す「再帰呼び出し関数」(関数型言語)と同じ場合もあります

再帰呼び出し関数
階上演算は、以下の自分自身の内部で自分を呼び出す再帰関数で実現されます。

階乗演算の実装関数

Python2.7
def fact(n):
    if n==0: return 1
    return n * fact(n - 1)

fact(5)

p10.png

これは、以下のように、reduce()をつかっても実装可能だからです。

Python2.7
data_list = [5, 4, 3, 2, 1]

def multiply(x, y):
    return x * y

reduce(operator.multiply, data_list)

p11.png

(以下も等価)

Python2.7
import operator
reduce(mul, data_list) # mul は、operatorモジュールに定義された乗算演算子

p12.png


クロージャ(closure)

関数をネストさせて、関数を生成する関数(返り値で、生成した関数を返す)

Step 1 関数を生成する関数を作る
(今回は、引数で受け取った数を、ある数に乗算する関数を返す

Python2.7
def multiply(n):
    return lambda x: n*x

Step 2 上記の関数生成式に、引数を与えて、特定の演算を行う関数を受け取る
(今回は、引数に2を与えて、ある数に2倍する関数を受け取る)

Python2.7
multiply_by_2 = multiply(2)

Step 3 特定の演算を行う関数に、引数を与えて、結果を受け取る
(今回は、引数に10を与えて、10に2倍した結果を受け取る)

最後に、2を掛け算する関数 multiple_by_2()関数に、引数を与えて、結果を出力する

Python2.7
multiply_by_2(10)

p13.png

Python Tips:Pythonでクロージャを使いたい
Python2.7
x = 1

def func_1():
        x = 10
    return x

def func_2():
    print func_1()

func_2()

p14kai.png

Python2.7
x = 1

def func_1():
        x = 10
        return x
    def func_2():
        print x

func_1()

上記は、関数をネストさせて、外側の関数内で定義した変数を、内側の関数にとっての「グローバル変数」として利用しています。

但し、この変数 x = 10 が有効な名前空間のスコープは、この変数が宣言された場所である func_1() 関数の領域内のみである。(なのでグローバル変数ではない) このことがもつ利点は、グローバル・スコープに宣言するグローバル変数の数を減らすことができることである。

p15kai.png


上記を利用した例(参考サイトより転載)

内側の関数 circle_area_func() にとっては、この関数がその中で宣言された circle_area_func(pi) で定義された(=関数の引数で受け取った)変数 pi は(自分より)上位の階層の名前空間内に定義されているため、アクセスすることができる
Python2.7
def circle_area_func(pi):
    """円の面積を求める関数を返す"""
    def circle_area(radius):
        return pi * radius ** 2 #このpiは、circle_area_func()の引数に指定された値

    return circle_area #関数を返り値として返す

#円周率を 3.14 に設定した場合の面積を計算する関数を生成
ca1 = circle_area_func(3.14)

#次に、円周率を3.141592に設定した場合の関数を生成
ca2 = circle_area_func(3.141592)

#上記で作成した2つの関数に、半径=1 を引数に与えて、演算結果を取得
ca1(1)
ca2(1)

#上記で作成した2つの関数に、半径=2 を引数に与えて、演算結果を取得
ca1(2)
ca2(2)

p16.png

===========================

reduce() は、クロージャを使って、以下でも置き換えられます。
Python2.7
data_list = [1, 2, 3, 4, 5]

def repeat_multiply(n, data_list):
    return map(lambda x:x ** n, data_list)

g = repeat_multiply(2, data_list)
g(data_list)

p17.png

(上は以下と等価)

Python2.7
def repeat_mutiply(n, data_list)
    def multiply(x):
        return x ** n

    return map(multiply, data_list)

g = repeat_multiply(2, data_list = data_list)
g

p18.png

「カリー化」(関数の部分適用)

「1.関数型プログラミング言語の機能」
「Pythonのfunctoolsモジュール(デコレータ、部分適用)」

2つ以上の引数を受け取る関数を呼び出す際に、必要な個数の引数をすべて一度に与えないで、何回かに分けて、引数を小出しに渡して関数を呼び出して、式評価する方法です。

引数を複数個、要求する関数に対して、「必要な数より少ない個数を渡して、残りの必要個数の引数を要求する関数を得る」ということをしています。

これはつまり、返り値として、「(残りの必要個数を引数に取る)関数」が返されるということを意味しています。

これは、関数を返す関数、つまり、高階関数 を生成した、という動作を意味します。

例として、4個引数を受け取って、返り値として、(「関数」以外の)何らかのリテラル(文字列型リテラル、数値型リテラル、Boolean型リテラル、など))を返す関数 f()があったとします。

この関数f()は、引数を4つ渡せば答え(関数評価した結果の値(value))が帰ってくる普通の関数です。

しかし、この関数f()に、例えば引数を2つしか渡さなかった場合、返り値として帰ってくるのは、「あと引数を2個渡してくれたら、返り値(value)を返すことができるよ」という関数になります。

つまり、『残りの引数を受け取って値(value)を返す関数』を返す 関数、すなわち、「関数を返す関数」である高階関数を生成しているのです。

ここで見てきた動作は、引数をn個受け取る関数f() を、引数をx個(但し、x < n)受け取って、返り値として、『引数 x-n 個を受け取る関数』を生成して返す関数(高階関数) に変換することを意味します。

上記で、x に 1 をあてはめると、引数をn 個要求する(もともとの)関数f()は、

引数を1個だけ受け取って、返り値として、引数を「(もとの関数が要求する引数の個数)- 1」(個)要求する 関数 g()

を、n 回、合成した関数』置き換えられます

言い換えると、引数をn 個要求する(もともとの)関数f()は、

引数を1個だけ要求して、返り値として、引数を「(もとの関数が要求する引数の個数)- 1」(個)要求する 関数 g()

n

分解 あるいは、置き換える
ことができると言うこともできます。
(但し、n個の関数 は、前の関数が、次の関数を自分の関数の引数(代入する値)に入れる(=関数が作用する対象に求める)合成関数の連鎖の関係にあります)

このように、n 変数関数1変数関数分解・置換する ことを、関数のカリー化 と呼びます。 

以下から引用
「Pythonでカリー化と部分適用について考えてみる」

Python2.7
def multiply(x, y):
    return x * y


def curried_multiply(x):
    def _curried_multiply(y):
        return x * y
    return _curried_multiply

curried_multiply(2)(3)

p19.png

lambda式を用いた以下も等価

Python2.7
curried_multiply = lambda x: lambda y: x * y
curried_multiply(2)(3)

p20.png

functoolsモジュール の partial()メソッドを使っても実装可能

Python2.7
from functools import partial

partial_applied_multiply = partial(multiply, 2)
partial_applied_multiply(3)

p21.png

さらに以下の参考ウェブサイトに面白い使い方を見つけたので、転載。

「Python で部分適用 - functools モジュールの partial 関数」

reduce()メソッド に渡された関数を、partial()メソッドで引数を小出しに渡しながら(部分適用)実行

Python2.7
L = [1, 2, 3, 4, 5]

import operator
from functools import partial

# 合計
mysum = partial(reduce, operator.add)
print mysum(L)

p22.png

さらに、上記のウェブサイトは面白い続きがある
合成関数を実装しているのだ。

Python2.7
def c(f, g):
    def _(f, g, h):
        return f(g(h))

    return partial(_, f, g)


add3 = lambda x: x+3
mul3 = lambda x: x*3

print c(add2, mul3)(10)

print c(mul3, add3)(10)

p23.png

上記のサイトをみると、以下も等価であると漏れなく書いてある(感動!)。

【短く】

Python2.7
def c(f, g):
    return partial(lambda f, g, h: f(g(h)), f, g)

p24.png

【もっと短く、エレガントに】

たしかに簡潔でエレガントではある。しかし、ここまでくると、可読性が落ちてくるか?

Python2.7
c = lambda f, g: partial(lambda f, g, h: f(g(h)), f, g)

p25.png

===========================

遅延評価

(ジェネレータ式 & yield文)

演算を1度にまとめて行うと計算負荷がかかるときに、遅延評価で、計算結果が必要なときまで、計算を先延ばしして、
計算資源をなるべく使わないようにできる。(最後まで必要とされない結果については、その結果を得るための演算を実行しないで済む)

以下が分かりやすい(転載)

綺麗なコードが良い「Pythonのyield文」

Python2.7
squares = (x ** 2 for x in range(5))

イテレータ・オブジェクトは、next()メソッドで要素を1つづつ順に取り出すことができる。

Python2.7
squares.next()
squares.next()

p26.png

( 引用 )

Pythonにはさらに、内部にループを持つ関数をジェネレータ化する仕組みが用意されている。yield文がそれだ。

Python2.7
def generateSquares(N):
    for i in range(N):
        yield i ** 2

G = generateSquares(5)
list(G)

G.next()
G.next()
G.next()
G.next()

p27.png

( 引用 )

このようにyield文でジェネレータ化した関数は、yield文を実行した後で関数の状態を保存して一時停止し、次のイテレーション(next()が呼ばれた時)で、関数を前の状態に戻して、yield文の次から処理を再開する。
yieldを使う、この遅延評価の仕組みは、PythonのシミュレーションライブラリであるSimPyにおいて、モデルを記述するのに使われている。

【参考URL】


【カリー化関数】


【部分適用 functools.partial()メソッド】


【遅延評価】