💨

リーダブルコードチートシートPython版

2023/06/26に公開

第Ⅰ部 表面上の改善

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