pythonと関数型プログラミング

【 目次 】

pythonはマルチパラダイム言語といわれオブジェクト指向だけでなく、関数型プログラミング的な要素も取り込んでいる。

IBMのdeveloperworksの記事も参考になるかな。

関数型プログラミングの用語等については。

関数はファーストクラスのオブジェクト

  • hanepjiv: Python / 第一級関数と無名関数

    Pythonは全ての関数を第一級関数 (first-class function)として扱えます。

    第一級関数は、

    「変数に入れること」ができ 「他の関数への引数として渡すこと」ができ 「戻り値として返すこと」ができます。

「Pythonは全ての関数を第一級関数として扱える」ので、関数型プログラミングの要素を含んでいるという事か。

関数を変数に代入して、実行したり

def func():
    print u"funcを実行"

f_var=func
f_var()

実行結果

funcを実行

関数をリストの要素にしたり

def func1():
    pass

def func2(x):
    print u"func(",x,")を実行"

func_list=(func1,func2)
func_list[1]("a")

実行結果

func( a )を実行

関数の引数に関数を渡したり

def func(x,arg_func):
    arg_func(x)

func("a",func2)

と、関数を変数のように扱う事ができる。

関数のネストとクロージャ

関数をネストする事ができる。

def func():
    def inner_func():
        print u"inner_funcを実行"
    inner_func()

func()

実行結果

inner_funcを実行

ネストした内部の関数はローカルスコープとなり外部から見えない。

inner_func()
NameError: name 'inner_func' is not defined

ネストした関数の外側の関数のローカル変数は内側の関数から参照可能である。

def func(outer_arg):
    a="x"
    def inner_func():
        print "outer_arg=", a
        print "a=", a
    inner_func()

func("y")

実行結果

outer_arg= x
a= x

このような、外側の関数のローカル変数を自由変数,内側の関数内の変数を束縛変数と呼ぶようだ。

nonlocal 宣言

しかし、外側の関数のローカル変数(外側の関数の引数を含め)を内部の関数から変更する事はできない。

def func():
    a="x"
    def inner_func():
        a+="X"
    inner_func()
    print a

func()

実行結果

UnboundLocalError: local variable 'a' referenced before assignment

python3の場合にはnonlocal宣言を使う事で外側の関数のローカル変数を変更する事が可能。

def func():
    a="x"
    def inner_func():
        nonlocal a
        a+="X"
    inner_func()
    print(a)

func()

実行結果

xX

ネストした内部の関数を戻り値として返す

関数をネストして内部の関数を戻り値として返すことで外部から呼び出す事ができる。

def func(arg):
    def inner_func():
        pass
    return inner_func

f_var=func()
f_var()

このような内部で定義された関数を外部から呼び出す場合、内部の関数名は意味を成さない場合が多いので無名関数として定義したいものであるが、defによる通常の関数の定義では無名関数は定義できない。
無名関数として定義したい場合は後述のラムダ式を使う。

高階関数

関数を引数にして関数に渡したり、関数を戻り値として返す関数を高階関数と言う。

クロージャ

内側の関数は外側のローカル変数を参照できる事は前に述べたとおりである。
この事は、外側のローカル変数の値を変えてやることで内側の関数の動作を変えてやる事ができる事を意味する。
クロージャは関数の外側の関数のローカル変数の値を内側の関数が保持して外側の関数のローカル変数の値を利用する事で関数の動作を変更する事ができる。

def func(times):
    def inner_func(arg_str):
        for i in xrange(times):
            print arg_str
    return inner_func

f_var=func(2)
f_var("ABC")

実行結果

ABC
ABC

このコードを実行すると文字列ABCを2回分表示する。
このように外側の関数(エンクロージャと言う)funcの引数に2を指定する事で、関数の戻り値である内側の関数(こちらがクロージャ)が2回分の文字列を出力する関数になる。

また、以下のように関数funcの引数に3を指定してやれば、同じ関数を利用して3回分の文字列を出力できる関数を生成できることになる。

f_var=func(3)
f_var("XYZ")

実行結果

XYZ
XYZ
XYZ

クロージャはカリー化やデコレータ 等、いろいろなところで利用されている。

以下も参照。

カリー化

カリー化とは乱暴に言ってしまえば、複数の引数をとる関数を1つの引数をとる関数の組み合わせに分解する事。
3つの引数の合計を求める関数をカリー化する例を示す。

def add(x,y,z):
    return x+y+z

これをカリー化すると

def add_x(x):
    def add_y(y):
        def add_z(z):
            return add(x,y,z)
        return add_z
    return add_y

カリー化された関数を実行してみる。

実行結果

print add_x(2)(3)(4)

以下も参考に。

関数型プログラミングに有益な組込み関数

