Python♪コーディングスタイルに悩む。例えば長い数式は?

コーディング規約にはPEP 8があります。しかし、PEP 8は全ての細かい書式を網羅したものではないので、いざコーディングを始めると迷うことも多いです。そこで、私がコーディングスタイルで悩んだことをメモとして残したいと思います。

なお、私の過去の記事もコードスタイルが乱れているので、修正しなければならないと思っています。

1.pep8とは

pep8とはPythonのコードのスタイルガイドであり、Pythonでは最も一般的なものです。内容は以下のリンク先を参考にして下さい。

pep8-ja

2.pep8を確認しよう

それでは、pep8の中で数式に関係する内容をピックアップして確認してみましょう。

(1) チュートリアル

Pythonのチュートリアルでは、pep8の数式に関係する記述としては下記のようにあっさりと紹介されているだけです。

・ソースコードの幅が 79 文字を越えないように行を折り返すこと。

・演算子の前後とコンマの後には空白を入れ、括弧類のすぐ内側には空白を入れないこと: a = f(1, 2) + g(3, 4)。

Pythonチュートリアル:4.8. 間奏曲: コーディングスタイル

(2) 優先順位が一番低い演算子の両側のみスペースを入れる

pep8では以下のようにスタイルが示されており、「優先順位が一番低い演算子の両側にスペースを入れる」とされています。

優先順位が違う演算子を扱う場合、優先順位が一番低い演算子の両側にスペースを入れることを考えてみましょう。入れるかどうかはあなたの判断にお任せしますが、二つ以上のスペースを絶対に使わないでください。そして、2項演算子の両側には、常に同じ数の空白文字を入れてください。

https://pep8-ja.readthedocs.io/ja/latest/
正しい:
i = i + 1
submitted += 1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
間違い:
i=i+1
sumitted +=1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)

ただ、pep8に従って数学の解の公式を記述するとコード2-2-1のようになりますが、読みやすいかと言われると何か微妙な気がします。

#コード2-2-1
#正しい
xp = (-b+math.sqrt(b**2-4*a*c)) / (2*a)

例えばコード2-2-2のように、それぞれの括弧内で優先順位が低い演算子にスペースを入れた方が、長い数式では読みやすいようにも思えます。

#コード2-2-2
#間違い
xp = (-b + math.sqrt(b**2 - 4*a*c)) / (2 * a)

しかし、色々な書式を試した結果、やはりpep8のコーディングスタイルが読みやすいという結論になりました。式が長ければ長いほど「両側に空白がある演算子は、優先順位が一番低い演算子である」という一貫したルールがある方が読みやすいです。

(3) 複数行にわたる数式について

pep8において演算子は2項演算子の前後どちらで改行してもよいとしながらも、演算子の前で改行することを推奨しています。

# 正しい:
# 演算子とオペランドを一致させやすい
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)
# 間違い:
# 演算子がオペランドと離れてしまっている
income = (gross_wages +
          taxable - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

演算子が前にある方が、前の式の続きであることが分かりやすいです。「算術演算子と/」と「コマンドが複数行に渡ることを示すバックスラッシュ」を混同することもありません。

なお、上記の式は全体が括弧でくくられているので、4行目以降は3行目の括弧の後から始まっていますが、括弧がなければ、コード2-3-1のようにイコールの後の開始位置はそろえます。また、行の最後に「\」があることにも注意しましょう。

#コード2-3-1
income = gross_wages \
         + taxable_interest \
         + (dividends - qualified_dividends) \
         - ira_deduction \
         - student_loan_interest

(4) キーワード引数

なお、間違いやすいコーディングスタイルとして、キーワード引数の前後にはスペースは入れません。

アノテーションされていない 関数の引数におけるキーワード引数や、デフォルトパラメータを示す = の両側にスペースを入れてはいけません:

https://pep8-ja.readthedocs.io/ja/latest/
正しい:
def munge(sep: AnyStr = None): …
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): …
間違い:
def munge(input: AnyStr=None): …
def munge(input: AnyStr, limit = 1000): …

(5) 引数の途中での改行

同様に「https://pep8-ja.readthedocs.io/ja/latest/」からの引用ですが、「,」は改行後の先頭ではなく、改行前の最後に配置します。

また、開き括弧にそろえない場合は、はじめの行には引数を付けないのもポイントです。

1レベルインデントするごとに、スペースを4つ使いましょう。

