Python
Python3
可読性

Pythonで可読性を向上させる

内容

Pythonで可読性を高めるために行っていることをまとめていきます.
まだプログラミングを始めて長くないので不適切な部分も絶対にあると思うので, その辺りはコメントしてくださると助かります.
こんな良い方法があるよ, という方は是非コメントください.

外観

PEP8に準拠する

外観としてはまずこれがあげられるでしょう.
参考 : pycodestyle 2.4.0
toolを使って確認する事ができます.

この辺りに関しては, 時間のある時にでも別の記事に書いてみたいと思います.

コメントアウトをする

友人の友人のデバッグを頼まれたことがありますが, 1文字もコメントアウトされていないC言語で, 流石に厳しいものがありました.
適宜コメントアウトをするのは当然だと思いますが, そう思っていない人もいるようなので一応...

コメントアウトする際, 複数行(長め)に渡って処理が行われる場合,

# ----- コメントアウト 1 -----
    処理1-1
    処理1-2
    処理1-3
    処理1-4
    ...
    # コメントアウト 1-k
    処理1-k
    ...
    処理1-N

# ----- コメントアウト 2 -----
    処理2-1
    処理2-2
    処理2-3
    処理2-4
    ...
    処理2-N

のように, 処理に対してindentを左にズラし, かつ5個のハイフンで囲むというルールを自分で決めています. 1行の処理に対するコメントアウトはズラさずハイフンも入れないことにしています. ハイフンがある場合は処理の範囲が分かるようにズラしています.

[追記] そもそもコメントアウトがいらぬように関数名などを定める, というのが原則で, なるべくコメントアウトの量は減らした方が良さそうです(コメントアウトだけ訂正し忘れるなどの弊害もあるため).

適宜改行する

ありえない程改行がないコードをたまに見ます.

for each_x in x:
     # ----- 処理1 -----
     # ----- 処理2 -----  
     for each_y in y:
         # ----- 処理3 -----
         # ----- 処理4 -----
            for each_z in z:
                # ----- 処理5 -----
                # ----- 処理6 -----

読みづらくないんでしょうか. 自分はかなり読みづらさを感じます.

for each_x in x:
     # ----- 処理1 -----
     # ----- 処理2 -----

     for each_y in y:
         # ----- 処理3 -----
         # ----- 処理4 -----

            for each_z in z:
                # ----- 処理5 -----
                # ----- 処理6 -----

の方が確実に読みやすいと思うのです.

変数

よく使う名前を当てる

友人にデバッグを頼まれた時, 配列の長さを"nagasa"という変数に格納していました. 最初一瞬なんの変数だか分かりませんでした. まさかの日本語をそのままローマ字にしてありました. 配列の長さなら"length"や"len"あたりがよく使われる変数名だと思います. よく使う変数名に関しては, やはりそちらを使った方が他人が読む際にも自分が他人のを読む際にも読みやすくなると思います.

要素を複数持つ変数には複数形で命名する

例えば, ファイル操作をするためにファイル名を複数格納したリストを作成したいとします. その際, "files"という命名が良いでしょう.
"data"に関しては微妙なところで, 厳密には複数形ですがネイティブでも単数形として扱っている人もいます.
そもそも"data"だけでは何のデータかわかりませんし, "data"以外の, 何のデータかわかるような命名をした方が賢明でしょう.

定数は大文字で書く

ファイルのパスなど, 基本的に変わらない変数は大文字で書いた方が良いです.

# これだと普通の変数に見える
save_dir = "..."

# これなら定数に見える
SAVE_DIR = "..."

変数は適宜多く使う

無理にコードを短くしようとすると可読性が下がります.

np.save("../dir1/dir1_1", x)

よりも,

DATA_ARR_SAVE_DIR = "../dir1/dir1_1"
np.save(DATA_ARR_SAVE_DIR, x)

の方が分かりやすいです.

使わない変数には_(アンダースコア)を使う

複数の返り値を持つ関数を使う場合, 全ての変数を実際に使うとは限らないと思います. 例えば, 機械学習でデータセットを使う場合, load_datasetという関数がtrain_X, train_T, test_X, test_Tという返り値を持つとします. しかし, ここではtest_Xしか使わないというケースに遭遇したとします. その場合,

_, _, test_X, _ = load_dataset()

とした方が良い(と思っています).
また, 単に複数処理をしたいだけの場合, for文で変数を割り当てず,

for _ in range(100):
    # ----- 処理 -----

とした方が良いでしょう.

for 単数形 (あるいは, each_~) in 複数形 で書く

for文の変数にやたらとi, j, kを使ってしまいがちですが, 出来るだけ意味のある変数にした方が良いです.
例えば, fileが複数格納されているオブジェクト, filesから各ファイルに対して処理をする場合,

for i in files:
    # ----- 処理 -----

ではなく,

for file in files:
    # ----- 処理 -----

とした方がfor文の中の処理が分かりやすくなります. 行列の各値ならrow, col, 画素値へのアクセスならw(width), h(height), または, x, yなどが良いでしょう.

[追記] integer, indexの略としてiを使うのは良いと思います.

先を見据えて変数名を統一する

例えば, normalなarrayデータxと5%刻みにノイズ加工したarrayデータx
["x.npy", "x_noise_5per.npy", "x_noise_10per.npy", ..., "x_noise_50per.npy"]
があるとします. 普通ならx.npyとx_noise_5per.npyのようにするでしょう.
しかし, 後にこれらを比較して, 10%までのnpyの読み込みをしたいします. その場合, 今のままだと,

max_noise_per = 10