関数型プログラミングに有益な組込み関数がpythonには用意されている。

これらの関数は引数として関数オブジェクトとiterableなオブジェクトをとる。
これによりシーケンス型の要素等のiterableなオブジェクトの各要素に対していろいろな処理をおこなう事ができる。

map関数

map 関数を利用するとiterableなオブジェクトの全要素に対して指定した関数の処理を実行した結果を返す。

次の例はタプルの各要素の値をインクリメントしたタプルを生成。

def calc(x):
    return x+1

result=map(calc, (7,3,5))
print result

実行結果

[8, 4, 6]

map関数は複数のiterableオブジェクトを指定する事もできる。
複数のiterableオブジェクトを指定した場合には、引数として指定する関数にもiterableオブジェクトの数だけ複数の引数を指定する。
次の例は2つのタプルの各要素の値を加算したタプルを生成。

def calc2(x, y):
    return x+y

result=map(calc2, (7,3,5),(3,2,4))
print result

実行結果

[10, 5, 9]

文字列型もシーケンス型のオブジェクトなので...
次のコードは少し複雑な例であるが、文字列「abcdefg」の中の文字c,fを「*」に置換して返す。
関数calc_cはクロージャ関数(そしてカリー化)の例にもなっている。
そしてcalc_cの内部には後置if文(式)というか三項演算子が使われている。

def calc3(arg_str):
    def calc_c(c):
        return "*" if c in arg_str else c
    return calc_c

result="".join(map(calc3("cf"), "abcdefg"))
print result

実行結果

ab*de*g

reduce関数

reduce 関数はiterableオブジェクトの各要素を累積的に演算処理した値を返す。
mapやfilteと大きく異なる点は、関数が作為的に複数の要素を持つ値を返さない限り、reduce関数の結果は基本的に単一の値を返す。

次のコードは、iterableオブジェクトの各要素の和を求める例である。

def my_add(x,y):
    return x+y

result=reduce(my_add, (2,5,7))
print result

実行結果

14

reduceにはオプションでinitializerという引数が用意されていて初期値を指定する事もできる。

result=reduce(my_add, (2,5,7), 100)
print result

実行結果

114

次のコードは、文字列中の各文字をインクリメントした文字列を返す。
最初に1文字目をインクリメントするためにif文が使われている。
(あまりエレガントなコードとは言えないが)

def inc_add(x,y):
    def chr_inc(c):
        return chr(ord(c) + 1)
    if len(x)==1:
        x=chr_inc(x)
    return x+chr_inc(y)

result=reduce(inc_add, "abc")
print result

実行結果

bcd

mapや次のfilterの意味はなんとなくわかるが、reduceという聞きなれない言葉が気になって、単語の意味は何だろうと調べてみたら、

「減らす」とか「縮める」とかいろいろな意味があって、この場合は多分「整理して簡単な形にまとめる」という事だろう。

filter関数

filter 関数はiterableオブジェクトの各要素をフィルタリンングして指定した条件にあった要素のリストを返す。

filter関数の戻り値は引数が

文字列型かタプル型の場合、結果も同じ型になります。そうでない場合はリストとなります。

以下の例はタプルの要素内の偶数の値だけの要素のタプルを返す。

def iseven(x):
    return x%2==0

result=filter(iseven, (3,8,2,7))
print result

実行結果

(8, 2)

後述するラムダ式を使えば、上記のコードのiseven関数をムダ式に置き換えて、filter関数の部分を以下のように簡潔に記述する事もできる。

result = filter(lambda x: x % 2 == 0, (3, 8, 2, 7))

文字列の場合の例も示しておく。

次のコードは文字列型の文字種判定のメソッドを使って、文字列の中の英文字以外の文字を削除する例である。

def isalpha(c):
    return c.isalpha()

result=filter(isalpha, "abc123def456@_?ghi")
print result

実行結果

abcdefghi

filter関数の第1引数である判定条件を指定する関数にNoneを指定するとiterableオブジェクトの各要素の値がTrueと判定できる要素のみのリストを返す。

result = filter(None, (0, 1, False, True, "", "abc"))
print result

実行結果

(1, True, 'abc')

0やFalseそして空文字はFalseと判定されるのでそれ以外の要素を返す。

itertoolsモジュールの関数

itertoolsモジュールの関数も何かの役にたつかも。

無名関数とlambda式(らむだしき)

以下のような関数を

def func(x):
    return x*x

ラムダ式で記述すると

func = lambda x: x * x

lambda式は通常の関数と糖衣(シンタックスシュガー)

しかし、ラムダ式には関数には無い制限も。

ラムダ式は、単一の式しか記述できない。
そして、ラムダ式は文を含むことができない。

