💨
リーダブルコードチートシートPython版
第Ⅰ部 表面上の改善
2章 名前に情報を埋め込む
明確な単語を選ぶ
- 動作や目的に適した明確な名前を付ける
- 類義語をよく調べる
明確な名前付け
# Bad
def get_page(): # どこからページを取得するのか曖昧
# Good
def fetch_page(): # ネットからページを取得する場合
def download_page():
明確な名前付け
# Bad
def size(): # 何のサイズか分からない
# Good
def height():
def num_nodes():
def memory_bytes():
類義語
send # deliver, dispatch, announce, distribute, route
find # search, extract, locate, recover
start # launch, create, begin, open
make # create, set up, build, generete, compose, add, new
tmpやretvalなどの汎用的な名前を避ける
- エンティティ値や目的を表した名前を選ぶ
-
tmp
は生存期間が短く,一時的な保管が最も大切な変数にだけ使う - イテレータが複数ある時は,インデックスにもっと明確な名前を付けるとよい
目的を表した名前
# Bad
retval += v[i] * v[i] # 2乗した値の合計を保存しておく変数
# Good
sum_squares += v[i] * v[i]
一時的な保管
# Good
tmp = right
right = left
left = tmp
一時的な保管が目的ではない
# Bad
tmp = user.name()
tmp += " " + user.phone_number()
# Good
user_info = user.name()
user_info += " " + user.phone_number()
インデックスに名前を付ける
# Bad
for i in range(clubs.size()):
for j in range(members.size()):
# Good
for club_i in range(clubs.size()):
for member_i in range(members.size()):
for ci in range(clubs.size()):
for mi in range(members.size()):
抽象的な名前よりも具体的な名前を使う
# Bad
server_can_start(): # TCP/IPポートをサーバがリッスンできるかどうかを確認するメソッド
# Good
can_listen_on_port():
変数名に情報を追加する
- 変数名に単位を入れる
- 危険や注意を喚起する情報を追加する
変数名に単位を入れる
import time
# Bad
start = time.time()
# 計測したい処理
elapsed = time.time() - start
print("処理時間:", elapsed, "秒")
# Good
start_sec = time.time()
# 計測したい処理
elapsed_sec = time.time() - start_sec
print("処理時間:", elapsed_sec, "秒")
危険や注意を喚起する情報を追加する
# Bad
password # プレインテキストであるため,処理前に暗号化すべき
comment # ユーザが入力したcommentは表示する前にエスケープする必要あり
html # htmlの文字コードをUTF-8に変えた
data # 入力されたdataをURLエンコードした
# Good
plaintext_password
unesvaped_comment
html_utf8
data_ulrenc
名前の長さを決める
- スコープが小さければ短い名前でもいい
- プロジェクト固有の省略形はダメ
- 不要な単語を排除する
スコープが小さい
# Good
if debug:
m = {}
LookUpNamesNumbers(m)
print(m)
プロジェクト固有の省略
# Bad
be_manager # back_end_manager
# Good
eval # evaluation
doc # document
str # string
不要な単語を削除
# Good
to_string(): # convert_to_string():
名前のフォーマットで情報を伝える
- クラス,関数,変数,メンバ変数等で異なるフォーマットを使う
- 基本的にはコーディング規約やプロジェクト内の規約に従う
PythonClass # クラス,例外など
python_class # メソッド,関数,変数
PYTHON_CONST # 定数
3章 誤解されない名前
曖昧な表現を避ける
# Bad
filter(): # 選択するのか除外するのか不明
# Good
select(): # 選択する
exclude(): # 除外する
# Bad
length # 何の長さか不明
# Good
max_lenght # 最大の長さ
max_chars # 文字の長さ
限界値を含めるときはminとmaxを使う
# Bad
CART_TOO_BIG_LIMIT = 10 # 未満か以下か不明
# Good
MAX_ITEMS_IN_CART = 10 # 10以下となる
範囲を指定するときはfirstとlastを使う
# Bad
integer_range(start=2, stop=4) # [2,3] or [2,3,4]
# Good
integer_range(start=2, stop=4) # [2,3,4] 包含していることが明確
包含/排他的範囲にはbeginとendを使う
- 4月中を表す場合,
begin
の4月1日は含み,end
の5月1日は含まないとなる
# Bad
print_event_int_range(begin="04-01 00:00:00", end="04-30 23:59:59")
# Good
print_event_int_range(begin="04-01 00:00:00", end="05-01 00:00:00")
ブール値の名前
- 頭にis, has, can, shouldなどを付ける
- 否定形ではなく肯定形にする
頭にhasをつける
# Bad
space_left(): # 残りスペースを返すの?
# Good
has_space_left(): # 残りスペースがあればTrue
肯定形にする
# Bad
disable_ssl = False
# Good
use_ssl = True
軽い処理か重い処理かをユーザーに伝える
# Bad
get_mean() # 一瞬で取得するイメージ
size()
# Good
compute_mean() # 計算コストがかかるイメージ
count_size()
4章 美しさ
一貫性のある簡潔な改行位置
# Bad
class PerformanceTester:
wifi = TcpConnectionsSimulator(
500, # kbps
80, # millisecs latency
200, # jitter
1 # packet loss
)
t3_fiber = \
TcpConnectionsSimulator(
4500, # kbps
10, # millisecs latency
0, # jitter
0 # packet loss
)
# Good
class PerformanceTester:
# TcpConnectionsSimulator(throughput, latency, jitter, packet_loss)
# [kbps] [ms] [ms] [parcent]
wifi = TcpConnectionsSimulator(500, 80, 200, 1)
t3_fiber = TcpConnectionsSimulator(4500, 10, 0, 0)
メソッドを使った整列
# Bad
database_connection = database_connection()
error = ""
full_name = expand_full_name(database_connection, "Doug Adams", error)
assert full_name == "Mr. Douglas Adams"
assert error == ""
full_name = expand_full_name(database_connection, "John", error)
assert full_name == ""
assert error == "more than one result"
# Good
def check_full_name(partial_name, expected_full_name, expected_error):
database_connection = database_connection()
error = ""
full_name = expand_full_name(database_connection, partial_name, error)
assert full_name == expand_full_name
assert error == expected_error
check_full_name("Doug Adams", "Mr Douglas Adams", "")
check_full_name("John" , "" , "more than result")
縦の線をまっすぐにする
# Bad
details = request.POST.get('details')
location = request.POST.get('location')
# Good
details = request.POST.get('details')
location = request.POST.get('location')
一貫性と意味のある並び
- 対応するHTMLフォームの
<input>
フィールドと同じ並び順にする - 最重要なものから重要度順に並べる
- アルファベット順に並べる
宣言をブロックにまとめる・コードを段落に分割する
# Bad
def suggest_new_friends(user, email_password):
friends = user.friends()
friends_emails = set(f.email for f inf friends)
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)
# Good
def suggest_new_friends(user, email_password):
# ユーザーの友達のメールアドレスを取得する
friends = user.friends()
friends_emails = set(f.email for f inf friends)
#ユーザのメールアカウントからすべてのメールアドレスをインポートする
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)
5章 コメントすべきことを知る
コメントするべきではないこと
- コードから分かることをコメントに書かない
- ひどいコードを補う補助的なコメント
コードから分かる
# Bad
# 2番目の'*'以降をすべて削除する
name = '*'.join(line.split('*')[:2])
補助的なコメントを記述するのではなく適切な変数名をつける
# Bad
# レジストリキーのハンドルを開放する.実際のレジストリは変更しない.
delete_registry()
# Good
release_registry_handle()
自分の考えを記録する
- 考えを記録する
- コードの欠陥にコメントをつける
- 定数にコメントを付ける
考えを記録
# Good
# このデータだとハッシュテーブルよりもバイナリツリーの方が40%速かった
# 左右の比較よりもハッシュの計算コストの方が高いようだ
コードの欠陥コメント
# Good
# TODO: あとで追加,修正する
# FIXME: 既知の不具合があるコード
# HAXK: あまりきれいじゃない解決策
# XXX: 危険!大きな問題がある
定数にコメント
# Good
NUM_TUREADS = 8 # 値は「>= 2 * num_processors」で十分
MAX_RSS_SUBSCRIPTIONS = 100 # 合理的な限界値.人間はこんなに読めない
IMAGE_QUALITY = 0.72 # 0.72ならユーザはファイルサイズと品質の面で妥協できる
読み手の立場になって考える
- コードを読んだ人が疑問に思うところを予想してコメント
- 読み手が驚くような動作は文書化
- ファイルやクラスには「全体像」のコメント
- 読み手が細部にとらわれないよう,コードブロックにコメントを付け要約
6章 コメントは正確で簡潔に
- 複数のものを指す可能性がある「それ」や「これ」などの代名詞は避ける
- 関数の動作はできるだけ明確に説明する
- コメントに含める入出力の実例慎重に選ぶ
- コードの意図は,詳細レベルでなく,高レベルで記述する
- よくわからない引数にはインラインコメントを使う
- 多くの意味が詰め込まれた言葉や表現を使って,コメントを簡潔に保つ
第Ⅱ部 ループとロジックの単純化
7章 制御フローをよみやすくする
条件式の引数の並び方
# Bad:
if 10 == length:
while bytes_received < bytes_expected:
# Good: 左に期待値や比較対象の式を置く
if length == 10:
while bytes_expected > bytes_received:
if/elseブロックの並び順
- 条件は否定形よりも肯定形を使う
- 単純な条件を先に書く
- 関心を引く条件や目立つ条件を先に書く
肯定形を使う
# Bad
if not debug:
# Good:
if debug
関心を引く条件を先に処理
# Bad
if not url.has_query_parameter("expand_all"):
response.render(items)
else:
for item in items:
item.expand()
# Good: "expand_all"が関心を引く条件なため,これを先に処理する
if url.has_query_parameter("expand_all"):
for item in items:
item.expand()
else:
response.render(items)
三項演算子
- 基本的には誰もが分かりやすいif/elseを使い,三項演算子は簡潔になるときのみ使う
# Bad
return mantissa * (1 << exponent) if exponent >= 0 else mantissa / (1 << -exponent)
# Good
time_str += "pm" if hour >= 12 else "am"
関数から早く返す
# Good: 早期returnを使用する
def contains(str, substr):
if str is None or substr is None: return False
if substr == "": return True
ネストを浅くする
- 関数内で早期
return
を使う - ループ内で
continue
を使う
continueを使う
# Bad
for result in results:
if result is not None:
non_null_count += 1
if result != "":
print("considering")
# Good
for result in results:
if result is None:
continue
non_null_count += 1
if result == "":
continue
print("considering")
実行の流れを追えるか?
- スレッド:どのコードがいつ実行されるかよくわからない
- シグナル/割り込みハンドラ:他のコードが実行される可能性がある
- 例外:いろんな関数呼び出しが終了しようとする
- 関数ポインタと無名関数:コンパイル時に判別できないので,どのコードが実行されるかわからない
- 仮想メソッド:object.virtulMethod()は未知のサブクラスのコードを呼び出す可能性がある
使わない方が良い構文
- 基本的にdo/whileは避け,whileループを使う
- 基本的にgotoは使わない
8章 巨大な式を分割する
説明変数
# Bad
if line.split(':')[0].strip() == "root":
# Good: 式を表す変数を使う
user_name = line.split(':')[0].strip()
if user_name == "root":
要約変数
# Bad
if request.user.id == document.owner_id:
# 編集可能
if request.user.id != document.owner_id:
# 読取専用
# Good: 式を変数に代入しておく
user_own_document = request.user.id == document.owner_id
if user_owns_document:
# 編集可能
if not user_owns_document:
# 読取専用
ド・モルガンの法則を使う
# Bad
if not (file_exists and not is_protected):
# Good
if (not file_exists) or (is_protected):
9章 変数と読みやすさ
変数を削除する
- 役に立たない一時変数
- 中間結果を保持する変数
- 制御フロー変数
役に立たない一時変数
# Bad
now = datetime.datetime.today()
root_message.last_view_time = now
# Good
root_message.last_view_time = datetime.datetime.today()
制御フロー変数
# Bad
done = False
while 条件 and not done:
...
if 条件:
done = Ture
continue
# Good
while 条件:
...
if 条件:
break
変数のスコープ縮める
- グローバル変数をできるだけ使わない
- 大きなクラスを小さなクラスに分割する
- 定義の位置を下げる
変数は一度だけ書き込む
- 変数に一度だけ値を設定する
- イミュータブルにする
第Ⅲ部 コードの再構成
10章 無関係の下位問題を抽出する
抽出すべき機能
- 高レベルの目標と関係のない下位の処理
- 純粋なユーティリティコード(文字列操作,ファイルの読み書きなど)
- その他汎用的な機能
- やりすぎには注意
# Bad
def find_closest_location(lat, lng, array):
closest_dist = MAX_VALUE
for i in array:
...
# 高レベルの目標と関係ない複雑な処理
...
if dist < closest_dist:
...
return closest
# Good
def spherical_distance(lat1, lng1, lat2, lng2):
...
# 高レベルの目標と関係ない複雑な処理
...
def find_closest_location(lat, lng, array):
closest_dist = MAX_VALUE
for i in array:
dist = spherical_distance(...)
if dist < closest_dist:
...
return closest
# Bad
user_info = {"username": "...", "password": "..."}
user_str = json.dumps(user_info)
cipher = Chiper("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = chpher.update(user_str)
encrypted_bytes += chiper.final()
url = "http://exmaple.com/?user_info=" + base64.urlsafe_b64encode(encrypted_bytes)
# Good
def url_safe_encrypt(obj):
obj_str = json.dumps(obj)
cipher = Chiper("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = chpher.update(obj_str)
encrypted_bytes += chiper.final()
return base64.urlsafe_b64encode(encrypted_bytes)
user_info = {"username": "...", "password": "..."}
url = "http://exmaple.com/?user_info=" + url_safe_encrypt(user_info)
11章 一度に1つのことを
- 一度に1つのタスクを行う
- タスクをできるだけ異なる関数に分割,もしくは異なる領域に分割する
12章 コードに思いを込める
- ロジックを明確に説明する
- 既存のライブラリが何を提供してくれるかを理解し活用する
ロジックを明確に説明する
# Bad
if document:
if not is_admin_request() and (document["username"] != SESSION["username"]):
return not_authorized()
else:
if not $is_admin:
return not_authorized()
# Good
if is_admin_request(): # 管理者
pass
elif document and (document["username"] != SESSION["username"]): # 文書の所有者
pass
else: # その他
return not_authorized()
13章 短いコードを書く
- 汎用的なユーティリティコードを作って,重複コードを削除する
- 未使用のコードや機能を削除する
- プロジェクトをサブプロジェクトに分割する
- 定期的にすべてのAPIを読んで,標準ライブラリに慣れ親しんでおく
標準ライブラリに慣れ親しんでおく
# Bad: リストから重複を取り除くため関数を実装
def unique(elements):
temp = {}
for element in elements:
temp[element] = None
return list(temp.keys())
print(unique([2,1,2])) # [2,1]
# Good: set型を使えば解決
print(list(set([2,1,2]))) # [1,2]
14章 テストと読みやすさ
テストを読みやすく保守しやすいものにする
- テストのトップレベルはできるだけ簡潔にする
- 入出力のテストはコード1行で記述できるとよい
入出力のテストを1行で記述
# テスト対象の関数
class ScoredDocument:
def __init__(self):
self.url = ""
self.score = 0.0
def sort_and_filter_docs(docs):
docs.sort(key=lambda x: x.score, reverse=True) # スコアに基づいて昇順にソート
docs[:] = [doc for doc in docs if doc.score >= 0] # 負の値を削除
# Bad
def Test1():
docs = [ScoredDocument() for _ in range(5)]
docs[0].url = "http://example.com"
docs[0].score = -5
docs[1].url = "http://example.com"
docs[1].score = 1
docs[2].url = "http://example.com"
docs[2].score = 4
docs[3].url = "http://example.com"
docs[3].score = -5999
docs[4].url = "http://example.com"
docs[4].score = 3.0
sort_and_filter_docs(docs)
assert len(docs) == 3
assert docs[0].score == 4
assert docs[1].score == 3.0
assert docs[2].score == 1
Test1()
# Good
def check_score_before_after(scores, expected):
docs = [ScoredDocument() for _ in scores]
for doc, score in zip(docs, scores):
doc.score = score
sort_and_filter_docs(docs)
actual = [doc.score for doc in docs]
return actual == expected
check_score_before_after([-5, 1, 4, -5999, 3.0], [4,3,1])
エラーメッセージを読みやすくする
unittestモジュールを使ってより詳細なエラーメッセージを表示する
# Bad
a = 1
b = 2
assert a == b
# エラーメッセージ:aとbの中身が分からない
Traceback (most recent call last):
File "Main.py", line 5, in <module>
assert a == b
AssertionError
# Good
import unittest
class NormalTest(unittest.TestCase):
def test_normal_test(self):
a = 1
b = 2
self.assertEqual(a, b)
if __name__ == '__main__':
unittest.main()
# エラーメッセージ:詳細が表示される
F
======================================================================
FAIL: test_normal_test (__main__.NormalTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "Main.py", line 17, in test_normal_test
self.assertEqual(a, b)
AssertionError: 1 != 2
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
テストの適切な入力値を選択する
- 入力値を単純化
- 1つの機能に複数のテスト
入力値を単純化
# Bad
check_score_before_after([-5, 1, 4, -5999, 3.0], [4, 3, 1])
# Good
check_score_before_after([1, 2, -1, 3], [3, 2, 1])
1つの機能に複数のテスト
# Good: 小さなテストを複数作る方が良い
check_score_before_after([2, 1, 3], [3, 2, 1]) # ソート
check_score_before_after([-10, -1, 3], [3]) # マイナスは削除
check_score_before_after([1, -2, 1, -2], [1, 1]) # 重複は許可
check_score_before_after([], []) # 空の入力は許可
テストの機能に名前を付ける
- クラス名や関数名,状況やバグなどを名前に含める(テストの機能名は長くて良い)
# Bad
Test1()
Test2()
# Good
test_sort_and_filter_docs()
test_sort_and_filter_docs_basic()
テスト容易性の低いコード
- グローバル変数を使っている
- 多くの外部コンポーネントに依存
- コードが非決定的な動作をする
テスト容易性の高いコード
- クラスが小さい.あるいは内部状態を持たない
- クラスや関数が1つのことをしている
- クラスは他のクラスにあまり依存していない
- 関数は単純でインターフェースが明確
Discussion