for noise_per in range(max_noise_per):
    if noise_per == 0: 
        LOAD_DIR = "x.npy"
        x = np.load(LOAD_DIR)
        # ----- 処理 -----

    else :
        LOAD_DIR = "x_noise"+str(noise_per*5)+"per.npy"
        x = np.load(LOAD_DIR)
        # ----- 処理 -----

となります. もしこれを, x_noise0per.npyとすれば,

max_noise_per = 10

for noise_per in range(max_noise_per):
    LOAD_DIR = "x_noise"+str(noise_per*5)+"per.npy"
    x = np.load(LOAD_DIR)
    # ----- 処理 -----

で済みます. もちろんそれによる弊害や, 処理を同等に扱えない場合もあるでしょうが, もし支障ないのであれば_noise0perを足してあげた方が無駄なif分岐をなくすことができます.

関数

何をしているか一目で分かるようにする

例えば, Nullかどうかチェックしたい時に,

def check():

では何をチェックするのか一目では分かりません.

def isNull():

とすれば, 対象がNullか否か確認する関数だということが一目で分かります.
このあたりに関しては,
プログラミングでよく使う英単語のまとめ【随時更新】
うまくメソッド名を付けるための参考情報
が非常に参考になると思います.

1つの関数・メソッドに機能を詰め込み過ぎない

UNIXの哲学には次のようなものがある.
参考 : UNIX哲学

これがUNIXの哲学である。
一つのことを行い、またそれをうまくやるプログラムを書け。
協調して動くプログラムを書け。
標準入出力(テキスト・ストリーム)を扱うプログラムを書け。標準入出力は普遍的インターフェースなのだ。
— ダグラス・マキルロイ、UNIXの四半世紀

関数名やメソッドの命名の際に長すぎると感じたら, それはもしかすると機能を詰め込みすぎている可能性があります.
"Small is beautiful"というもので, 小さな機能を繋げていって大きなことを実現するようにするのが良いでしょう.

[追記]@shiracamus 様から, 「1関数/メソッドは25行以内。1行は79文字以下。」という助言をいただきました. PEP8の基準です.

複雑な処理やトリッキーな処理は関数化する

main分の中で, 高速化したいがためにトリッキーな処理や頑張って行列計算に落とし込んで処理することがあると思います.
その処理を延々とmain文の中で書かれてしまうとウンザリしてくるので, いっそそういった高速化のために可読性の低い処理は関数化してしまうのが良いでしょう. 実際, numpyなども可読性捨てて高速化を実現しています.

if

elseには例外処理を書く

例えばxには何らかの制限で1から3までの整数しか持たないとします. xの値によって処理を書く場合に,

if x==1 :   
    # ----- 処理1 -----
elif x==2 : 
    # ----- 処理2 -----
else:       
    # ----- 処理3 -----

としても基本的には動きます. ですが, もしどこかで不具合やバグがあってxに4などの想定されない数字が入った場合困る可能性があります. xに4が入った時点でバグなのに, ここではxが3の場合の処理をしてしまい, どこでバグしているのか, あるいはバグになっていることすら気づかないかもしれません.

if x==1 :   
    # ----- 処理1 -----
elif x==2 : 
    # ----- 処理2 -----
elif x==3:  
    # ----- 処理3 -----
else:       
    # ----- 例外発生 -----

とした方が確実です. 可能ならば, try, exceptした方が良いでしょう.

1文なら改行しない

これは好き好きです. 自分はif文の処理が1行(で軽い処理)なら改行しません.

if 条件:
         # ----- 処理 -----

自分は

if 条件:     # ----- 処理 -----

とします. 1行程度の軽い処理なら複数行使わない方が軽い処理というイメージができるからです.

Class

メソッドでprivateとpublicを区別する

クラスのメソッドで, そのclass内でしか使わないprivateなメソッドには_(アンダースコア)から始まる関数名を, そうでないpublicなメソッドは普通の関数名を付けましょう.

class family_data:
     def __init__(self,...):
        # ----- Initialize -----

     def print_info(self, ...):
        # ----- 処理 -----
        # self._birthdayで生年月日を計算

     def _birthday(self, ...):
        # ----- 処理 -----
        # 生年月日を計算する, print_infoでしか呼び出さない

import

import * は避ける

名前空間を省略しすぎるとよくわからないことになります. 例えば, "open"という関数にはPIL.Image.openと組み込みのopenがあります. これをfrom PIL.Image import *とするとどちらがどちらなのかわからなくなってしまいます.

デバッグ

オプションをつける

例えば, 完成品としてはprintデバッグをしないが, 完成するまではprintデバッグをしながら書いていきたいとします.
debugというbool値を格納する変数を用意して,

if debug:
    # ----- debug処理 ------

とすれば実現できます.
コマンドラインの取得には,

argc = len(sys.argv)
option = sys.argv[1:]

debug = ("-debug" in option)

func(debug)

のようにすればできます. 他にもグラフの表示オプションに-show, ログの出力に-saveなども考えられるでしょう.

ログを出力する

printするだけではターミナルや端末の履歴に残しきれないこともあるでしょう. その場合, .txt形式でログを出力しておくと良いでしょう.

LOG_FILE_NAME = "....txt"
with open(LOG_FILE_NAME, 'a') as f:
    f.write("# logの中身")

docstring

docstringはpep257に準拠して書く

func.__doc__とすることで見ることができるdocstringを記述することも良いでしょう.

参考 : numpydoc docstring guide

自分は上記の"NumPy docstring conventions"を参考に書いています.

def length(x):
    """
    Return the length of array_like.

    Parameters
    ----------
    x : array_like

    Returns
    -------
    length : int

    """
    if not x:
        return 0
    else:
        return 1 + length(x[1:])