どうすれば良いプログラムが書けるようになるのか?


私は、情報科学の出身ではないため、プログラマーとして就職して以降にいろいろと学ぶ必要がありました。今では機械学習のチームでバックエンドエンジニアとして仕事して、後輩のコードをレビューしたり教えたりする立場になりました(ビックリですね!)。

ただ学び方は効率良くなく、かなり無駄な時間も過ごしてしまったと思います。後輩に同じことさせるのは忍びないので、なんとかプログラミングしているときに考えていることを言語化して伝えられるようにしようと思ってこの記事を書いています。できる人にとっては当たり前かもしれません。

この記事より先に、プログラマーとしての価値観や心構えについて、まず「ハッカーになろう (How To Become A Hacker)」や「優秀なプログラマーになるためのコツ」を読んでください。

またプログラミング以外にも身につけるべきことは多いですが、キリが無いので別の機会にします。

どうエラーに対処すればいいのか?

いつでもエラーは出ます。その対処方法が分からずになかなか作業が進まないこともあります。そんなとき、プログラマーは(意識・無意識に関わらず)次のような「仮説検証」のプロセスに従って行動しています。

  1. エラーメッセージを読む
  2. 原因の仮説を立てる
  3. 調べて試す
  4. だめだったら違う仮説を立てて調べる

これでも分からなかったときは詳しそうな奴に聞いてください。また、調べる際は、QiitaやStackoverflowより、公式ドキュメントのほうが内容が正確なので先にそちらを調べましょう。特に我々がよく使うAWSやPythonはドキュメントが充実しています。

例えば「数値計算結果をMySQLに投入する」ようなバッチ処理で、なぜか本番環境だけtimeoutのエラーが出たことがあります。この時はまず「テスト環境と本番環境でtimeoutの設定値が違うのではないか?」と仮説を立てて実際にそうであることを確認し、十分に長い値が設定されているので「数値計算処理より先にコネクションを貼って、長い間貼り続けているのではないか?」と考えて対処しました。

この習慣がつくと、エラーへの対処中にひとつ下のレイヤーの知識を学ぶことができます。もしこの考え方が身につかないなら、何年プログラマーのキャリアを歩んだとしても、浅い知識でとどまってしまうでしょう。

ただ、yak shavingになって延々と調査してしまわないよう、「別の選択肢で一旦終わらせる」ような発想も残しておいてください!

プログラムが遅い!どうすれば早くなる?

「一応機能はできたけど、妙に遅くて使い物にならないよ…」ってことはあります。そのときはこれに従って対処してください。

  1. ボトルネックを特定する
  2. そこを高速化する

ボトルネックを調べずに対処すると、速度は改善しない上、複雑さも追加された糞の塊ができていきます。この考え方は(「目標とする指標を作る」というプロセスを足せば)業務改善や機械学習モデルの精度改善でも同様だと思います。Andrew Ng先生のCourseraのMachine Learningでもたしか「機械学習のパイプラインの中で、精度の低下にクリティカルなモジュールを改善すべき」みたいな話があったと思います。

ボトルネックによって対処する方法が変わります。

  • 計算処理が遅い(CPUバウンド/メモリバウンド)
  • IOが遅い(IOバウンド)

計算処理が遅い(CPUバウンド/メモリバウンド)

プログラムが妙に遅い場合は、まずアルゴリズムの「計算量(正確には時間計算量)」に問題のある場合を疑ってください。具体的には不要に多重ループしている場合が多いです。

# タグidの数nが増えるたびに、計算ステップがO(n^2)で増えてしまう!
result = []
for tag_id in ユーザーが好きなタグのリスト:
    for product_id, 商品に付いているタグidのリスト in (商品id, タグidのリスト)のリスト:
        for product_tag_id in 商品に付いているタグidのリスト:
            if tag_id == product_tag_id:
                result.append(product_id)
                break
return result

これに対処するには、「アルゴリズムとデータ構造」を学ぶ必要があります。私のおすすめははてなの開発者の記事で知った「Algorithms, Part I」という無料のCourseraのコースです。計算量に問題がある場合、より早い言語(例えばGo言語)で実装し直しても解決しないので気をつけてください。

先程のコードは、辞書(ハッシュテーブル)から取得するようにして実装します。

# タグid -> {商品id} の辞書
商品の辞書 = {
    "タグ1": {"商品1", "商品2"},
    "タグ2": {"商品2", "商品3"},
    ...
}

result = set(候補の商品idのリスト) 
for tag_id in ユーザーの好きなタグのリスト:
    result &= 商品の辞書[tag_id] # 和集合
