「リーダブルコード」を読んだのでアウトプット

TL;DR

  • ちゃんと名前付けをする!
  • ブール値やブール値を返す関数は肯定形で!
  • 整列させて見た目をきれいに!
  • コメントは動作内容よりも目的や背景を!
  • ネストは浅く!
  • 説明変数・要約変数を導入する!
  • 大きな処理は下位問題を抽出して分割する!
  • 言語仕様やAPIのドキュメントを参照できるように準備しておく!

はじめに

タイトル通り,前々から読みたかった「リーダブルコード」を読みました.
本記事はそのアウトプットです.

コードは他の人が最短時間で理解できるように書かなければいけない.

www.oreilly.co.jp

ネーミングに関する指針

何も考えずにプログラムを書いていると,よくvarだのtmpだのvalueだのといった意味のない変数名を使ってしまいがちです.

また,意味があったとしても,retvalindexのように汎用的だったり抽象的だったりする名前を付けがちです.

以下は,(やりすぎな)例です.

def calc(h, w):
  r = w / (h**2)

  if r < 18.5:
    m = "slim"
  elif r >= 25:
    m = "fat"
  else:
    m = "healthy"

  return m

もはや適当すぎて何をやっているのかすら良く分かりません.
実際は,入力された身長と体重からBMIを計算して,結果に応じたメッセージを返却する関数です.

このようなコードは,関数名や変数名がそれ自身で意味を表すようにすると分かりやすくなります.

def get_message_for_bmi(height_meter, weight_kg):
  bmi = height_meter / (weight_kg ** 2)

  SLIM_RANGE_END    = 18.5
  HEALTHY_RANGE_END = 25.0

  if bmi < SLIM_RANGE_END:
    return "slim"
  elif bmi < HEALTHY_RANGE_END:
    return "healthy"
  else:
    return "fat"

かなり読みやすくなったと思います.
上記のコードからは,簡単に以下の事柄が推察できます.

  • この関数はbmiに対するメッセージを返す関数なんだろう
  • 引数として,メートル単位の身長とキログラム単位の体重を受け取るんだろう
  • XXX_RANGE_ENDは範囲の上限を表す定数なんだろう
  • 最終的に,bmiが範囲のどの部分に当てはまるかによって,異なったメッセージが返されるんだろう

便利なツール

類語辞典・シソーラス・対義語 - Weblio辞書
一番おすすめはシソーラスです.プログラミングに限らず,文章を書く時にも有用です.

codic - プログラマーのためのネーミング辞書
最近だと,codicとかも良いんではないでしょうか.

ブール値に関する指針

基本的に,否定形のブール値や条件判断は,肯定形のそれと比べて読みにくいことが多いです.

if is_not_logged_in:
  return loginPage
else:
  return userPage

肯定形に書き直してみます.
こちらの方が直感的に読めるのではないでしょうか.

if is_logged_in:
  return userPage
else:
  return loginPage

美しさに関する指針

見た目がきれいなコードがすべて読みやすいとは限りませんが,読みやすいコードは見た目もきれいだと思います.
具体的な指針は以下の通りです.

整列する

このコードより

first_greeting = "Hello"
name = "Szarny"
last_greeting = "bye"

print(first_greeting, name, last_greeting)

整列された以下のコードの方が読みやすと思います.

first_greeting = "Hello"
name           = "Szarny"
last_greeting  = "bye"

print(first_greeting, name, last_greeting)

意味のあるまとまりに分ける

意味的に分かれているコードは,見た目上でも分かれさせた方が見やすくなります.

こんなコードはごちゃごちゃしていて読む気になりませんが,

# 与えられた得点リストの平均と標準偏差を返す
def calculate_stats_of_test_points(points):
  total_point = 0
  for point in points:
    total_point += point
  average = total_point / len(points)
  deviation_square = 0
  for point in points:
    deviation_square += (point - average) ** 2
  variance = deviation_square / (len(points) - 1)
  standard_deviation = variance ** 0.5

  return (average, standard_deviation)

以下のようなコードは意味的にチャンキングされていて読みやすく感じます.(pythonならこんな処理書く必要ないですが...)

# 与えられた得点リストの平均と標準偏差を返す
def calculate_stats_of_test_points(points):
  # 総得点を求める
  total_point = 0
  for point in points:
    total_point += point

  # 平均を求める
  average = total_point / len(points)

  # 偏差の二乗平均を求める
  deviation_square = 0
  for point in points:
    deviation_square += (point - average) ** 2

  # 分散を求める
  variance = deviation_square / (len(points) - 1)

  # 標準偏差を求める
  standard_deviation = variance ** 0.5

  return (average, standard_deviation)

コメントに関する指針

コメントすべきではないことと,コメントすべきこと

コメントはコードからすぐに読み取れることを書くべきではありません.

TIMEOUT_LIMIT_SEC = 10 # タイムアウトを10秒に設定する

動作内容そのものよりも,その処理や変数を用いる目的や背景をメモしておくべきです.

TIMEOUT_LIMIT_SEC = 10 # 10秒経っても処理が終わらなければ大抵エラー

関数については,動作例を示しておくと分かりやすくなります.

# 与えられたリストから平方数の要素のみを返却する
# [1, 2, 4, "abc", 9, 25, 55, 100] -> [1, 4, 9, 25, 100]
def extractSquareNumbers(numbers):

