長い式や複雑な式には説明変数を使う

概要

変数は、後続処理でも使いまわしたいものを一時的に格納しておくためのもので、再利用しないのであれば変数を使う必要はないと思っていた。
が、リーダブルコードを読んで「説明変数」という言葉を知り、その限りではないと思ったときがあったのでメモしておく。

まえおき

ここでは、統計学の文脈で出てくる説明変数ではなく、リーダブルコードに載っている説明変数について記載する。

説明変数について

まずは単語の理解から整理したい。
リーダブルコードには、説明変数について以下のような説明がなされている。

式を簡単に分割するには、式を表す変数を使えばいい。この変数を「説明変数」と呼ぶこともある。式の意味を説明してくれるからだ。
例えば、以下のようなコードがあったとする。


if line.split(':')[0].strip() == "root":
    ...

説明変数を使えば、以下のようになる。


username = line.split(':')[0].strip()
if username == "root":
    ...

たしかに、line.split(':')[0].strip() == "root" の場合、line から何かの文字列を取り出して root かどうかを判定しているのはわかるが、何の文字列を取り出しているのかはぱっと見わからない。
line.split(':')[0].strip() 部分を username という変数に格納することで、この式ではユーザー名を抽出していることが明確になり、処理内容が把握しやすいコードになった。

要約変数というのも登場する

リーダブルコードでは、説明変数の他に要約変数というのも登場する。
要約変数については以下のように書かれている。

式を説明する必要がない場合でも、式を変数に代入しておくと便利だ。
大きなコードの塊を小さな名前に置き換えて、管理や把握を簡単にする変数のことを要約変数と呼ぶ。
例えば、以下のコードの式を考えてみよう。


if (request.user.id == document.owner_id) {
  // ユーザはこの文書を編集できる
}
...
if (request.user.id != document.owner_id) {
  // 文書は読み取り専用
}

request.user.id == document.owner_id はそれほど大きな式ではない。でも、変数が5つも入っているから、考えるのにちょっと時間がかかる。
このコードが言いたいのは「ユーザは文書を所持しているか?」だ。要約変数を追加すれば、この概念をもっと明確に表現できる。


final boolean user_owns_document = (request.user.id == document.owner_id);

if (user_owns_document) {
  // ユーザはこの文書を編集できる
}
...
if (!user_owns_document) {
  // 文書は読み取り専用

説明変数と要約変数

正直、説明変数も要約変数も自分にはどちらも同じようなものに思えてしまったが、概ね以下のように理解した (することにした)。

  • 説明変数
    • 長い式や、ぱっと見わかりにくい複雑な式に対して、その式から出力されるものが何かを説明するような名前をつける
    • その式が何を出力するのかを説明するのが目的の変数なので、1度しか登場しない式に使っても問題ない
  • 要約変数
    • 説明するほどの長い式ではないが、この式がやろうとしていることの概念に名前をつける
    • とくに、上記のサンプルコードのように、request.user.id == document.owner_idrequest.user.id != document.owner_id は、どちらも「文書の編集権限の有無」を判定しているものなので、「文書の編集権限の有無」を表すような変数に入れたほうが短くてわかりやすいし、コード量も減って見通しがよくなる

どちらも、式を説明していることに変わりはないと思うし、明確に使い分けないといけないものではないように感じたが、自分は上記のように理解した。
今回は、概要に記載の通り、説明変数に着目したいと思う。

説明変数の使い所

上記のサンプルコードのように、インデックスアクセスする場面では積極的に説明変数を使うとよさそうに思った。


# 旅の移動データがタプルで格納されている (距離km, 時間h)
journey = (150, 2.5) 

# 説明変数を使わない場合
print(f"平均速度: {journey[0]} / {journey[1]} km/h")  # インデックス0は何だったかのぅ..となる

# 説明変数を使った場合
distance = journey[0]
time = journey[1]
print(f"平均速度: {distance} / {time} km/h")  # 距離/時間、わかりやすい

テキスト整形のコードも説明変数に入れるとわかりやすくなる。


# 説明変数を使わない場合
if "777" in "".join(re.findall(r"[0-90-9]+", text)).translate(ZEN2HAN)  # えーっとコレは何しているんだろなっと..
  print("ラッキーニャンバーにゃ!")

# 説明変数を使った場合
# 半角数字の文字列が格納されていることが予想できる
hankaku_nums = "".join(re.findall(r"[0-90-9]+", text)).translate(ZEN2HAN)  

if "777" in hankaku_nums:
  print("ラッキーニャンバーにゃ!")

商品の価格などを計算する場合にも活用するとよさそう。


# 説明変数を使わない場合
# 「(商品の価格 * 消費税) - 割引額 + 送料」のようなコメントがないとわかりにくい
total_price = (1000 * 1.08) - 200 + 500 

# 説明変数を使う場合
item_price = 1000
tax = 1.08
discount = 200
shipping_cost = 50
total_price = (item_price * tax) - discount + shipping_cost  # コメントがなくても意味がわかる

逆に、式の内容がぱっと見で自明なものには、説明変数を使う必要はない。

例えば、以下の datetime.now() のようなものは、説明変数に入れる必要はない。
複雑な式を分割しているわけでもなく、後続処理で使いまわしているわけでもなく、このままでも現在日時というのが明確なので、now を使う意味がない。
むしろこれは、リーダブルコードでいう「役に立たない一時変数」に当てはまる。


now = datetime.now()
...

同様に、以下のようなコードも "" だけで空文字であることは明確なので、empty_string という名前の変数に格納するのは冗長で意味がない。


empty_string = ""

if "meow" in title:
  category = "cat"
else:
  category = empty_string

おわり

振り返ってみると、長い式を一旦変数に格納して名前をつけるというのは自然にやっていたように思う。
が、「説明変数」という名前がついていることを知ったおかげで、レビューや会話がしやすくなった気がする。

参考