「リーダブルコード」を身につける【第Ⅰ部】表面上の改善

# はじめに
リーダブルコード（オライリー）では、「読みやすさの基本定理」を下記のように定義している。
> コードは他の人が最短時間で理解できるように書かなければいけない。

ここで、「他の人」とは、何週間後の自分自身も含む。また、「理解できる」とは、バグに気づき、コードの修正を行い、他のコードとの連携を設計することができることを指す。
本記事では特に、「表面上の改善」によるリーダブルコード達成方法を取りまとめる。
# 表面上の改善
コードの表面的な改善を行ううえで下記3項目に意識を向けると、十分に読みやすくなる。
1. [命名方法](#命名方法)
1. [コードの視認性](#コードの視認性)
1. [コメント](#コメント)
## 命名方法
### 情報を詰め込んだ名前
変数名、関数名、クラス名…に名前を付けるときは、簡潔かつ具体的に命名すべきである。

:::note info
- デザインパターンだと…
    - 適切に情報(変数の目的や値の意味)を伝えることができる（コメントが不要になる）
    - バグの発見に役立つ
:::

:::note alert
- アンチパターンだと…
    - 長ったらしく、コードのレイアウト的に難あり
    - 抽象的で誤解を与えてしまう
:::
「リーダブルコード」では、デザインパターンを達成するために以下6テーマを紹介している。
1. 明確な単語を選ぶ
    - 類語辞典を使用してより的確な単語がないか調べる
      ```python:anti-pattern
      # stopだと、一時停止(pause)なのか終了(kill)なのか判断が付かない
      def stop_computing():
      ```
      ```python:design-pattern
      # pauseなら一時停止とすぐにわかる！
      def pause_computing():
      ```
1. 汎用的な名前を避ける
    - tmpやretval、fooのようなどのような状況でも使えてしまう名前は極力使わない
    - イテレータが複数ある時は1つ1つのインデックスに明確な名前をつける（例：i、j、kではなく、club_i、members_i、users_iなど）
      ```python:anti-pattern
      # volume_addの計算式にて、jとkが入れ替わってしまっているが、
      # バグに気が付きにくい
      for i, u_vel in enumerate(df["u_velocity"]):
          for j, v_vel in enumerate(df["v_velocity"]):
              for k, w_vel in enumerate(df["w_velocity"]):
                  volume_add = (
                      df_height[i] * u_vel + 
                      df_height[k] * v_vel + 
                      df_height[j] * w_vel +                       
                  )
     
      ```
      ```python:design-pattern
      # インデックス名が明確であるため、バグに気が付きやすい！
      for u_i, u_vel in enumerate(df["u_velocity"]):
          for v_i, v_vel in enumerate(df["v_velocity"]):
              for w_i, w_vel in enumerate(df["w_velocity"]):
                  volume_add = (
                      df_height[u_i] * u_vel + 
                      df_height[w_i] * v_vel + 
                      df_height[v_i] * w_vel +                       
                  )
     
      ```

1. 抽象的な名前よりも具体的な名前を使う
    - メソッドの動作や変数の情報を漏れなく伝える意識で名前をつける
1. 接尾辞や接頭辞を使って情報を追加する
    - 計測できるものは変数名に単位をつける
      ```python:design-pattern
      start_ms = 0.5 #末尾にmsと付けることでミリ秒単位の変数とわかる
      ```
1. 名前の長さを決める
    - その名前がコード内の広範囲で使用される場合は、名前に十分な情報を詰め込むためにある程度長くてもよい
      ```python:anti-pattern
      # 略称を使用しているため、
      # 初見では何のデータを格納したDataFrameかわからない
      df_hlat_hmd = pd.read_csv(path)
      ```
      ```python:anti-pattern
      # 広範囲で使用される変数なら、多少長くても誰にでもわかる命名を行う
      df_highlatitude_humidity = pd.read_csv(path)
      ```
1. 名前のフォーマットで情報を伝える
    - 定数は大文字、変数・関数は小文字で示す
      ```python
      THRESHOLD_ANOMALY = 0.55 # 異常度スコアの閾値（定数）
      anomaly_score = 0.       # 異常度スコア（変数）
      ```
### 誤解されない名前
ユーザがコードを誤解なく読めるよう、名前をつけるときは下記のことに注意すると良い。
- 積極的に「誤解」を探す
- ユーザの「常識」に合わせる
- 複数の名前を検討する

以下で誤解されにくい名前の例をケースごとに示す。
- boolean型の変数には、has,is,can,shouldを頭につけると誤解されにくい
  ```python:design-pattern
  #先頭にisが付いているため、boolean型であることがすぐわかる
  is_boundary = True 
  ```
- boolean型の変数を設定するときは否定形を避ける
  ```python:anti-pattern
  #変数名に否定形を入れると、余計に考えなければいけないことが増える
  disable_mitigate = False
  ```
## コードの視認性
### 美しいコード構成
「優れたコードは目に優しい」という理念のもとに、全体的な視認性を向上させるコード構成を意識すると良い。
- 一貫性のあるコードが複数行続くとき…
    - スペースを活用して縦の位置を揃える
      ```python
      list_params = [
      #    height, depth,  velocity
      #    [m]     [km]    [m/s]
          [100,    15000,  97],
          [98,     9800,   10],
          [3022,   15000,  23],
      ]
      ``` 
    - メソッドを活用して一貫性のある操作を簡潔にまとめる
        - 似た操作が連続する場合はメソッドで操作をひとまとまりにしてしまう
    - 適切な改行により、一貫性を伝えやすくする
      ```python:anti-pattern
      plt.plot(df_high_altitude_temperature["1500m"],df_high_altitude_temperature.index, color="blue",label="1500m")
      plt.plot(df_moisture["1500m"],df_moisture.index, color="orange",label="moisture")
      ```
      ```python:design-pattern
      plt.plot(
          df_high_altitude_temperature["1500m"],
          df_high_altitude_temperature.index, 
          color="blue",
          label="1500m"
      )
      plt.plot(
          df_moisture["surface"],
          df_moisture.index, 
          color="orange",
          label="moisture"
      )
      ```
    - 並びに意味づけをし、統一する
        - 手順書に記載されている順
        - アルファベット順
        - etc...
- メソッド宣言やパラメータ設定では…
    - 論理的なグループにまとめることで、最初に各グループの存在を認識する
      ```python:design-pattern
      ### Parameters
      # S3 info
      S3_HEADER = "s3://"
      BUCKET    = "hoge"
      PREFIX    = "project_XXX/analysis/"
      TARGET_BUCKET = S3_HEADER + BUCKET + PREFIX
      
      # Input
      PATH_INPUT_FOLDER = "/hogehoge/hugahuga/"
      PATH_MART_NAME    = "hogehoge_mart.csv"
      PATH_INPUT = TARGET_BUCKET + PATH_INPUT_FOLDER + PATH_MART_NAME

      # Output
      PATH_OUTPUT_FOLDER = "/hogeout/hugaout/"
      PATH_OUTPUT_FILENAME = "XXX_hogeout.csv"
      PATH_OUTPUT = TARGET_BUCKET + PATH_OUTPUT_FOLDER + PATH_OUTPUT_FILENAME
      ```
- 一つのセルに複数行のコードを記述する際は…
    - コードを段落に分割し、各段落にタイトルコメントをつける
      ```python:design-pattern
      def list_s3_objects(bucket_name, prefix):
          """
          S3の対象フォルダ内に存在するファイル名、フォルダ名を取得する関数
          Args:  
            bucket_name(str): S3バケット名
            prefix(str): フォルダのパス（例: 'folder/subfolder/'）   
          Returns:
            list_files(list(str)):ファイル名のリスト
            list_folders(list(str)):フォルダ名のリスト
          """
          #指定したBucketとPrefixに一致するobjectの情報を取得
          s3 = boto3.client('s3')
          response = s3.list_objects_v2(Bucket=bucket_name, Prefix=prefix)

          # object情報にContentsが存在しない場合、空リストを返す
          if 'Contents' not in response:
              return [], []
            
          # フォルダ名、ファイル名を区別し各リストに分類
          objects = [obj['Key'] for obj in response['Contents']]
          list_files   = [obj for obj in objects if not obj.endswith('/')]
          list_folders = [obj for obj in objects if obj.endswith('/')]
            
          return list_files, list_folders
      ```
## コメント
### コメントすべきタイミング
コメントの目的は、書き手の意図を読み手に知らせることである。つまり、コメントすべき内容とは、知らせるべき意図である。
その観点から、コメントすべきことと、コメントすべきでないことをまとめた。

:::note info
コメントすべきこと
- コードを書いているときの気づき・考え
    - 欠陥に気づいたとき
    ```python
    df.iloc[1, 5:8] = [3.5, 0, 0] # HACK:マジックナンバーを使用している。
    ```
    - 定数を定義するとき
    ```python
    THRESHOLD = 0.3 # NOTE:正常データの95%以上が正常判定される閾値
    ```
    - その他のアノテーションコメント
    ```
    TODO:   後て対応すべきタスク
    FIXME:  不具合による修正箇所
    HACK:   リファクタリング対象（OPTIMIZEやDEPRICATEDで具体化させてもよい）
    REVIEW: 確認orテストが必要
    NOTE:   そのコードの意図
    ```
- 読み手の視点に立った説明
    - 読み手が質問しそうなこと
    - 読み手が陥りそうな罠
- ブロックごとのサマリー
    - （単位：大）スクリプトの一番上に数文で全体内容をサマる
    - （単位：中）notebookのセルごとに一文程度でタイトルを書く
    - （単位：小）関数内の段落ごとに動作の説明を書く
:::
:::note alert
コメントすべきではないこと
  - 「コードを読んで理解する時間」＝「コメントを読んで理解する時間」
      ```python
      # double_scoreはscoreを2倍した数値である
      double_score = 2 * score
      ```
  - 品質の低い名前の不足分を補うためのコメント（良い名前を付けることが先決）
      ```python
      # 東西、南北、鉛直方向の速度のプロファイル情報を読込
      df_speed = pd.read_csv(path_input)
      ```
:::
### 正確・簡潔なコメント
読み手の時間を奪わないコメントは正確かつ簡潔である。正確かつ簡潔なコメントを書くために心がけることを下記5項目でまとめた。
- 代名詞を避ける（読み手のキャパのために、「それ」「その」などを使わない）
  ```python:anti-pattern
  # S3内に対象名のオブジェクトがある場合、そのキーを返す
  def find_object(object_name):
  ```
  ```python:design-pattern
  # S3内に対象名のオブジェクトがある場合、対象オブジェクトまでのキーを返す
  def find_object(object_name):
  ```
- 実例を使う（適切なシチュエーションを選択し、動作のイメージを伝える）
  ```python:design-pattern
  def remove_elements(input_list, remove_value):
      """
      リスト内の特定の値を持つ要素を除外する関数
      Args:
        input_list(list()):対象リスト
        remove_value():除外対象の値
      Returns:
        (list()):除外対象の値を除外したリスト
      実例:
          >>> remove_elements([1, 4, 3, 2, 4], 4)
          [1, 3, 2]
      """
      return [element for element in input_list if element != remove_value]
  ```
- コードの意図を書く（プログラムより高い視座でコメントを書く）
  ```python:anti-pattern
  # "prefecture"ごとに"price"を計算した結果を降順で並び替える。上5行を返す。 
  df_price_top5prefecture = (
      df_price[["prefecture","price"]]
      .groupby("prefecture")
      .sum()
      .sort_values("price",asacending=False)
      .head(5)
  ) 
  ```
  ```python:design-pattern
  # 都道府県ごとの金額について、上位5都道府県名と金額を表示する
  df_price_top5prefecture = (
      df_price[["prefecture","price"]]
      .groupby("prefecture")
      .sum()
      .sort_values("price",asacending=False)
      .head(5)
  ) 
  ```
- 情報密度の高い言葉を使う（専門用語によって端的に説明する）
  ```python:anti-pattern
  # 所在地から余計な空白を除去し、"Avenue"を"Ave."にするなどの整形を施す。
  ```
  ```python:design-pattern
  # 所在地を正規化する(例:"Avenue"->"Ave.") 
  ```
# 終わりに
本記事では、リーダブルコーディングのための「表面上の改善」方法について紹介した。
以降の記事では、求められる「ルールとロジック」について紹介していければと思う。