制御フローに関する指針

人間は,何段にもネストされたコードを読むとき,そのコードをブロックごとに頭の中のスタックにプッシュしていく必要があります.

以下のような関数は,最初の条件分岐が関数全体に影響しているため,肝心な部分を集中して読むことができません.
また,Trueの時の処理を読み終わってelse節に入るときに,「そもそも条件ってなんだったっけ」となると読み返す必要が生じます.

# リストarrayに含まれる要素targetの数を返却する
# count_elements([1,2,3,2,3,2,3], 2) => 3
def count_elements(array, target):
  if(type(array) == type([])):

    match = 0

    for elem in array:
      if elem == target:
        match += 1

    return match

  else:
    return 0


なるべくネストを深くせずに,ガード節を設置して早く結果を返すようにすれば,精神的な負担が減ります.

# リストarrayに含まれる要素targetの数を返却する
# count_elements([1,2,3,2,3,2,3], 2) => 3
def count_elements(array, target):

  # arrayがリストでなければ0を返す
  if(type(array) != type([])):
    return 0

  match = 0

  for elem in array:
    if elem == target:
      match += 1

  return match

巨大な式に対する指針

巨大な式や複雑な式は,それが何を表しているのか説明する説明変数や,メタな視点から捉えた要約変数が有効です.

BMI計算関数の例(を改悪したもの)を再掲します.

def get_message_for_bmi(height_meter, weight_kg):
  SLIM_RANGE_END    = 18.5
  HEALTHY_RANGE_END = 25.0

  if height_meter / (weight_kg ** 2) < SLIM_RANGE_END:
    return "slim"
  elif height_meter / (weight_kg ** 2) < HEALTHY_RANGE_END:
    return "healthy"
  else:
    return "fat"

条件分岐のあたりが読みにくく感じます.
そもそも,height_meter / (weight_kg ** 2) BMIの数値を表しているため,単純に説明変数で置き換えるだけですっきりします.

def get_message_for_bmi(height_meter, weight_kg):
  bmi = height_meter / (weight_kg ** 2)

  SLIM_RANGE_END    = 18.5
  HEALTHY_RANGE_END = 25.0

  if bmi < SLIM_RANGE_END:
    return "slim"
  elif bmi < HEALTHY_RANGE_END:
    return "healthy"
  else:
    return "fat"

巨大な関数についての指針

ある問題を解決するための「巨大な関数」や「ひとかたまりの処理」は,いくつかの下位問題から構成されていることが多いです.
このような時は,そのカタマリの主目的とは関係のない部分を抽出するとうまくいきやすいです.

以下は,与えられた2つのファイルのハッシュ値が同一か検査する関数です.

# 2つのファイルのハッシュ値が同一か検査する
def is_hash_same(filename_1, filename_2):
  hash_value_1 = ""
  hash_value_2 = ""

  with open(filename_1, "rb") as f1:
    contents_1 = f1.read()
    sha256 = SHA256.new()
    sha256.update(contents_1)
    hash_value_1 = sha256.hexdigest()

  with open(filename_2, "rb") as f1:
    contents_2 = f1.read()
    sha256 = SHA256.new()
    sha256.update(contents_2)
    hash_value_2 = sha256.hexdigest()

  return hash_value_1 == hash_value_2

この関数の主目的は,ハッシュ値が同一か検査すること」のみです.

そのため,ファイルのオープンやハッシュ関数用モジュールの設定を事細かに記述するべきではありません.
これらは,主目的とか関係ない下位問題であるからです.

以下のように書き換えるとすっきりします.

# 2つのファイルのハッシュ値が同一か検査する
def is_hash_same_new(filename_1, filename_2):
  contents_1 = get_contents_of_file(filename_1)
  contents_2 = get_contents_of_file(filename_2)

  hash_value_1 = get_hash_value(contents_1)
  hash_value_2 = get_hash_value(contents_2)

  return hash_value_1 == hash_value_2

# ファイルの内容を取得する
def get_contents_of_file(filename):
  with open(filename, "rb") as f:
    return f.read()

# 入力値のSHA-256ハッシュ値を返却する
def get_hash_value(data):
  sha256 = SHA256.new()
  sha256.update(data)
  return sha256.hexdigest()

このコードには,以下のような利点もあります.

  • 抽出した関数は汎用的なので,再利用が容易である
  • 関数は独立しているので,修正や拡張が容易である

コードの長さに関する指針

最も読みやすいコードは,何も書かれていないコードだ.

複雑に書くよりもシンプルに書く方がいいに決まっています.

以下は,リストの重複を除く関数です.

def make_distinct(array):
  distinct_array = []

  for item in array:
    if item not in distinct_array:
      distinct_array.append(item)

  return distinct_array

これでも正しく動作しますが,もっと簡単に書けます.

def make_distinct(array):
  return list(set(array))

このような処理をサッと書けるようになるには,APIドキュメントを読んでおいて,必要な時に参照できるようになっておくと良いとのことです.
覚えておくのには限界がありますしね.

おわりに

「リーダブルコード」のアウトプットを行いました.

これでもまだ書籍全体の半分にも満たないと思います.
説明がうまく,コーディング例も平易なものが多かったので,肩に力を入れることなく読める一冊でした.おすすめです!

次は,デザインパターンとかのプログラムの構造に重きを置いた書籍を読みたいです.