7. プログラミング上達のコツ

ここでは、授業で話した(もしくは話そうと考えている) プログラミング上達のコツを書いておきます。

7.1. 良いコードとは

まず、プログラミングを上達させる鍵は、 常にもう少し良いコードは書けないかとしばし考えてみることです。

良いコードとは、もう少し具体的にいうと:

  • 読みやすい

  • バグになりにくい

  • 実行効率がよい

上達のコツ

まず、良いコードを書こうと工夫することです

76342b4539934ebbb57f3a38454c54ec

ちなみに、「読みやすい」コードを追求するだけで、本1冊に相当する内容なっています。(リーダブルコードは、新人エンジニアがみんな読む名著です。)

7.2. アプローチ

まず、プログラミングは色々な書き方があります。

例題(三数の整列)

3つの整数を読み込み、それらを値が小さい順に並べて出力するプログラムを作成して下さい。

入力例:

1 8 3

出力例:

1 3 8

まず、次を見ずに自分ならどう解くか考えてみてください。

7.2.1. アプローチの違い

簡単な問題ですが、 プログラミングコンテストに有用な4つのアプローチがあります。

  • コーディング力で解く

  • アルゴリズム力を活かして解く

  • 数学的に考えて解く

  • ライブラリ力を活かして解く

7.2.2. アプローチ1:コーディング力で解く

一番ストレートなアプローチは、 次のように3変数の大小関係のケースを全て列挙し、順番に出力することです。 実際、プログラミング初学者の多くはこの方法で解こうとします。

a, b, c = map(int, input().split())

if a <= b:
        if b <= c:
                print(a, b, c)
        else:
                print(a, c, b)
...

問題を解くことき、難しいことを考えず、 コーディング力で突破することも必要になります。

コーディングで勝負するとき大切なのは、バグ少なくコードを書く力です。 より一般的には防衛的プログラミング(defensive programming)と呼ばれる能力です。

バグの少ないコードを書くには

  1. バグなりやすそう箇所を予測する

  2. 回避する

今回の問題は、バグになりやすいのは\(a,b,c\)の大小比較においる条件の抜け漏れです。 それを回避するためには、ゴール指向、 つまり、先に全部のprint出力(ゴール)を列挙し、条件を後から書きます。

先にゴールを書く

a,b,c = map(int, input().split())

        print(a,b,c)

        print(a,c,b)

        print(b,a,c)

        print(b,c,a)

        print(c,a,b)

        print(c,b,a)

そして、条件をゴールに合わせて書きます。

a,b,c = map(int, input().split())
if a <= b and b <= c:
        print(a,b,c)
elif a <=b and c <= b:
        print(a,c,b)
elif b <=a and a <= c:
        print(b,a,c)
elif b <=c and c <= a:
        print(b,c,a)
elif c <=a and a <= b:
        print(c,a,b)
elif c <=b and b <= a:
        print(c,b,a)

コーディング力は、プログラミング力の基礎です。 しかし、バグになりやすい箇所を予測する力は、 何度もバグを踏んで苦労した経験からしか身につきません。 コーディング経験豊富な人の書いている様子をみせてもらうと、 バグを回避する書き方を学ぶことができます。

7.2.3. アプローチ2:アルゴリズム力で解く

問題の指示通り、変数を並べ換えてから、表示するアプローチを考えます。

しかし、どの変数をどういう順番で入れ替えるのか、結構難しいです。 左側はよくある間違いです。右側は正しい解答例になります。

よくある間違い

a,b,c = map(int, input().split())
if b > a:
        a, b = b, a
if c > b:
        b, c = c, b
if c > a:
        a, c = c, a
print(a,b,c)

正しい並び替え

a,b,c = map(int, input().split())
if b > a:
        a, b = b, a
if c > b:
        b, c = c, b
if b > a:
        a, b = b, a  # もう1回
print(a,b,c)

どうして正しい例が正しいか確信もてますか?

理由は簡単です。 最後の比較の前で変数\(c\)が最大値になることは保証されています。 なぜなら、プログラムは「バブルソート」アルゴリズムで書かれており、 バブルソートの名前の由来の通り、一番大きな値が最後尾にくるようになっています。

このように、アルゴリズムの知識は、複雑なプログラムを構築するときの助けとなります。 「アルゴリズムとデータ構造」をしっかり学んでおきましょう。

7.2.4. アプローチ3:数学的考察を活かして解く

並び替えるという言葉を忘れて、もう少し数学的な意味を考えることにします。

  • 左側に出力される値は、\(a,b,c\)の最小値

  • 右側に出力される値は、\(a,b,c\)の最大値

  • 真ん中に出力される値は..

数学では、等しい式を置き換えて考えてゆきます。 プログラミングで同じように、等しいプログラムに置き換えることで、 コードの意味がはっきりして読みやすくなります。 このようなテクニックを等式推論と呼びます。

a, b, c = map(int, input().split())

_min = min(a,b,c)
_max = max(a,b,c)
mid = (a+b+c) - (_min + _max)

