名著「リーダブルコード」を読みました!!!
特に7・8・9章が感動しました!!!
内容を全部まとめていくのもあれなので、
大事!と思った以下の「5つ(+1)」を所感も交えながら、抜粋します!
競プロよりの記事ですw
(0.本書の目的)
1.変数名を明確にする
2.書くべきコメント
3.制御フローを読みやすくする
4.巨大な式を分割する
5.変数と読みやすさ
#そのまえに
競プロは一人でやるプログラムなんだから、読みやすさも何も自分さえ分かればいいんだよ!
も一つの考えですが、
複雑なコードになってしまい、
入出力例を試したら正しくない出力結果が返ってきた時に
どこにバグがあるの〜???
となって、複雑なコードのどこに原因があるか調べるのに、多くの時間を取られた経験の人は多いはず(?)。
変数名のつけ方、コメントの記述、制御フロー(条件文やループ処理等)を少し工夫するだけで、バグやデバッグ時間が激減する。
かもしれない。
(それに、本書は名著なので読んでマイナスになることは100%ありません。)
#(0.本書の目的)
リーダブルコードでは、本の副題にもありますが、
「より良いコードを書くためのシンプルで実践的なテクニック」
が具体例を交えながら、たくさん散りばめられております。
では、その「良いコード」とは具体的になんなのか?
本書では
「良いコード」 = 「理解しやすいコード(読みやすいコード)」
としています。
プログラムというものは、よくよく考えると、コードを書くよりも読む時間のほうが圧倒的に多いです。
(競プロの1人でやるプログラムでさえそうなのだから、チームでプログラムをやるとなるとなおさら!)
「理解しやすいコード」は、見ただけでわかりやすいのでバグもみつけやすく、保守性も強いんです!!!
「理解しやすいコード」を書くためのテクニックを身につけることが本書の目的です。
#1.変数名を明確にする
変数名(や関数名など)にどんな名前をつけるか、悩みますよね?
よくtmp
とかretval
という変数を使いたくなりますが、本当に大丈夫?
例:ユークリッド距離
def Euclidean_distance(A):
retval = 0
for i in range(A):
retval += A[i]
return retval**0.5
もし、retval
ではなくsum_square
という変数にしていれば、
sum_squares += A[i] #合計する「square(2乗)」がない。バグだ!
とバグにすぐに気付けるかもしれませんね。
def Euclidean_distance(A):
sum_squares = 0
for i in range(A):
sum_squares += A[i]**2
return sum_squares**0.5
まあ5行程度のプログラムとかなら、全然tmp
やretval
で問題ないと思うんですけどねw
複雑なコードになりそうと思ったら、変数名をしっかり考えるのは大事かな?と。
「2乗 英語」でググれば5秒で「square」と出てきますしね!
変数名をつける時は、名前が明確かどうか一度立ち止まってみよう!バグも未然に防げるかもね!
#2.書くべきコメントと書くべきでないコメント
本書のどこかに書いていましたが、人間の脳は同時に3つくらいのことしか考えられないようです。
「精神的スタック」が4つ以上くらいになるコードは、「理解しにくいコード」です!
複雑なコードや混乱の元になりそうな予感がした時は、
コメントを記載して、「精神的スタック」からPop
してやりましょう!
書くべきコメント例:「0_indexed」か、「1_indexed」か
dpmap = [0]*(N+1) #1_indexed
「0_indexed」にすべきか、「1_indexed」にすべきか、
問題によって変わると思います。
特に初心者は、index番号の扱いに苦戦していると思います。
コメントを書いてしまえば、頭で思考する量は減るでしょう!
書くべきでないコメント例:ユークリッド距離その2(複雑なコードを説明するためのコメント)
for i in range(N):
if i%2==0:
# iが偶数の時は、リストの平方和のルートを答えにたす
sum_squares = 0
for i in range(A):
sum_squares += A[i]**2
ans += sum_squares**0.5
else:
...
コードが複雑になってくるから・・・と、わかりにくいコードの補足コメントになってしまうのは微妙。
理想は、上からすらすら「精神的スタック」をためずに読めるコードがいい!
日本語を読むだけで「精神的スタック」がたまります。
だったら、そもそもわかりにくいコード自体を修正してみましょう!
今回の例だと、わかりにくい部分は関数として別ロジックにしちゃう。
関数名もしっかりつけてあげれば、コメントも不要になります。
def Root_sum_squares(A):
sum_squares = 0
for i in range(A):
sum_squares += A[i]**2
return sum_squares**0.5
for i in range(N):
if i%2==0:
ans += Root_sum_squares(A)
else:
...
iが偶数の時は、答えにRoot_sum_squares
を足してるんだなぁ、と全体像が分かる。
その上でRoot_sum_squares
の中を見てみると、リストの平方和のルートを返していることがわかる。
これが修正前のコードNG.py
だと、
iが偶数の時・・・平方和を計算して、ルートして、答えに足して・・・
と頭の中で考えることがいっぱいいっぱいになってしまう。バグ発生時もソースの理解に一苦労。
ロジックを分けることで、コードもわかりやすくなり、すっきり。デバッグ時間も減ることでしょう!
コメントを記載して、「精神的スタック」からPop
してやろう!
ただし、わかりにくいコードの補足コメントを書くくらいであれば、そのコード自体を修正できないか?一度立ち止まってみよう!
#3.制御フローを読みやすくする
制御フロー(条件文やループ処理等)は、コードを読む時に読み返したりしないように書けるといい!
そのためのテクニックが3つほどある!
①条件式の引数の並び順
②if/elseブロックの並び順
③ネストを浅くする
###①条件式の引数の並び順
NG.py
とOK.py
見比べると・・・
if 10 <= length:
...
while bytes_expected > byte_received:
...
if length >= 10:
...
while byte_received < bytes_expected:
...
2つともOK.py
のほうが直感的に読みやすいと思うが、これには法則がある!
左側:「調査対象」の式。変化する。
右側:「比較対象」の式。あまり変化しない。
無意識にできている気がするが、迷ったらこの法則を使おう!
###②if/elseブロックの並び順
NG.py
とOK.py
見比べると・・・
if a!=b:
#ケース2
else:
#ケース1
if a==b:
#ケース1
else:
#ケース2
どちらも同じ処理だが、OK.py
のほうが直感的に読みやすい。
これにも法則がある!!
A.条件は否定形よりも肯定形を使う!
上記の例の通り。
B.単純な条件を先に書く!
ifの後ろにすぐelifとかくるのでわかりやすい。
C.関心を引く条件や目立つ条件を先に書く!
先に書いてあげることで「精神的スタック」からPop
できる。
A・B・Cは競合することもあったりするので、その時は優先度を個別に判断しよう!
これもある程度は無意識にできている気がするが、迷ったらこの法則を使おう!
###③ネストを浅くする
本文からそのまま引用。
ネストの深いコードは理解しにくい。ネストが深くなると、読み手は「精神的スタック」に条件をプッシュしなければいけない。閉じ括弧(})を見てスタックからポップしようとしても、その条件がなんだったのかうまく思い出せない。
exit()
、return
、break
、continue
等を駆使し、ネストを減らしていこう!
#4.巨大な式を分割する
競プロあるあるw
1行で色々頑張ろうとするw
コード例(ひどいw雰囲気だけ感じてくださいw)
for i in range(N):
for j in range(M):
if S[i][j][A:B+1][::-1][::2]=='HANAKO':
...
for i in range(N):
for j in range(M):
name = S[i][j][A:B+1][::-1][::2]
if name=='HANAKO':
自分の頭の中で処理しきれるうちはいいですけど、
見やすいコードが正義ですし、バグも少なくなるはず!
長すぎる式は積極的に分割しよう!
#5.変数と読みやすさ
できる限り、変数のスコープを小さくしましょう。
一時的な変数(tmp)などを使うなら、直前で変数宣言しましょう!
でないと、想定外の箇所で変数が上書きされるなどのバグ発生につながったり、「精神的スタック」もたまっていくし、とにかく理解しにくいコードになります!
コード例(これもひどいw雰囲気だけ感じてくださいw)
tmp = X
for i in range(N):
for j in range(M):
if (i+j)%3==1:
tmp *= 5
ans += tmp
for i in range(N):
tmp = X
for j in range(M):
if (i+j)%3==1:
tmp *= 5
ans += tmp
tmp
はfor内しか使わないのであれば、わざわざ外に出さない!
(NG.py
は、tmpがずっと初期化されないというバグも・・・)
OK.py
だと、tmp
についてはfor文の中だけ考えれば良い!
変数のスコープを小さくしよう!
おわり!
ここで紹介した内容は、リーダブルコードの内容の本当にごく一部です!
とりあえず、リーダブルコードは家に置いておきたい一冊。
また読み返そうと思います。