そして、乱用しない方が良い事も。

  • 小さな関数とラムダ式 - 関数型プログラミング HOWTO — Python 2.7.x ドキュメント

    著者のスタイルとしてはできるだけ lambda を使わないようにしています。

    そのようにしている理由の一つに、 lambda は定義できる関数が非常に限られているという点があります。一つの式として算出できる結果にしなければいけませんので、 if... elif... else や try... except のような分岐を持つことができないのです。 lambda 文の中でたくさんのことをやろうとしすぎると、ごちゃごちゃして読みにくい式になってしまいます。

print文も文なので、以下のラムダ式はエラーになる。

func = lambda x:  print x

式と文の違いについては、

ラムダ式の中に関数を記述する事はできるので、もしprint文を使いたいなら別に関数を用意して

def func_print(x):
    print x

func = lambda x: func_print(x)
func(10)

実行結果

10

if文も文なので記述できない。
しかし、論理式のショートサーキットを利用すれば同等の機能をラムダ式でも記述できる。

def func(x):
    if x==0:
        print "A"
    else:
        print "B"

のような関数をラムダ式で実現しようとすると、別にTrueを返す関数を用意して

def func_print(x):
    print x
    return True

次のようなラムダ式を記述する。

func = lambda x: x==0 and func_print("A") or func_print("B")

ラムダ式を実行すると

func(0)
func(1)

実行結果

A
B

以下のような、少し複雑なif文を含む関数は

def func(x):
    if x==0:
        print "A"
    elif x==1:
        print "B"
    else:
        print "C"

以下のようなラムダ式になる。

func = lambda x: x==0 and func_print("A") or x==1 and func_print("B") or func_print("C") 

ラムダ式を実行。

func(0)
func(1)
func(2)

実行結果

A
B
C

でも、可読性が悪いので、できればショートサーキットは使わずにラムダ式では無い通常の関数の方がいいかな。

デフォルトの引数キーワード引数(名前付き引数)も指定できる。

func = lambda x=10: x * x

# デフォルトの引数を使う
print func()

# 名前付き引数を使う
print func(x=2)

実行結果

100
4

map,reduce,filter関数を利用する事で、ラムダ式の中でfor文が使えないという欠点を補うことができる。

以下のタプルの各要素の合計を求めるコードは

def func(items):
    total=0
    for item in items:
        total=total+item
    return total

print func((2,3,4))

以下のラムダ式で置き換える事ができる。

print (lambda items: reduce(lambda a,b: a+b,items))((2,3,4))

実行結果

9

ラムダ式でも関数のように*引数や**引数も使えるようで、

ちょっと複雑な例であるが、以前に任意の引数を受け取る関数の定義で紹介した異なる数の引数を持つ関数を引数として受け取って、その結果の2倍の値を返す関数をラムダ式で記述すると

double_func = lambda func_arg, *list_args, **dict_args: func_arg(*list_args, **dict_args) * 2

このラムダ式に2つの引数をとるラムダ式や3つの引数をとるラムダ式を引数として渡してやると、

# ラムダ式double_funcの引数funcに2つの引数の合計を返すラムダ式を指定
print double_func(lambda a, b: a + b, 2, 3)
# ラムダ式double_funcの引数funcに3つの引数の合計を返すラムダ式を指定
print double_func(lambda a, b, c: a + b + c, 2, 3, 4)

実行結果

10
18

lambda式は名前の無い関数、つまり無名関数(匿名関数とも呼ばれる)になる。

でも、無名関数イコールlambda式では無くて
pythonの場合は、通常の関数は常に名前付きの関数であり、lambda式でないと無名関数を定義できない。
でも、JavaScriptのようにlambda式ではなく通常の関数を無名関数として定義できる言語もある。
本来は、lambda式で無名関数を定義できるが、無名関数は必ずlambda式でなければ定義できないわけじゃない。
pythonもJavaScriptのように、lambda式では無い無名関数が定義できればいいのになぁ。

lambda式はクロージャとしても使える。
以下のようなクロージャ関数は

def func_a(arg_a):
    a = "a"
    def func_b(arg_b):
        b = "b"
        return a , b, arg_a, arg_b
    return func_b

closure_func=func_a("A")
print closure_func("B")

実行結果

('a', 'b', 'A', 'B')

上記のコードの関数func_aはラムダ式を使って以下のコードに置き換える事ができる。

def func_a(arg_a):
    a = "a"
    return lambda arg_b:(a , "b", arg_a, arg_b)

簡単な関数であればラムダ式で代用できる。
ラムダ式はLispのような関数型プログラミング言語からやってきたようだ。

関数型プログラミングをサポートする標準モジュール

標準モジュールfunctools,operator,functionalにも関数型プログラミングをおこなううえでの有用な関数が用意されているようだ。

以下も参照。

ページのトップへ戻る