行を継続する場合は、折り返された要素を縦に揃えるようにすべきです。括弧やブラケットおよび波括弧で囲まれた要素については、Python が暗黙のうちに行を結合することを利用して揃えます。そうでない場合は、手でインデントさせることで揃えます。突き出しインデント [7] を使う場合は、次のことを考慮すべきです: はじめの行には引数を付けずに次の行以降をインデントし、継続行だとはっきりわかるようにしましょう。:

https://pep8-ja.readthedocs.io/ja/latest/
# 正しい:

# 開き括弧に揃える
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# 引数とそれ以外を区別するため、スペースを4つ(インデントをさらに)加える
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# 突き出しインデントはインデントのレベルを深くする
foo = long_function_name(
    var_one, var_two,
    var_three, var_four)

3.こんな時はどうする?

さて、ここからは私なりのルールについてメモを残したいと思います。特殊な書式は使わないようにするつもりですが、一般的なルールではない可能性もありますので注意して下さい。また、私用のメモなので内容が変更される可能性があります。

(1) 「'」と「"」

ドキュメンテーション文字列は「"」×3、文字列を囲む記号にも「"」を使う。

#コード3-1
def hello():
    """helloの表示

    「"」と「'」は、どちらが使いやすい?
    """
    print("hello")

Pythonでは、ドキュメンテーション文字列では「"」×3、文字列を囲む記号は「'」が使われていることが多いですが、他のプログラム言語では文字列を示す記号に「"」が使われることが多く、平行して複数の言語を扱うときに頭が切り替わらず混乱します。コードの移植も容易ですし、「"」なら左手だけで入力できるので、コーディングのスピードもアップします。

また、Djangoなどで、htmlやJavaScriptなど他の言語と混在するときも記号を揃えられるので楽です。

ただ、一般的な書き方と異なるのが気になります。やっぱり、 ドキュメンテーション文字列は「"」×3、文字列を囲む記号は「'」が無難かな?

正直、どちらがよいか悩み中です。

(2) ルートや累乗

ルートは「math.sqrt()」を使う。その他の累乗は「**」を使う。

#コード3-2
x = 3
y = math.sqrt(x) + x**3 + x**2
print(y)

以下の記事では様々なルート計算の速度が検証されています。非常に興味深い内容です。

Pythonの平方根どれが早いか

1番速いのは「math.sqrt()」、2番目に速いのは「**」です。したがって、numpyをimportしているような場合でも「numpy.sqrt()」ではなく、「math.sqrt()」を使った方がよさそうです。NumPyが必ず速い訳ではないんですね。

また、「**」も十分に速いようですのでルート以外の累乗は「**」を使うことにしました。

(3) マイナスの値

式の一番最初の値がマイナスの場合、マイナスの符号の直後にスペースは設けない。

また、例えばコード3-3-1の11行目のように括弧の前のマイナスがある場合、括弧の前のマイナスを優先順位にカウントせず、括弧内の(x - 2)の「-」の前後にスペースを入れます。

