PEP8という、Python自体の標準ライブラリなどに対するコーディング規約に関して、過去誤って理解していたことが何度かありました。(少し理解が大変なところもあって、しっかり読み込まないと、実はその書き方NGだった、という経験が・・)
ただ、17年前に書かれたある種古典的なドキュメントで、今でも大事にされているということを考慮すると、とても良いことが書かれているというのも事実なのかなと思います。(Pythonを使用したプロジェクトでPEP8を加味したり、Jupyterの拡張機能含めPEP8を考慮したライブラリなどが多く用意されていることなども考えると)
そこで、本記事では個人的な解釈をしつつ、PEP8について読み解いてまとめてみます。(元の記事は著作権がパブリックドメインなので、使いつつまとめさせていただきます)
※PEP8すべては触れません。
はじめに考慮すべきこと
- プロジェクトごとに、すでに独自の規約がある場合はそちらを優先します。
- PEP8準拠を重視するあまり、後方互換性がなくなるようなことは避けるべき(後方互換性を優先すべき)です。
- PEP8に準拠するとコードが読みづらくなる個所が発生した場合は、準拠よりも読みやすさを優先すべきです。
- 古いコードなどで、PEP8(もしくはプロジェクト独自のルール)に準拠していない場合には、既存のコードの書き方に合わせた方がいい場合もあります。ただし、モジュール単位などで、少しずつ直したりできそうであれば、直していくのもいいと思います。
一番重要なのは、特定のモジュールや関数の中で一貫性を保つことです。
インデント
- インデントは半角スペース4つで統一します。
- タブではなくスペースを使用します。
- リストや関数呼び出しなどで、長い場合に途中で改行を入れる際には、括弧の開始部分と要素がぴったり合うようにします。
# この例だと、var_oneとvar_threeの開始位置を揃えます。
foo = long_function_name(var_one, var_two,
var_three, var_four)
# リストなども同様。
# この例だと、100000000と600000000の位置を揃えます。
sample_list = [100000000, 200000000, 300000000, 400000000, 500000000,
600000000, 700000000]
- 括弧の直後に改行するのであれば、インデント1つ分ずらします。
# この例でいえば、var_oneの前に1インデント入れます。
foo = long_function_name(
var_one, var_two, var_three, var_four)
sample_list = [
100000000, 200000000, 300000000, 400000000, 500000000, 600000000,
700000000, 800000000]
- 関数定義時で、引数が多く改行を入れる場合には、2つインデントを入れる必要があります。(私はPython触り始めたころ、知らずに1つのインデントで書いていました・・)
- インデントが1つだと、引数部分とコード内容でインデントがそろってしまい区別が付きにくいためです。
- ※ただし、docstringを書くことも多いと思うので、それで区別が付くという印象もあります。
# 引数部分で、2つインデントを加えています。
def long_function_name(
var_one, var_two, var_three,
var_four):
print(var_one)
- if文で長くなる場合には、括弧を加えて、且つ改行後はインデントを2つ加えるなどの対応を行います。(前述と同様、後に続く処理とインデントの位置が被って区別が付きにくいためです)
# 2行目以降の条件部分でインデントを2つ加えています。
if (var_one == 100
and var_two == 200
and var_three == 300):
do_something()
1行の長さ
- コード部分は半角で79文字以内に制限します。
- ただし、エラーメッセージなどの長い文字列など、無理やり79文字以内に収めるために分割する、といったことをすると煩雑になるケースもあると思うので、そういった個所は無理に準拠しなくても、という印象があります。
- コメントやdocstringなど、改行が入れやすいものに関しては半角72文字以内に制限します。
- Jupyterであれば、拡張機能のRulerを使ったり、他のエディタでも大体79文字あたりを可視化してくれる機能があるとおもうので利用します。
- 通常はシンタックスエラーで改行が入れれない場合でも、バックスラッシュを行末に入れることで、改行が許容されるので、必要な場合には利用します。
with open('/path/to/some/file/you/want/to/read') as file_1, \
open('/path/to/some/file/being/written', 'w') as file_2:
file_2.write(file_1.read())
プラスやマイナスなどの演算子の位置
- 複数行にまたがる場合、先頭に持ってくると位置が揃ってみやすくなります。(数学などの記述に合わせる形に)
# 演算子を、行末ではなく先頭に。
income = (gross_wages
+ taxable_interest
+ (dividends - qualified_dividends)
- ira_deduction
- student_loan_interest)
- ただし、既にプロジェクトで行末などに統一されている形であれば、そちらに合わせるべきです。
空行の数
- インデントがない(=トップレベル)ところに書くクラスや関数に関しては2行分の空行を入れます。
# 要素ごとに2行空けます。
def sample_func_1():
pass
def sample_func_2():
pass
class SampleClass():
pass
- クラス内部だったり(=インデントが1つ入っている領域)に追加する関数などは1行ずつ空行を加えます。
class SampleClass():
def __init__(self):
pass
def sample_func_1(self):
pass
- トップレベルの複数の定数や変数間で2行空けるのかは特に記載がなく、なんとも言えません。定数などが多いと大分行がかさんでしまうのと、Djangoなどのライブラリの定数記述用のモジュールでも1行空けだったり2行空けだったりまちまちです。(個人的には定数などは1行空けで対応することが多いです)
- また、yapfでも、定数間などで空行が追加されたりはしないので、定数や変数などは空行入れてもいれなくてもOK、という印象です。
- このあたりは、手動が面倒であればJupyterであればCode prettifyなどの拡張機能、通常の.pyファイルであればyapfなどを使うと楽ができます。(必要に応じてCIに組み込んだりなど)
importの書き方
- 基本的にimportは行を分けて書きます。
- PEP8には特に理由は記載されていませんが、importエラーになった際に、行が分かれている方が問題のモジュールを特定しやすい、などのメリットは感じています。
# import os, sys といったように、1行にまとめるべきではありません。
import os
import sys
- 同一モジュール内での複数のimportであれば問題はありません。(分けると、記述が煩雑になったりしてしまう)
# from datetime import datetime
# from datetime import date
# といったように書かずに、以下のようにまとめるのはOK。
from datetime import datetime, date
- importは基本的に、(必要な場合は)エンコーディングの記述とモジュール自体のdocstringの直下(モジュールの先頭の方)に記載すべきです。
- ただし、相互にモジュール間でimportをする場合はトップレベルにimportを書くとエラーになる(例 : Pythonで循環インポートするとどうなるのか)ケースや、NumPy(pandasだったかな・・?)の内部のコードを読んでいて、最初にすべてimportすると時間がかかるので、必要になった際にimportするために関数内でimportしている、という記述も見受けられました。利便性などに合わせて、必要であれば調整していいと思われます。
- Pythonの標準ライブラリ(最初から入っているもの)、サードパーティーのライブラリ、自身のプロジェクトのモジュールでグループ化し、それぞれに1行ずつ空けます。
# osなどの標準ライブラリでグループ化、NumPyなどのサードパーティーで
# グループ化、最後に自身のプロジェクトのモジュールでグループ化します。
import os
import shutil
import sys
import numpy as np
import pandas as pd
from apps.your_app import your_module
- 手動で扱うのが手間な場合は、Jupyterであればisort formatterなどの拡張機能、.pyファイルであればisortライブラリなどを利用すると楽ができます。
- 基本的に(webなどのプロジェクトであれば)誤読を避けたりリネーム・パス移動などで影響を少なくするため、絶対パスを指定してimportします。
- ワイルドカードを使ったimportは避けます。(from os import * といったような)
- 何が読み込まれいるのかが分かりづらいのと、複数のモジュール間で同じ名称のものがあった場合に片方が上書きされてしまい、事故の元になるためです。
クオーテーション
- PEP8では、文字列の引用符はシングルクォーテーションでもダブルクォーテーションでも、どちらでもOKとされています。ただし、プロジェクトでどちらにするか決めて統一します。
- ダブルクォーテーションで統一している中、文字列内にJSONを含んだりなど、バックスラッシュを入れないといけない場合などには、記述をシンプルにするため統一から外れてそこだけシングルクォーテーションを使う、といったことはOKです。
- docstringなどの複数行の文字列の場合には、ダブルクォーテーションを使います。
スペースの入れ方
- 余分なスペースは入れないようにします。
# 以下好ましくない例。
# hamの前や1の前後、eggsの前、2の後など、不要なスペースは
# 入れるべきではありません。
spam( ham[ 1 ], { eggs: 2 } )
# tuple定義のためのコンマの後にも、不要なスペースは入れるべきでは
# ありません。
bar = (0, )
# 関数と括弧でスペースを入れるべきではありません。
sample_func (1)
# 辞書やリストなども、変数名と括弧の間にスペースを
# 入れるべきではありません。
dct ['key'] = lst [index]
- スライスは少し条件が複雑目です。(今見ても、あれ、この書き方しているけどアウトなんじゃ・・というものが)
- スペースを入れない形で統一するのはOKです。
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
- 変数などを指定する場合でも、スペースを入れない形での統一はOKです。
ham[lower:upper], ham[lower:upper:], ham[lower::step]
- 括弧内の途中でコロンを書く場合、コロンの両側にスペースを入れます。演算子などを入れる場合は、スペースを入れても入れなくてもどちらでも良いようです。
ham[lower+offset : upper+offset]
ham[lower + offset : upper + offset]
- 括弧内の途中ではなく、端にコロンを書く場合は括弧との間にスペースを入れずに記載します。
# この例では、コロンの左の括弧との間にはスペースを入れていません。
ham[: upper_fn(x) : step_fn(x)]
ham[:: step_fn(x)]
- イコールの位置を合わせるために、スペースを入れたりはしないようにします。
- ※メリット・デメリットあり、他の言語やプロジェクトなどでは推奨されていたりもしますが、郷に入っては郷に従えということで、Pythonで書く際には避ける形でいいかなと思います。
- 理由を少し調べてみました。参考 : 変数や配列とかの縦位置を揃えないほうがいい?
# 以下のように、イコールの位置を揃えるためにスペースを入れるのは避けます。
x = 1
y = 2
long_variable = 3
- キーワード引数の指定や、引数のデフォルト値の記述の際に、イコールの前後にはスペースを入れないようにします。
# imagの後のデフォルト値のイコールの前後には
# スペースを入れません。
def complex(real, imag=0.0):
# キーワード引数を指定する際にも、イコールの
# 前後にはスペースを入れません。
return magic(r=real, i=imag)
- ただし型アノテーションとデフォルト値の両方を指定する場合にはスペースを追加します。
# 型アノテーションとデフォルト値を併用する場合にはイコールの両端に
# スペースを入れます(float = 0.0といったように)。
def complex(real, imag: float = 0.0):
...
- 1行につき、複数の処理を書くのは避けます。
# ifの条件文と処理を同一行に書いたりは避けます。
if foo == 'blah': do_blah_thing()
# コンマで区切って、複数の処理を1行にまとめるのも避けます。
# 改行して分割しましょう。
do_one(); do_two(); do_three()
末尾のコンマ
- リストや辞書などで、1行に1つずつ要素を記述する場合などに、末尾にコンマを入れるのは問題ありません。(要素追加時や、バージョン管理的に扱いやすくなります)
- たしか、Test-Driven Development with Pythonの本でも、ミスを減らすためにリストなどで末尾にコンマを入れるのが推奨されていたような気がします。
# tox.iniの後にも、コンマを入れるのは問題ありません。
# setup.cfgの後に行を追加する場合でも、tox.iniの後に行を追加する場合でも、
# コンマの有無を気にすることなく、毎回コンマを書く、という形で対応できます。
FILES = [
'setup.cfg',
'tox.ini',
]
命名規則
関数名や変数名
- 小文字のみで、単語間をアンダースコアで区切ります。(スネークケース : sample_func)
クラス名
- 先頭を大文字で、パスカルケースで記載します。(SampleClass)
定数名
- すべて大文字で、単語をアンダースコアで区切ります。(SAMPLE_CONST)
モジュール名
- すべて小文字で表現します。読みやすくなるのであればアンダースコアを使用しても問題ありません。(samplemodule, sample_module)
パッケージ名(フォルダ名)
- すべて小文字で表現します。アンダースコアの使用は推奨されません。(samplepackage)
- ※アンダースコア非推奨というのは結構見落としがちで、仕事だと引き継いだ時点でアンダースコアが使われていたので、合わせる形で普通に使用しています。このあたりは、stackoverflowでも、アンダースコア使っても別にだれも責めないよ、といったことが言われているので、まあいいか・・という印象。
But honestly, I don't think anyone will blame you if you use underscores and your code will run with either decision.
python: naming a module that has a two-word name
公開されていない変数・関数名
- モジュールやクラスが持つprivate的な要素(厳密にはアクセスできるのでprivateではない)の先頭にアンダースコアを記載します。
- 基本的に、各モジュールで先頭にアンダースコアが付いている要素はアクセスすべきではありません。公開されている要素と異なり、バージョンによっては互換性のない更新がされる可能性があります。
- ※仕事では、自身で書いたモジュールに関してはテスト用のモジュールのみに限ってアクセスしています。
- ※一括のimportを指定した場合にも、import対象にならないなど、若干の挙動も変わります。
予約語などと変数名が被る場合
- 他の変数名などが思いつかない場合(もしくは無理に省略などして読みづらくなる場合など)には、末尾にアンダースコアを付けて被らないようにします。(id_, list_など)
- ※サードパーティのライブラリなどによっては別のルールで使われています。例 : scikit-learnであれば、推定済みの値などに使われます。
trailing underscore (self.gamma_) in class attributes is a scikit-learn convention to denote "estimated" or "fitted" attributes.
Why do you use so many leading and trailing underscores in the code examples?
条件文の書き方
- Noneなどと比較する場合には、is None もしくは is not Noneといった具合に書きます。
if your_val is None:
if your_val is not None:
- 真偽値であれば、イコールなどは使わずに書きます。
if is_empty:
※Noneや真偽値に関して、stackoverflowを読んでいたところif not foo is None だったりif is_empty == True:、if is_empty is True:などと書くよりも、前述した記述の方が英文的に自然だよね、ということが書かれていました。
-
リスト、タプルなどであれば、空の場合にはそのままFalseになるので、空かどうかはlen関数などは挟まずに判定します。
- 仕事でlen関数使っている時があるので気を付けます・・
文字列のサフィックス、プレフィックスなどを調べるには、スライスではなくstartswith関数やendswithなどを使う方がシンプルでミスが起きづらいです。(スライスによる、うっかり1文字多かったり / 少なかったりといったミスを避けるなど)
-
型の比較はtype関数よりもisinstance関数を使うべきです。
- ※isinstance関数は、継承元のクラスを指定した場合にもTrueを返し、type関数はクラスが完全一致していないとTrueを返しません。
- ※なぜtype関数ではなくisinstanceを使うべきなのか、理由を少し調べていたら、型チェックが必要なケースであれば、ダックタイピング的に扱うのがいいよね、というのが理由なようです。(Python2.7環境の際などに)文字列かどうかをチェックしたいのであればstrとunicode両方チェックせずに、basestringだけチェックすればいいよね、といった具合です。
- 参考 : What are the differences between type() and isinstance()?
- 参考 : ダックタイピングって一体なんなのよ【golang】
- basestringという存在を初めて知った・・
- また、isinstanceの場合、第二引数でタプルを指定することで、複数のクラスに対してOR条件で調べたりといったこともできるようです。(typeで個別に条件を書かずに済みます)
例外
- 例外はクラスで定義し、クラス名の末尾はErrorとします。(AssertionError、ImportErrorといった具合に)
- 独自のクラスを作る場合には、Exceptionクラスを継承して作ります。
- except単体ではなく、エラーのクラスを指定します。except単体や、except BaseExceptionとすると、KeyboardInterruptなどもキャッチしてしまうため、Ctrl + Cなどでプログラムを止めるのが難しくなる場合があります。
- ここも仕事でexcept単体を使ったことがあるので、あとで直さないと・・
try:
import platform_specific_module
except ImportError:
platform_specific_module = None
- バグが埋もれたりしないように、tryで囲む範囲は最小限にしましょう。
おまけ
以下、PEP8に無関係ですが、他の書籍などで推奨されていた内容です。
一つのモジュールに集約するのは悪ではない
- たしか、Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理の本だったかで、Javaなどを書いてきた人には違和感があるかもしれませんが、一つのモジュールを短くするよりも、長くなってもいいので集約しておいた方がモジュール管理の面で好ましい、といったことが書かれていたような気がします。
- たしかに、たとえばNumPyを使うのに10も20もimportが必要といった感じだと結構つらみがあります。
- NumPyであれば、import numpy as np、Pandasであればimport pandas as pd、TensorFlowであればimport tensorflow as tfとすれは大体の機能が使えるといったように各ライブラリなっていますし、有名なライブラリのコードをみていても結構数万行とかになっています。
- この点も、郷に入っては郷に従え、という感じでしょうか。
関数などは、短い方がいいかも
- Pythonコミュニティでも意見が割れている?そうですが、テストの書きやすさなどの面で関数が短い方が好ましいから、(コミュニティの件にも触れつつも)関数はなるべく短くする方が好ましい、といったようなことがMastering matplotlibの本で言われていました。
- 個人的にも、.pyファイルの関数などは短い方が好きです。
デフォルト値を取る引数に関しては、キーワード引数を利用する
- 省略可能なデフォルト値を取る引数に関しては、キーワード引数を指定するようにすると、引数の順番などを気にせずに済みますし、デフォルト値を取る引数は順番が変わったりもしがちなので、可読性や堅牢な感じにするためにもプラスになる、といったことが確か(記憶が若干うろ覚えですが)Effective Pythonに書かれていたような気がします。
- 個人的には、エディタ側で入力補完などを対応しておけば、大した手間ではないので、デフォルト値を取らない引数に関しても自身で書いた関数に関しては結構書くことが多いです。(ビルドインモジュールの関数で、キーワード引数を受け付けてくれないものも一部ありますが)
- 例えば、仮に以下のように書籍などで特定ライブラリの知らない関数が出てきた際に、キーワード引数を使う場合と使わない場合だと、使った方が調べなくても内容が理解しやすいと感じています。
sample_func(100, 120)
sample_func(fruit_id=100, location_id=120)
他にもPythonなどを中心に色々記事を書いています。こちらもどうぞ!今までに投稿した主な記事たち