return result

また、計算量の改善が行えない場合は並列処理を検討しましょう。Pythonで使う並列処理を扱うなら、concurrent.futures.ProcessPoolExecutorjoblibを使うことになると思います。

CPUの問題ではなくメモリが足りてない場合もあります。サーバーのメトリクスを確認してください。Unixコマンドならtopで確認できます。

IOが遅い(IOバウンド)

WEBアプリケーションや、データベースや他のサーバーに対して多数のリクエストが発生している場合、そのIO待ちでプログラムが遅くなっている場合もあります。サイトをクローリングする場合でも同様です。

その場合、まず「並行処理」で対処しましょう。具体的な対処方法は「Pythonをとりまく並行/非同期の話」を読んでください。

  • 本当に複数の処理を同時に実行するのが -> 並列処理
  • 複数処理を効率良く切り替えながらあたかも同時に実行するのが -> 並行処理

IO待ちの時間に他の処理に切り替えているだけです。つまり、「並行処理」であるasync/awaitThreadPoolExecutorはOSのプロセス自体は増やさないので、「CPUを使う計算処理を速くする」効果はありません。あとC10K問題の話なども知っておいてください。

この資料からのPython界隈の差分は、trioというasyncioより扱いやすいライブラリが出てきたことだと思います。FastAPIあたりが対応してくれないかな…。

※ そもそも、並列(Parallel)/並行(Concurrent)の用語は混乱していて、微妙に違う定義で使われることもあります。ただ、ここでは上記資料が引用している「並行コンピューティング技法」の定義で話しています。

どうすれば良いプログラムが書けるようになるのか?

私も知りたいですが、いくつか自分のできる範囲でアドバイスします。Joel Spolskyの「Javaスクールの危険」で言われている"Java"は現代ではPythonです。

grepを使うリクルーターが騙されるのには理由がある。私の知るSchemeとHaskellとCのポインタが使える人はみな、Javaを使い始めて2日で経験5年のJavaプログラマよりいいコードを書くようになる。しかしそのことが平均的な頭の鈍い人事部の連中には理解できないのだ。

私もこの記事を読んで、Python以外にC言語やHaskellも触ってみたことが役に立っています。特に静的型関数型プログラミングの型による設計方法の感覚(うまく言えないが、純粋関数を作って、その合成としてプログラムを組み立てていく)はPythonの実装でも役に立っているし、numpyの処理の高速化などでC言語を多少書いたことあることが役に立っていると思います。良いプログラムを書く方法の答えの一つは、「問題に適したパラダイムの発想でプログラムを書くこと」かもしれません。

設計については、「iOSアプリ設計パターン入門」に良い情報がまとまっていました。「『iOSアプリ設計パターン入門』が設計に悩む『非』iOSエンジニアも救ってくれる良書だった」にある通り、各パターンの利点・欠点を把握した上で、どれを採用すればいいかという議論ができるようになると思います。

パターンが最初にあるのではなく、コードを修正していく過程で、その最終形にパターンを見出すのです。 ...(略)...スタートは単純な設計からです。もしそこで既知のパターンが適用できそうであれば、その結果どうなるかを想像できます。パターンの利点・欠点は分析済みですので、いきなり完成度の高い設計に辿り着くこともありえます。あるいは少しずつテストを書きながらリファクタリングを進めることもありえるでしょう。

このような「トレードオフを意識して適切な選択肢を決定する」という意識が無いと、教条主義的になってこの記事にあるような「シンプルに実装されたコードを複雑な物にどんどん書き換えて行きました」という状態になってしまいます。ちょっと極論だとは思いますが。

また、オブジェクト指向についても学ぶ必要があります。ただ、そもそもいくつかの役割が混在したまま議論されることが多く、ややこしいです。詳しくは『コーディングを支える技術』を読んでください。著者のブログ記事「クラスが持つ3つの役割」にも少し言及があります。

やり方は何にせよ、「選択肢を増やして、その中からトレードオフを考えて選択する」ことの繰り返しだと思います。外国語の勉強で、語彙を増やしていくのと似ているのかもしれません。長期戦になると思うので、自分自身が楽しめる学び方を探してください。

まとめ

私の5年間の学びを3行にまとめるとこんな感じになります。えらくスッキリしましたね。

  • エラーは仮説検証プロセスとして対処しよう
  • プログラムの問題にたいしては、ボトルネックを見つけて対処しよう
  • 設計や実装は、選択肢の中からトレードオフを考えて意思決定すること

記事の中でなにか間違ったことを言ってたら、コメントで教えてください。