print(_min, mid, _max)

数学的考察に基づいて、コードが書けると気持ちがよいものです。 あと、等式推論のおかげでコードの正しさに自信が持てます。 常に、「これとこれは結局同じ意味だから..」と考えながら、 数理的な考察を加えるといいでしょう。

7.2.5. アプローチ4:ライブラリ力で解く

最後は、Python に慣れた人が使うアプローチです。 3数をリストに読み込んで、ライブラリでソートして、 そのまま出力する方法です。

ns = list[map(int, input().split())]
ns.sort()
print(*ns)

ライブラリは、プログラミング技法やアルゴリズム知識の資産です。 経験の多いプログラマは、ライブラリを使いこなし、活用できます。 ライブラリは、多くの利用者にテストされており、信頼性も高いです。

車輪を再発明しない

ライブラリがあるのならライブラリを使う

7.3. 複雑な問題へのアプローチ

プログラミングは、最終的に、ソフトウェアを開発するときに用いられます。ソフトウェア開発は、ソースコードの行数も1000行から1000行まで、桁違いに大きくなります。そのような複雑なプログラムを書くにはどうしたら良いのでしょうか?

7.3.1. 少し複雑なプログラム

少し複雑なプログラムを考えてみましょう。

例題(市松模様)

たて\(H\) よこ \(W\) の市松模様の長方形を描くプログラムを作成して下さい。

入力例:

3 4

出力例:

#.#.
.#.#
#.#.

まず、「先生動きません!助けて!!」と来た学生の書いたコード(抜粋)を見てください。

残念なコードの例

a, b = map(int, input().split())
kigusu=b%2;
for i in range(a/2):
        for j in range(b/2):
                print("#.", end='')
                if kigusu == 1:
                        print('#')
                print()
        for j in range(b/2):
                print(".#", end='')
                if kigusu == 1:
                        print('.')
                print()
if a%2 == 1:
        for j in range(b/2):
                print("#.", end='')
        if kigusu == 1:
                print('#')
        print()

いろいろ、残念なところがあります。

  1. 変数名が読みにくい。(a, bが何をさすのかわかりにくい)

  2. if文が多すぎて、状態が複雑すぎる

  3. 見通しが悪すぎて、書いた本人も何をしているのか覚えていない

見通しの悪い読みにくいコード

諦めて捨てて、見通しよく書き直しましょう

7.3.2. 見通しよくするコツ

まず大原則は、問題を小問題、つまりプログラム機能を小機能に分割することです。 プログラミングは、どんな複雑な問題であっても、 簡単な小問題に分割することでかけるようになります。 (もちろん、あとから述べる通り、プログラミングできない問題もあります。)

小問題(機能)に分割するのは、経験者にとっても難しい作業です。 そこで、書いたことのあるプログラムやシンプルなケースを考えてみます。 市松模様を気にせず、長方形を描画するだけなら簡単です。

H, W = map(int, input().split())
for h in range(H):
        for w in range(W):
                print('#', end='')
                print()
        print()

このとき、#の代わりに、 市松模様を生成してくれる関数があったら.. となりますよね。

H, W = map(int, input().split())
for h in range(H):
        for w in range(W):
                print(市松模様(), end='')
                print()
        print()

便利な関数があると信じて、 既に書いたことがあって書けそうなところから書いてゆくのは悪くありません。 すると、次に解決すべき課題が見えてきます。

7.3.3. 市松模様の生成

さて、市松模様を生成する関数はあるのでしょうか?

たぶん、Python にはこのような関数は都合よくありません。 なければ、自分で作ってしまえばよいのです。

ここは、市松模様とパラメータの関係性を考えます。 すると、横(w) と縦(h)の位置によって決まると気づけばおしまいです。

def 市松模様(w, h):
    if if w+h % 2 == 0:
        return '#'
    else:
        return '.'

for h in range(H):
        for w in range(W):
                print(市松模様(w, h), end='')
                print()
        print()

注意:読みやすさのため市松模様をそのまま関数名にしてあります。 Pythonは、日本語の関数名も使えます。

最初のごちゃごちゃしたコードの例に比べるとだいぶ見通しがよくなっていることに気づくと思います。 複雑なプログラムは、小機能に分割してプログラミングしてゆきます。

7.3.4. 方法序説

「大きくて複雑すぎる問題は、解ける小問題に分割して解く」というのは、 ルネ・デカルトが1637年に公刊された「方法序説」で述べられている手法です。 今日、科学的手法とも呼ばれ、現代科学の最も基礎的な考え方になっています。

6c983d7885fd4fc3950eb35461a42d93

プログラミングは、新しい言語や文法を学び、言語的なセンスも必要となります。 しかし、根っこの部分は、数学や理科で学んだ科学的手法、 つまり、難しい問題をどのように解くかという考え方が身についているかが、 大切になります。

7.4. 演習問題

繰り返しやリスト、ライブラリなどを総合的に使う問題に取り組んでみましょう。 できるだけ簡単に書いて見ましょう。