📘

Pyゆき「なんだろう、独自ルールでコーディングするのやめてもらっていいですか」

2022/10/12に公開

「なんだろう、独自ルールでコーディングするのやめてもらっていいですか」

本記事ではPythonでのコーディング規約(PEP8)についてまとめていきます。
プログラミングはコードの効率性ももちろん大事ですが、改修などを考えていくと可読性もとても重要なものとなっていきます。コードを書いた日は当然のように覚えていますが、1ヶ月後、半年後、1年後はなかなか覚えていないものです。
またそのときにクソコードやなぁと思うのも少なくないかと思います。
そういうのをできるだけ減らすためにプログラミング言語にはコードのスタイルを一貫させるためにコーディング規約が設けられています。

ただし、以下のような場合は、必ずしもコーディング規約に従う必要はありません。

  • プロジェクトごとに、すでに独自の規約がある場合はそちらを優先してください。
  • PEP8に準拠を重視するあまり、後方互換性がなくなるようなことは避けてください。
  • PEP8に準拠するとコードが読みづらくなる個所が発生した場合は、読みやすさを優先してください。

インデント

ブロックのインデントは、スペースを4つ使いましょう。
括弧やブラケットおよび波括弧で囲まれた要素については、Pythonが暗黙のうちに行を結合します。
上記以外は手動でインデントしましょう。

# OK

# 開き括弧に揃える
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)
# NG

# 折り返された要素を縦に揃えない場合、1行目の引数は禁止
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# インデントが区別できないので、2行目以降でさらにインデントが必要
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

複数行を継続したときにインデントする場合は、4つスペースを使うルールを守らなくても良い。

# OK

# 突き出しインデントの場合は、インデントにスペースを4つ使わなくてもよい
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

if文の条件部分が、複数行にまたがって書かなければならないくらい十分に長い場合があります。
この場合は、以下のどちらかで統一してください。

# OK

# 追加のインデントをしない
if (this_is_one_thing and
    that_is_another_thing):
    do_something()

# 継続された行の条件をインデントする
if (this_is_one_thing
        and that_is_another_thing):
    do_something()

行を継続して波括弧/ブラケット/括弧を閉じる時は、リストの最後の要素が置かれた行の、はじめの文字の直下に閉じる記号を置いても構いません。

# OK

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

もしくは、閉じる記号を継続された行のはじめの文字に合わせて置いても良いです。
ちなみに私はこっち派。

# OK

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

タブか、スペースか?

インデントにはスペースを使いましょう。
Pythonでは、インデントにタブとスペースを混ぜることを禁止しています。

1行の長さ

すべての行の長さを、最大79文字までに制限しましょう。
docstringやコメントのように構造に関する制約が少ないテキストのブロックについては、1行72文字までに制限すべきです。

二項演算子の前で改行すべきか、後で改行すべきか

昔は二項演算子の後で改行するスタイルが推奨されていましたが、現在は二項演算子の前で改行する方が推奨されています。

# OK

# 演算子とオペランドを一致させやすい
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)
# NG

# 演算子がオペランドと離れてしまっている
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

空行

トップレベルの関数やクラスは、2行ずつ空けて定義するようにしてください。
クラス内部では、1行ずつ空けてメソッドを定義してください。

import

import文は、通常は行を分けて記述してください。

# OK

import os
import sys
# NG

import os, sys

相対importを使用するときはOKです。

# OK

from subprocess import Popen, PIPE

import文は常にファイルの先頭、つまりモジュールコメントやdocstringの直後、そしてモジュールのグローバル変数や定数定義の前に置くようにします。
また、import文は次の順番でグループ化し、各グループの間には1行の空白行をおいてください。

  • 標準ライブラリ
  • サードパーティに関連するもの
  • 独自に作成したローカルにあるアプリケーション/ライブラリに特有のもの

モジュールレベルの二重アンダースコア変数名

__all____author____version__のような、モジュールレベルの「二重アンダースコア変数」は、モジュールに関するdocstringの後、そしてfrom __future__以外のあらゆるimport文の前に置いてください。
Pythonはfuture importを、docstring以外のあらゆるコードの前に置くように強制します。

# OK

"""This is the example module.

This module does stuff.
"""

