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宣言を使う事で外側の関数のローカル変数を変更する事が可能。
- 第1回 nonlocalでクロージャが便利に:Python 3.0 Hacks|gihyo.jp … 技術評論社
Python 3.0ではnonlocalというキーワードが導入され,外側の変数の更新が可能になりました。nonlocalの使い方はglobalキーワードと同じです。
- 7. 単純文 (simple statement) — Python 3.5.1 ドキュメント
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式(らむだしき)
- 4.7.5. ラムダ式 - 4. その他の制御フローツール — Python 2.7.x ドキュメント
名前のない小さな関数を生成できます。...
ラムダ式は、構文上単一の式に制限されています。...
ラムダ形式はただ通常の関数に構文的な糖衣をかぶせたものに過ぎません。...
入れ子構造になった関数定義と同様、ラムダ式もそれを取り囲むスコープから変数を参照することができます。 -
5.12. ラムダ (lambda) - 5. 式 (expression) — Python 2.7.x ドキュメント
-
ラムダ (lambda) による匿名関数の作成 - Python 入門
lambda は式
文 (statement) ではなく式 (expression) であることから、おのずと def とは出現場所が変わってきます。(def は文)
ラムダ式のボディ部は単一の式 (expression)
def の場合はボディ部はステートメント・ブロックでしたが、ラムダ式の場合は単一の式がボディ部となります。
以下のような関数を
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にも関数型プログラミングをおこなううえでの有用な関数が用意されているようだ。
- 9.8. functools — 高階関数と呼び出し可能オブジェクトの操作 — Python 2.7.x ドキュメント
- 9.9. operator — 関数形式の標準演算子 — Python 2.7.x ドキュメント
以下も参照。