はじめに
今となっては、プログラマにとってなんとなく理解して利用できることが当たり前になりつつあるオブジェクト指向ですが、しかし、それこそ今から数年前には、この「オブジェクト指向」というのは、いわばおじさん達が変な方針を打ち出したりして「え、それ変な実装方針じゃねえの」というツッコミが入ったりしていました(ちなみにそのあたりの雰囲気については、この記事を読むと分かりやすいでしょう)。
もちろん、これはこれなりにメリットがあるのかもしれませんが、しかしそれはまた別のオブジェクト指向を利用したモデリングと比較してのことであって、「これだけでいい」と考える人はいないでしょう。
原則: だってそのほうが開発しやすいから
まず最初に原則を考える必要があります。まずひとつに、必ずしもオブジェクト指向が正しいモデリングの方法ではないこと。少なくとも自分が思うに、オブジェクト指向を使うべき理由というのは、「そのほうが開発しやすいから」の一点にあると思います。
個人的には、オブジェクト指向が、人間の認知に沿っているかどうかはわかりませんが、少なくともそのほうが整理しやすい部分もあったのは疑い切れません。例えば、MVCのフレームワークは、ある程度「オブジェクト指向」をベースに考えて、その呼び出しの関係性で考えれば、Webサービスを構築する上において、混乱なく進む筈だ、という部分がミソになっているかと思いますし、事実、そのように感じています。
なので、まず最低ラインとして「そのほうが開発しやすいから」という、その原則を抑えておく必要はあるかなという気がします。
暫定的な印象: 高階関数という考え方
だいぶオブジェクト指向という考え方も枯れては来ている印象はあるのですが、しかし、同時に古くて新しいパラタイムが、最近見返されつつあります。それは「関数型言語」です。恥ずかしながら、理解できながらも、自分もちょこちょこと触っていたりするのですが、確かに関数型言語は、一つくらいは触って損はないパラタイムであるということを実感しつつあったりします。(もちろん、何を関数型と呼ぶのか、という問題はありますが、Lispも関数型の一つ、みたいな緩い定義で話したいと思います)。
関数型を触ってみて、特に影響を受けたのは、「関数を関数で扱うということの表現の豊かさ」にあったように感じます。本当に初歩的なのですが、ある要素を使うかどうかを、for
で選別するのではなく、filter
みたいな関数を使って、「ある関数に要素を入れた場合、返り値が真ならリストに追加してあげ、偽なら次の要素に移動する」みたいな定義を書いて表現したほうがむしろ、シンプルになるということを理解したことでした。
そういう風に「脳」が「なるほど」と理解してしまうと、むしろfor
を使うことというのは、冗長か、あるいは悪い習慣みたいなのを呼び寄せている印象のほうが強い気がしてきます(人間は身勝手なものです)。もちろん、自分自身も関数を扱う関数というのに対して戸惑いが無かったわけではないのですが、しかし、段々慣れてくると、むしろそのように表現するほうが簡潔に感じはじめた、というのが背景にあります。
配列を作るのは繰り返し?
前置きが長かったのですが、やっと本題です。
例えば、なにかリストで構成されたデータ構造があったとします。Pythonですと、[1, 2, 3]
が例にあげられるでしょう。このリストの各要素を二乗にした配列を取り出したいとします。さて、そこで、Pythonなら三つほど、ここから二乗のリストを取り出す方法が考えられます。一応、再帰関数も考えられるのですが、これはちょっと意識高すぎなので、考えません。
他の二つについてはあとあと考えるとして、ここではfor
について中心的に考えていきたいと思います。
配列に対してついforを気軽に書いてしまう
自分もどうしても下のようなものを書いてしまいがちになります。
def my_two_square(target_list): result = [] for elem in target_list: result.append(elem ** 2) return result
もちろん、これはこれで間違っていませんが、しかしこれはちょっとある傾向を招きやすいことに気が付きました。
たとえば、この関数について、二乗した結果ではなく、二乗する前の数字が必要らしいことが発覚します。そこで、とりあえず辞書型(連想配列型)で、元と結果を保存しようと考えます。突貫的に、forに対して下のようなものを保存するようにします。
def my_two_square(target_list): result = [] for elem in target_list: result.append({"origin": elem, "result": elem ** 2}) return result
さらに、この二乗に対して、何らかのメッセージを表示する必要が出てきたとします。そこで、辞書に対してメッセージを保存しておけば楽だよね、という風に考えます。既に嫌な予感がしますが、とりあえず悪い例として実装していきます。
def my_two_square(target_list): result = [] for elem in target_list: two_square_result = elem ** 2 append_dict = {"origin": elem, "result": two_square_result} append_dict["message"] = "%d の二乗は %d です" % (elem, two_square_result) result.append(append_dict) return result for e in my_two_square([1, 2, 3, 100]): print e["message"]
さて、段々と保守するのが辛いコードになって来ましたね。
forを使う問題点: なんでも加工ブロックになりがち
まずひとつの傾向として、for
を使ってリストを加工することの問題の一つに、そのfor
ブロックが何でも加工ブロックになる傾向になりがちなのではないか、と思っています。
もちろん、この問題はforに限ったものではないのは理解しています。「何でも加工ブロック」になりがちなのは、関数であれ、メソッドであれ、避けられない問題であるのは間違いないです。しかし、困ったことに、for
はもう一つの問題を引き寄せます。
forを使う問題点: テストしやすさの低下
問題は複合的であるように感じます。
まずひとつに、このようなfor
が、リストの要素に対して如何なる操作を行おうとしているのか、というのが見えにくくなる傾向にある。そこで、このfor
のブロックが関数として閉じ込められており、要素を投げてその結果が帰ってくる形として実装されているならば、まだテストもしやすい。しかし、ここで辛いのは、何らかのリストを要求してくる点だったりします。
今回の場合は単純な構造だからいいのですが、しかしfor
が二重にあったり、try - except(あるいは catch)
で全体を隠蔽していたり、あるいは特殊なクラス経由で利用されていたり、みたいなことが絡みあっている場合、もはやそのブロックを理解することは苦行でしかありませんし、下手にいじると意図しないバグが発生したりします。
問題の転換: 配列毎に繰り返し、ではなくあるブロックの過程をリストの要素に対して適応する
さて、なぜfor
を私達はつい使ってしまうのでしょうか。もちろん、全てでfor
を使うことが悪いとは思いません。しかし、何らかの配列に対して加工し、加工した結果を期待するといった場合に、for
を使うということは、最近はいい手ではないのではないか、と思っています。
そこでリスト内包記法だ!あるいはmapだ!という風に飛ぶのもいいのですが、そもそも自分が使っているfor
って別の視点から捉えられないかな、と考えて見たら、自分の中で上手く整理出来たので、ここにメモをしたいと思います。
他の言語だとfor (i = 0; i < 10 ; i ++) {}
という書き方が出来ますが、Pythonですと、基本的にはrange
という関数がlist
を生成しているという風に考えられます(同時にこれ特有の問題もあり、そのときにはxrange
という関数が使われているように記憶しています)。そうした場合、基本的にPythonの場合は、リストに対するブロックの適用としてfor
があると捉えなおすことが出来ます。
そうすると、そもそも配列を加工するということは、「ある操作を一つの要素に適用すること」と、「それを全ての要素に適用する」という二つの操作があり、それらはそれぞれ分割出来ることが分かります。
「えー? 話が抽象的すぎるよ」と、自分でも思い始めたので、先ほどのfor
文をリファクタしていきます。
リストを集める部分とその要素を実際に加工する部分の分離
まず最初に、for
で嫌な気持ちになった部分を関数で閉じ込めてみます。まず考えられることとして「ある要素を二乗にすること」と、「あるリストの要素を全て二乗にする」という部分は、それぞれ機能として分離できそうです。
def my_two_square_list(target_list): result = [] for elem in target_list: result.append(my_two_square(elem)) return result def my_two_square(elem): two_square_result = elem ** 2 append_dict = {"origin": elem, "result": two_square_result} append_dict["message"] = "%d の二乗は %d です" % (elem, two_square_result) return append_dict for e in my_two_square_list([1, 2, 3, 100]): print e["message"]
もちろん、綺麗な分離かというと微妙ですが、少なくともコマンドシェル上で、関数をimportして、テストするのは簡単になると期待出来ます。
「それぞれの関数に対して、要素を適用する」ための関数、ならびにSyntaxを利用する
さて、リファクタ後のmy_two_square_list
を見ると、ふとこういう疑問が過ぎります。
「あれ、なんでこいつは配列の要素をいちいち取り出して、新しい配列に追加しているんだろう。少なくともresult
の宣言は余計だし、綺麗ではない。こいつを消せればもう少し見通しがよくなるのでは」
そこで、二つの方法があります。リスト内包記法か、あるいはmap
関数です。
Python 3
になると、この三つの方法は、明確に違う目的を持っています。map
はmap object
を生成し、その結果を作るのを後回しにします。しかし、リスト内包記法は、そのままリストを生成することになります(詳しい議論はStack overflowにあります)。
さて、そこで二つの方法で書きなおして見ましょう。まずリスト内包記法です。
def my_two_square_list(target_list): return [my_two_square(elem) for elem in target_list] def my_two_square(elem): two_square_result = elem ** 2 append_dict = {"origin": elem, "result": two_square_result} append_dict["message"] = "%d の二乗は %d です" % (elem, two_square_result) return append_dict for e in my_two_square_list([1, 5, 10 ,25]): print e["message"]
これは好みの問題かもしれませんが、リスト内包記法を使う場合、どうしてもその記述が冗長になってしまうため、関数の中に閉じ込めたり、あるいは一旦何らかの変数に代入してから他のところに利用するというほうが好きだったりします。
ここでのポイントは、上のように書き換えたことにより、「あるリストの要素を、ある関数に適用した結果として、関数が生成される」ということがわかりやすくなったのではないかと思います(ちょっと強引のような気もしますが……)。また、Python
の場合ですと、リスト内包記法の中でif
によるフィルタリングができるので、それもまた便利です。
追記: あれ、リスト内包記法で使われているfor
も広義のfor
では?
これは改めて気が付きました。すいません。
ただ言い訳をすると、リスト内包記法の場合は、複雑なブロックを書くことがある程度抑えられるので、いわゆるfor
とは違う扱いにしてしたというのは事実です。細かいところではありますが、追記しておきます。
さて、今度はmap
関数を使ってみましょう。
def my_two_square(elem): two_square_result = elem ** 2 append_dict = {"origin": elem, "result": two_square_result} append_dict["message"] = "%d の二乗は %d です" % (elem, two_square_result) return append_dict for e in map(my_two_square, [1, 5, 10 ,25]): print e["message"]
こうなると、もはやなんでリストを生成するための関数が必要だったのかという感じです。
forの利点
とはいえ、for
にも利点があります。その利点とは、つまり「そのリストの返り値を利用しない場合」か、あるいは「要素が余りにも大きすぎるため、上のようなアプローチをとると遅くなってしまう」場合において、と自分では[思ったりします。例えば、上では要素の結果を表示するためのprint
ではfor
を使っていますが、この返り値は必要ないためですし、一応、リスト内包記法でも書き直しはできますが、このためにリストを作るのも、富豪的すぎる側面はあります。
つまりまとめると、for
を利用するメリットというのは、返り値を捨てる場合が多い気がします。
なぜ「forおじさん」なのか
さて、そこで釣りタイトルのようなforおじさん
という文面に戻りましょう。なぜ、forおじさん
になっていくのでしょうか。
上のアプローチは、別に珍しいことではなく、比較的最近の言語ならば、受け入れられつつある概念であるように思います。
今現在、周りを見渡すと、自分の知っている範囲なら、Pythonであれ、Rubyであれ、あるいはEcmaScript 6の仕様であれ、普通に「関数を配列の要素に対して適用し、その結果をリストとして受け取る」ことが出来るなんらかの方法があり、特別ではないように感じますし、そういう風に書いたほうがわかりやすいこともあるというのは、そう間違っていないのではないかと感じます。
またそれぞれ「要素をどういう風にしたいのか」といった部分や、「その配列をどう表現するか」ということが分離できるのも、見通しを良くする点でポイントになるかなと感じますし、保守するさいにもテストしやすかったり、あるいは関数にしておくことで、のちのち並列処理やcerely
などによるキューを使ったタスクの積み上げを行わなきゃいけないときにも書き直しやすかったりなど、メリットが多く存在するように感じます(実際のところ、現実にそういう書き直しはありました)。
逆に、これらを意識せずに、ただのっぺりとfor
を書いていると、余りにも追いにくいコードになってしまったり、あるいは多重for
でなんとなく動いているような散らかった形になってしまうのでよくない気がしますし、上記のような、方針の変更についても、まず最初に関数に閉じ込めるといった、まず最初にリファクタしないとどうしようもなくなったりするので、あまりよくないかなあという気はします。
とはいえ、自分もこういうのを書きがちなので「うわっ、何も考えずにfor
使ってる!forおじさんだ!」と言われないように頑張りたいと思います。
参考:
こっちはパフォーマンスという観点から、for
文をできるだけ使わないほうがいいという話がされています。ライブラリで高速化しているから、ライブラリにまかせようという話なので、若干自分の関心事とは違いますが、こういう観点もあるので、ここに紹介しておきます。