from __future__ import barry_as_FLUFL

__all__ = ['a', 'b', 'c']
__version__ = '0.1'
__author__ = 'Cardinal Biggles'

import os
import sys

文字列に含まれる引用符

Pythonでは、単一引用符で囲まれた文字列と、二重引用符で囲まれた文字列は同じです。
PEP8では、どちらを推奨するかの立場は示しません。
どちらを使うかのルールをプロジェクトで決めて、統一してください。
単一引用符や二重引用符が文字列に含まれていた場合は、文字列中でバックスラッシュを使うことを避けるため、もう一方の引用符を使うようにしましょう。

イライラの元になる空白文字

余計な空白文字を使うのはやめましょう。
括弧やブラケット、波括弧 のはじめの直後と、終わりの直前には不要です。

# OK

spam(ham[1], {eggs: 2})
# NG

spam( ham[ 1 ], { eggs: 2 } )

末尾のカンマと、その後に続く閉じカッコの間にも不要です。

# OK

foo = (0,)
# NG

bar = (0, )

カンマやセミコロン、コロンの直前にも不要です。

# OK

if x == 4: print(x, y); x, y = y, x
# NG

if x == 4 : print(x , y) ; x , y = y , x

関数呼び出しの引数リストをはじめる開き括弧の直前にも不要です。

# OK

spam(1)
# NG

spam (1)

インデックスやスライスの開き括弧の直前にも不要です。

# OK

dct['key'] = lst[index]
# NG

dct ['key'] = lst [index]

代入演算子を揃えるために、演算子の周囲に1つ以上のスペースを入れなくて良いです。

# OK

x = 1
y = 2
long_variable = 3
# NG

x             = 1
y             = 2
long_variable = 3

スライスではコロンは二項演算子のように振る舞います。
よって、スライスでは両側に同じ数のスペースを置くべきです。
拡張スライスでは、両側に同じ数のスペースを置かなければなりません。
ただし、スライスのパラメータが省略された場合は、スペースも省略されます。

# OK

ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]
# NG

ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower : : upper]
ham[ : upper]

関数アノテーションは、コロンに関する通常のルールを守るようにしつつ、->演算子がある場合、その両側には常にスペースを入れるようにしましょう。

# OK

def munge(input: AnyStr): ...
def munge() -> PosInt: ...
# NG

def munge(input:AnyStr): ...
def munge()->PosInt: ...

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

# OK

def complex(real, imag=0.0):
    return magic(r=real, i=imag)
# NG

def complex(real, imag = 0.0):
    return magic(r = real, i = imag)

デフォルト値をもった引数アノテーションと組み合わせる場合、=の前後にはスペースを入れるようにしてください。

# OK

def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...
# NG

def munge(input: AnyStr=None): ...
def munge(input: AnyStr, limit = 1000): ...

複合文 (一行に複数の文を入れること) は一般的に推奨されません。

# OK

if foo == 'blah':
    do_blah_thing()
do_one()
do_two()
do_three()
# NG

if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()

if/for/whileと短い文を同じ行に置くことがOKな場合もありますが、複合文を置くのはやめてください。また、複合文でできた長い行を折り返すのもやめましょう。

# NG

if foo == 'blah': do_blah_thing()
for x in lst: total += x
while t < 10: t = delay()

if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()

try: something()
finally: cleanup()

do_one(); do_two(); do_three(long, argument,
                             list, like, this)

if foo == 'blah': one(); two(); three()

末尾にカンマを付けるべき場合

末尾にカンマを付けるかどうかは、通常は任意です。
ただし、要素数が1のタプルを作るときは例外的に必須です。
要素数が1のタプルを作るときは、括弧で囲むことを推奨します。

# OK

FILES = ('setup.cfg',)
# NG

FILES = 'setup.cfg',

末尾のカンマは冗長ですが、便利な場合もあります。
たとえば値や引数、もしくはimportされた値のリストが繰り返し展開されることが期待される場合や、バージョン管理システムを使っている場合です。
それぞれの値一行にひとつずつ置き、末尾にカンマをひとつ追加し、その次の行を閉じ括弧/角括弧/中括弧で閉じるというのがひとつのパターンです。
しかし、区切りの終わりを示す目的で、同じ行の末尾にカンマを付けることは意味がありません。