#コード3-3-1
#採用
y = -x
z = -x - 2
income = (-gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

w = -(x - 2)
#コード3-3-2
#不採用
y = - x
z = - x - 2
income = (- gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

w = - (x - 2)

「-」のあとに空白がなければ先頭行、「-」のあとに空白があれば数式の2行目以降であることが明確になります。

(4) 優先順位が一番低い演算子が「**」

累乗の「**」の前後にスペースを設けない例も見られますが、優先順位が一番低い演算子が「**」の場合「**」の前後にスペースを設けます。

#コード3-4
y = x ** 2
y = (x**2+3) ** 2

(5) 「math.sqrt()」の中の演算子のスペース

「math.sqrt()」しかないような数式では「math.sqrt()」の括弧の中で一番優先順位が低い演算子の両側にスペースを設ける。

#コード3-5-1
z = math.sqrt(x**2+y) + 5
z = math.sqrt(x**2 + y)
z = -math.sqrt(x**2 + y)

つまり、普通の括弧と同じように扱うということです。

#コード3-5-2
z = (x**2+y) + 5
z = (x**2 + y)
z = -(x**2 + y)

max()のように括弧の中に2つ以上の式や値を記述するものもありますが、「math.sqrt()」と同様に、括弧の中で一番優先順位が低い演算子の両側にスペースを設けます。

#コード3-5-3
z = max(x**2+y, 3) + 5
z = max(x**2 + y, 3)
z = -max(x**2 + y, 3)

listやrange()なども、同じように「普通の括弧と同じように扱う」という考え方で記述すれば、コード3-5-4のようになります。

#コード3-5-4
y = x[i + 1]
y = x[i] + x[i+1]
for i in range(1, 2*n + 1):

(6) 複数行にわたる数式

数式が複数行に分かれる場合は行末を「半角スペース + バックスラッシュ(円マーク)」とします。また、改行後の先頭位置を数式のイコールの後でそろえます。

例えばコード3-6-1では、2行目(数式の1行目)の「12」と3行目の先頭の「*」の位置がそろいます。

#コード3-6-1
w = 12 / (2*math.pi) * (self.x**2-(self.y-self.z)**5) \
    * (self.z**3-(self.y-self.x)**4)

なお、括弧で囲まれた範囲を途中で改行する場合は「半角スペース + \」は不要です。また、改行後の先頭は1行目の片括弧の直後から始めます。

例えばコード3-6-2では2行目の「(」よりも、次の行先頭の「+」の方が半角スペース分、後ろに記述されます。

#コード3-6-2
w = ((self.x-5*self.y)*(self.z**4-(self.z-3*self.y)**2)
     +2*self.y*self.z**7) / 10

以下の例では、括弧の途中で改行された2~3行目の後ろは「半角スペース + \」が不要ですが、4行目の後には「半角スペース + \」が必要です。

#コード3-6-3
w = ((self.x-self.x_left-4*self.y)
     *(self.z**2-(self.y-2*self.z)**5)
     +8*self.z*(self.x-2*self.y)**3)/15 \
    - (2*self.y*self.y_left**7)/10

コード3-6-4も括弧で囲まれた範囲の途中で改行する例です。改行後は片括弧の直後から始めます。

#コード3-6-4
self.w = 5 * (self.x*(self.y-self.y_left-2*self.z)
              +math.pi*self.x*(4*self.y-self.x)/14)

コード3-6-5は2つ目の括弧の途中で改行した例です。

#コード3-6-5
self.w = 5 * (self.x*(self.y-self.y_left
                      -2*self.z)+math.pi*self.x*(4*self.y-self.x)/14)

これは、私独自の方法ではなく、Pythonの統合開発環境のSpyderで改行してもコード3-6-6のように自動でインデントが設けられます。いちいち、自分仕様のインデントに書き直すようでは大変ですから、基本的に統合開発環境の自動インデントと同じ方法を採用する方が楽です。

#コード3-6-6
a = 1 + (2*(1+(4*3*3)+2+4)+9+10)

a = 1 + (2*(1+(4*3
               *3)+2
            +4)+9
         +10)

a = 1 + (2*(1
            +(4*3*3)
            +2+4)
         +9+10)

a = 1 + 2*1 + 4*3*3 + 2 \
    + 4 + 9 + 10

このようにコードスタイルから改行の状況を読み取ることができます。

(7) イコールの前で改行したい

代入先の変数が長い場合には、イコールの部分で改行したくなります。この場合はイコールの前で改行(半角×4のインデント)し、その次の行からはイコールの位置を基準とします。

#コード3-7-1
self.cal_lim_g[dim][case] \
    = self.structure.steel("end", self.material, self.E, self.I, self.i) \
      + 7.85

4.あとがき

コーディングスタイルをそろえることで、可読性、作業効率を向上させ、入力ミスも減らすことができます。慣れない間は面倒なだけですが、できるだけ早いタイミングでコーディングスタイルに目を向けた方がよいと思います。

また、個性的なコーディングスタイルではなく、できるだけpep8に準拠させる方がよいと思います。

この記事は、今後も加筆・修正を行い内容を充実させたいと思います。

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー
UdemyのPythonの動画講座を書籍を買う感覚で購入してみた

その他

Twitterへのリンクです。SNSもはじめました♪

液晶ペンタブレットを買いました
 (1) モバイルディスプレイを買うつもりだったのに激安ペンタブレット購入

以下、私が光回線を導入した時の記事一覧です。
 (1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
 (2) NURO光の開通までWiFiルーターを格安レンタルできる
 (3) NURO光の屋外工事の状況をご紹介。その日に開通!
 (4) 光回線開通!実測するとNURO光はやっぱり速かった
 (5) ネット上のNURO光紹介特典は個人情報がもれないの?