読者です 読者をやめる 読者になる 読者になる

Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

迫り来る「forおじさん」と呼ばれる時代

はじめに

 今となっては、プログラマにとってなんとなく理解して利用できることが当たり前になりつつあるオブジェクト指向ですが、しかし、それこそ今から数年前には、この「オブジェクト指向」というのは、いわばおじさん達が変な方針を打ち出したりして「え、それ変な実装方針じゃねえの」というツッコミが入ったりしていました(ちなみにそのあたりの雰囲気については、この記事を読むと分かりやすいでしょう)。

 もちろん、これはこれなりにメリットがあるのかもしれませんが、しかしそれはまた別のオブジェクト指向を利用したモデリングと比較してのことであって、「これだけでいい」と考える人はいないでしょう。

原則: だってそのほうが開発しやすいから

 まず最初に原則を考える必要があります。まずひとつに、必ずしもオブジェクト指向が正しいモデリングの方法ではないこと。少なくとも自分が思うに、オブジェクト指向を使うべき理由というのは、「そのほうが開発しやすいから」の一点にあると思います。

 個人的には、オブジェクト指向が、人間の認知に沿っているかどうかはわかりませんが、少なくともそのほうが整理しやすい部分もあったのは疑い切れません。例えば、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

 もちろん、これはこれで間違っていませんが、しかしこれはちょっとある傾向を招きやすいことに気が付きました。

 たとえば、この関数について、二乗した結果ではなく、二乗する前の数字が必要らしいことが発覚します。そこで、とりあえず辞書型(連想配列型)で、元と結果を保存しようと考えます。突貫的に、下のように書き直します(Classを作れという指摘は正しいのですが、今回は話を簡単にするために、敢えて辞書型を使います)。

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になると、この三つの方法は、明確に違う目的を持っています。mapmap 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の仕様であれ、普通に「関数を配列の要素に対して適用し、その結果をリストとして受け取る」ことが出来るなんらかの方法があることは把握しています。とすると、思ったよりこのアプローチは特別ではないように感じますし、そういう風に書いたほうがわかりやすいこともあるというのは、そう間違っていないのではないかと感じます。

 またそれぞれ「要素をどういう風にしたいのか」といった部分や、「その配列をどう表現するか」ということが分離できるのも、見通しを良くする点でポイントになるかなと感じますし、保守するさいにもテストしやすかったり、あるいは関数にしておくことで、のちのち並列処理もやりやすかったりします。

 また直近ですと、キューで非同期的に処理を実行する為のceleryというのがPythonにあるのですが、これを利用するにおいて、関数単位に処理がまとまっていると、それらを分解して書き直しやすくなったりもします。

 こういう風に、現実的にスケールするために何らかの書き直しが発生するときでも、メリットが多く存在するように感じます(実際のところ、現実にそういう書き直しはありました。もちろん、こればっかり意識するのも「早すぎる最適化」なのですが)。

 逆に、これらを意識せずに、ただのっぺりとforを書いていると、余りにも追いにくいコードになってしまったり、あるいは多重forでなんとなく動いているような散らかった形になってしまうのでよくない気がします。また、上記のような、「並列化しよう」「キューで逐一実行するようにしよう」といったような変更についても、まず最初に関数に閉じ込めるといった感じで、肥大化したforのブロックをリファクタしないとどうしようもないという状態になったりして、あまりよくないかなあという気はします。

 とはいえ、自分もこういうのを書きがちなので「うわっ、何も考えずにfor使ってる!forおじさんだ!」と言われないように頑張りたいと思います。

参考:

こっちはパフォーマンスという観点から、for文をできるだけ使わないほうがいいという話がされています。ライブラリで高速化しているから、ライブラリにまかせようという話なので、若干自分の関心事とは違いますが、こういう観点もあるので、ここに紹介しておきます。

蛇足:

公開当初、celeryをcerelyとtypoしていました。ご指摘ありがとうございます。