# OK

FILES = [
    'setup.cfg',
    'tox.ini',
    ]
initialize(FILES,
           error=True,
           )
# NG

FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)

コメント

コードと矛盾するコメントは、コメントしないことよりタチが悪いです。
コードを変更した時は、コメントを最新にしてください。

ドキュメンテーション文字列

良いドキュメンテーション文字列(docstrings)を書くための規約は、PEP257にまとめられています。
複数行のdocstring"""だけからなる行で閉じます。

"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.
"""

docstringが1行で終わる場合は、同じ行を"""で閉じるようにしてください:

"""Return an ex-parrot."""

関数や変数の名前

関数の名前は小文字のみにすべきです。
また、読みやすくするために、必要に応じて単語をアンダースコアで区切ります。
要はスネークケースを採用します。

# OK

animal_name = "dog"

def fetch_config():
    pass
# NG

animalName = "dog"

def fetchConfig():
    pass

関数やメソッドに渡す引数

インスタンスメソッドのはじめの引数の名前は常にselfを使ってください。
クラスメソッドのはじめの引数の名前は常にclsを使ってください。

メソッド名とインスタンス変数

名前は小文字のみにして、読みやすくするために必要に応じて単語をアンダースコアで区切ります。
公開されていないメソッドやインスタンス変数にだけ、アンダースコアを先頭に付けてください。
サブクラスと名前が衝突した場合は、Python のマングリング機構を呼び出すためにアンダースコアを先頭に2つ付けてください。

# OK

class Sample:

    a = 0
    b = 0

    def sum(self):
        return self.a + self.b
	
    def _calc(self):
        return self.a * self.b

    def set(self, a, b):
        self.a = a
        self.b = b

定数

定数は通常モジュールレベルで定義します。
すべての定数は大文字で書き、単語をアンダースコアで区切ります。

# OK

MAX_OVERFLOW = 1000
TOTAL = 10

プログラミングに関する推奨事項

Noneのようなシングルトンと比較をする場合は、常にisis notを使うべきです。
等値演算子を使わないでください。
また、not ... is ...ではなく、is not演算子を使いましょう。

# OK

if foo is not None:
# NG

if not foo is None:

リソースがコードの特定の部分だけで使われる場合、 使った後すぐ信頼できるやり方で後始末ができるように with文を使いましょう。try/finally文でも問題ありませんがコード量が減ります。

# OK

with conn.begin_transaction():
    do_stuff_in_transaction(conn)

return文は一貫した書き方をしましょう。
関数の中のすべてのreturn文は式を返すか、全く何も返さないかのどちらかにすべきです。
式を返しているreturn文が関数の中にある場合、値を何も返さないreturn文は明示的にreturn None と書くべきです、到達可能であればreturn`文を関数の最後に明示的に置くべきです。

# OK

def foo(x):
    if x >= 0:
        return math.sqrt(x)
    else:
        return None

def bar(x):
    if x < 0:
        return None
    return math.sqrt(x)
# NG

def foo(x):
    if x >= 0:
        return math.sqrt(x)

def bar(x):
    if x < 0:
        return
    return math.sqrt(x)

文字列に特定のプレフィックスやサフィックスがついているかをチェックするには、文字列のスライシングではなく startswith()endswith()を使いましょう。

# OK

if foo.startswith('bar'):
# NG

if foo[:3] == 'bar':

オブジェクトの型の比較は、型を直接比較するかわりに、常にisinstance()を使うようにしてください。

# OK

if isinstance(obj, int):
# NG

if type(obj) is type(1):

シーケンス(文字列, リスト, タプル)については、空のシーケンスがFalseであることを利用しましょう。

# OK

if not seq:
if seq:
# NG

if len(seq):
if not len(seq):

Bool型の値とTrueFalseを比較するのに==を使うのはやめましょう。

# OK

if greeting:
# NG

if greeting == True:
if greeting is True:

try...finallyの組み合わせの中で、finallyの外に脱出する制御構文return/break/continueを使うのは推奨されません。
このような構文はfinallyの中から伝播する例外を暗黙のうちにキャンセルしてしまうためです。

# NG

def foo():
    try:
        1 / 0
    finally:
        return 42

Discussion