Pythonでコードを書くときのGood/Badプラクティス
こちらの記事は、DuomlyによりDev.to上で公開された『 Good and Bad Practices of Coding in Python 』の邦訳版です(原著者から許可を得た上での公開です)
元記事:Good and Bad Practices of Coding in Python
※ 記事の内容に注意すべき点と誤りがあるので、詳しくは注釈まで目を通すことをおすすめします。
(以下、翻訳した本文)
この記事は元々 https://www.blog.duomly.com/good-and-bad-practices-of-coding-in-python/ に公開されたものです。
Pythonは可読性を重視した高水準のマルチパラダイムプログラミング言語です。Pythonは、「Pythonの禅」、別名ではPEP 20と呼ばれるルールに従って開発、保守され、幅広く使用されている言語です。
この記事では、頻繁に会う可能性が高いPythonでのコーディングの良い例と悪い例をいくつか示します。
アンパック(unpacking)を使用して簡潔にコードを記述する
パック(packing)とアンパック(unpacking)は強力なPythonの特長です。アンパックを使用することで、複数の値を複数の変数に割り当てることが可能です1。
>>> a, b = 2, 'my-string'
>>> a
2
>>> b
'my-string'
この動作を利用して、コンピュータープログラミングの世界全体でおそらく最も簡潔でエレガントな変数スワップを実装することができます。
>>> a, b = b, a
>>> a
'my-string'
>>> bf
2
アンパックは、より複雑な場合の複数の変数への割り当てに使うことができます。たとえば、次のように変数へ値を割り当てることはできます。
>>> x = (1, 2, 4, 8, 16)
>>> a = x[0]
>>> b = x[1]
>>> c = x[2]
>>> d = x[3]
>>> e = x[4]
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
しかし、代わりに、より簡潔で間違いなく読みやすいアプローチを使うことができます。
>>> a, b, c, d, e = x
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
イケてますよね?でも、これはさらにイケてます。
>>> a, *y, e = x
>>> a, e, y
(1, 16, [2, 4, 8])
ポイントは、*
付きの変数が他に割り当てられていない値をまとめているという点です。
チェーンを使用して簡潔にコードを記述する
Pythonでは、比較演算をチェーンさせることができます。したがって、2つ以上の比較演算がTrue
であるかどうかを使用して確認する必要はありません。
>>> x = 4
>>> x >= 2 and x <= 8
True
代わりに、数学者のように、これをよりコンパクトな形式で書くことができます。
>>> 2 <= x <= 8
True
>>> 2 <= x <= 3
False
Pythonは連鎖割り当てもサポートしています。したがって、複数の変数に同じ値を割り当てる場合は、簡単に行うことができます。
>>> x = 2
>>> y = 2
>>> z = 2
よりエレガントな方法は、アンパックを使用することです。
>>> x, y, z = 2, 2, 2
ただし、連鎖割り当てを使用すると、状況はさらに改善されます。
>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)
値がミュータブルな型の場合は注意してください。すべての変数は同じインスタンスを参照します。
None
のチェック
None
はPythonでは特別でユニークなオブジェクトです。Cライクな言語でのnull
と同じような目的があります。
変数がNone
を参照しているかは比較演算子の==
および!=
で確認することができます。
>>> x, y = 2, None
>>> x == None
False
>>> y == None
True
>>> x != None
True
>>> y != None
False
しかし、よりPython的で望ましいのはis
およびis not
を使うやり方です。
>>> x is None
False
>>> y is None
True
>>> x is not None
True
>>> y is not None
False
さらに、より可読性の低い代替手段のnot (x is None)
よりも、is not構文であるx is not None
を使用することをお勧めします。
シーケンスと連想配列の繰り返し
Pythonでは、いくつかのやり方で繰り返しとforループを実装できます。Pythonはそれを容易にするためにいくつかの組み込みクラスを提供しています。
ほとんどすべての場合、range
を使用して整数を生成するイテレータを取得できます。
>>> x = [1, 2, 4, 8, 16]
>>> for i in range(len(x)):
... print(x[i])
...
1
2
4
8
16
ただし、シーケンスを繰り返すより良い方法があります。
>>> for item in x:
... print(item)
...
1
2
4
8
16
しかし、逆の順序で繰り返しをしたい場合はどうでしょうか?もちろん、範囲をまた使うことができます。
>>> for i in range(len(x) - 1, -1, -1):
... print(x[i])
...
16
8
4
2
1
シーケンスを逆にする方がよりエレガントなやり方です。
>>> for item in x[::-1]:
... print(item)
...
16
8
4
2
1
この場合、Python的なやり方は、reversed
を使用して、シーケンスのアイテムを逆の順序で生成するイテレータを取得することです。
>>> for item in reversed(x):
... print(item)
...
16
8
4
2
1
シーケンスの要素と対応するインデックスの両方が必要になる場合があります。
>>> for i in range(len(x)):
... print(i, x[i])
...
0 1
1 2
2 4
3 8
4 16
enumerate
を使用して、インデックスとアイテムを含むタプルを生成する別のイテレーターを取得するやり方の方が良いとされています。
>>> for i, item in enumerate(x):
... print(i, item)
...
0 1
1 2
2 4
3 8
4 16
イケてます。しかし、2つ以上のシーケンスを反復処理したい場合はどうでしょうか。もちろん、range
をここでも使うことができます。
>>> y = 'abcde'
>>> for i in range(len(x)):
... print(x[i], y[i])
...
1 a
2 b
4 c
8 d
16 e
この場合、またPythonはより良いソリューションを提供しています。zip
を適用して、対となる要素のタプルを取得できます。
>>> for item in zip(x, y):
... print(item)
...
(1, 'a')
(2, 'b')
(4, 'c')
(8, 'd')
(16, 'e')
アンパックと組み合わせることができます。
>>> for x_item, y_item in zip(x, y):
... print(x_item, y_item)
...
1 a
2 b
4 c
8 d
16 e
範囲は非常に役に立つものであることを覚えておいてください。ただし、(上記のような)より便利な代替手段がある場合もあります。
辞書を反復処理すると、キーが生成されます。
>>> z = {'a': 0, 'b': 1}
>>> for k in z:
... print(k, z[k])
...
a 0
b 1
ただし、メソッド.items()
を適用して、キーと対応する値を持つタプルを取得できます。
>>> for k, v in z.items():
... print(k, v)
...
a 0
b 1
また、メソッド.keys()
を使うことでキーを、.values()
を使うことで値を反復処理することもできます。
0
との比較
数値データがあり、数値がゼロに等しいかどうかを確認する必要がある場合は、比較演算子==
および!=
を使用できますが、そうする必要はありません。
>>> x = (1, 2, 0, 3, 0, 4)
>>> for item in x:
... if item != 0:
... print(item)
...
1
2
3
4
Python的なのは、ブール値のコンテキストで0
がFalse
として解釈される一方、他の全ての数字はTrue
として見なされるという事実を利用するやり方です。
>>> bool(0)
False
>>> bool(-1), bool(1), bool(20), bool(28.4)
(True, True, True, True)
これを念頭に置いて、if item != 0
の代わりにただif item
を使えば良いのです。(注意点に関して訳注あり2)
>>> for item in x:
... if item:
... print(item)
...
1
2
3
4
同じロジックに従い、if item == 0
の代わりにif not item
を使用できます。
ミュータブルなオプション引数(デフォルト引数)を避ける
Pythonには、関数とメソッドに引数を提供するための非常に柔軟なシステムがあります。オプション引数はこのシステムの一部です。ただし、注意が必要です。通常、ミュータブルなオプション引数を使用しない方が賢明です。次の例について考えてみます。
>>> def f(value, seq=[]):
... seq.append(value)
... return seq
seq
を指定しない場合、f()
は空のリストに値を追加し、[value]
のようなものを返します。これは一見すると、うまくいくように見えます。
>>> f(value=2)
[2]
問題なさそうですね?そんなことはありません!次の例を検討してみましょう。
>>> f(value=4)
[2, 4]
>>> f(value=8)
[2, 4, 8]
>>> f(value=16)
[2, 4, 8, 16]
驚いたでしょうか?混乱していますか?もしそうなら、あなただけではありません。
オプション引数(この場合はリスト)の同じインスタンスが、関数が呼び出されるたびに使われているようです。時には上のコードがしていることと全く同じことをしたい場合があるかもしれません。しかし、それを回避する必要がある場合の方がはるかに多いことでしょう。いくつかの追加ロジックを使うと、これを避けることができます。方法のうちの1つは次です。
>>> def f(value, seq=None):
... if seq is None:
... seq = []
... seq.append(value)
... return seq
さらに短いバージョンは次のとおりです。(※注意点に関して訳注あり3)
>>> def f(value, seq=None):
... if not seq:
... seq = []
... seq.append(value)
... return seq
ようやく、異なる動作が得られます。
>>> f(value=2)
[2]
>>> f(value=4)
[4]
>>> f(value=8)
[8]
>>> f(value=16)
[16]
ほとんどの場合、これが欲しい結果です。
慣例的なゲッターとセッターの使用を避ける
Pythonでは、C++やJavaと同様にゲッターメソッドとセッターメソッドを定義できます。
>>> class C:
... def get_x(self):
... return self.__x
... def set_x(self, value):
... self.__x = value
次が、ゲッターとセッターを使用してオブジェクトの状態を取得および設定する方法です。
>>> c = C()
>>> c.set_x(2)
>>> c.get_x()
2
場合によっては、これがやりたいことを実現するための最良の方法です。ただし、特に単純なケースでは、プロパティを定義して使用する方が洗練されていることがよくあります。
>>> class C:
... @property
... def x(self):
... return self.__x
... @x.setter
... def x(self, value):
... self.__x = value
プロパティは、従来のゲッターやセッターよりもPython的と考えられています。C#と同様に、つまり通常のデータ属性と同じように使用できます。
>>> c = C()
>>> c.x = 2
>>> c.x
2
したがって、一般的には、可能な場合はプロパティを使用し、どうしても必要な場合はC++ライクなゲッターとセッターを使用することがグッドプラクティスとされています。(※訳注:記事ではこのように書かれているがこの点は誤りと考えられる。Pythonでは一般的にはにはパブリックなアトリビュートを使い、必要な場合はプロパティやゲッター、セッターを使用することが慣例的に好まれている。4を参照)
保護されたクラスメンバーへのアクセスを避ける
Pythonには本当のプライベートなクラスメンバーはありません。ただし、インスタンスの外でアンダースコア(_)で始まるメンバーにアクセスしたり変更したりしてはならないという規約があります。Pythonのプライベートなクラスメンバーは既存の動作を保持していることが保証されていません。
たとえば、次のコードを考えます。
>>> class C:
... def __init__(self, *args):
... self.x, self._y, self.__z = args
...
>>> c = C(1, 2, 4)
クラスCのインスタンスには、.x
、._y
、._C__z
の3つのデータメンバーが存在します。メンバーの名前が2つのアンダースコアで始まる場合は、難号化(mangled)され、変更されます。そのため、.__z
の代わりに._C__z
ができます。
.x
には直接アクセスまたは変更しても問題ありません。
>>> c.x # OK
1
インスタンスの外部から._y
にアクセスまたは変更することもできますが、これはバッドプラクティスと見なされています。
>>> c._y # 可能だが悪い
2
.__z
にアクセスすることはできません。zは難号化されているからです。しかし、._C__z
にアクセスまたは変更することはできます。
>>> c.__z # エラー!
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'C' object has no attribute '__z'
>>> c._C__z # 可能だが、1個前の例よりさらに悪い!
4
>>>
これは避けてください。クラスの作者は、おそらく名前をアンダースコアで始めて、「使用するな」と伝えています。(※訳注あり5)
コンテキストマネージャーを使用してリソースを解放する
リソースを適切に管理するためのコードを記述する必要がある場合があります。これは、ファイル、データベース接続、または管理されていないリソースを持つ他のエンティティを操作する場合によく見られます。
たとえば、ファイルを開いて次のように処理することができます。
>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file`
メモリを適切に管理するには、ジョブ終了後にこのファイルを閉じる必要があります。
>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file and`
>>> my_file.close()
ファイルを閉じることは、閉じないよりもマシです。しかし、ファイルの処理中に例外が発生した場合はどうでしょうか?その後、my_file.close()
は決して実行されません。
この場合、例外処理構文またはwith
コンテキストマネージャーで対応できます。後者の方法は、コードをwith
ブロック内に配置することを意味します。
>>> with open('filename.csv', 'w') as my_file:
... # do something with `my_file`
with
ブロックを使用することで、特殊メソッドの.__enter__()
と.__exit__()
が例外が発生した場合でも呼び出されます。これらのメソッドがリソースの面倒を見てくれるでしょう。コンテキストマネージャーと例外処理を組み合わせることで、特に堅牢な構成を実現できます。
コードスタイルに関してのアドバイス
Pythonコードは、エレガントで簡潔で読みやすいものにする必要があります。それは美しいはずです。
美しいPythonコードの書き方に関する究極のリソースは、「Style Guide for Python Code」、またの名を「PEP 8」です。Pythonでコーディングする場合は、必ず読むべきです。
結論
この記事では、より効率的で読みやすく、より簡潔なコードを書く方法についていくつかのアドバイスを提供しています。つまり、Python的(Pythonic)なコードの記述方法を示しています。さらに、PEP 8はPythonコードのスタイルガイドを提供し、PEP 20はPython言語の原則を示しています。
Pythonicで役立つ美しいコードを書くことを楽しみましょう!
-
他言語(RubyやJavaScript、C#など)にある分割代入と同じ機能です。 ↩
-
一概に
if item
といった書き方が良いとは言えない。整数以外の値(例えばNoneや空文字など)が入る可能性がある場合はif item != 0
というように明示的に書いた方が良い書き方といえる時もあることに注意。詳しくはコメントでのやり取りを参照。 ↩ -
この書き方だとユーザが引数
seq
に空のリストを与えた場合もTrue
判定されてしまうため、ユーザーの意図しない結果が生じる可能性がある。詳しくはコメントを参照。 ↩ -
Pythonコミュニティ的にはPythonでは基本的にパブリックなアトリビュート(メンバ変数)を使うことが好まれている(クラス外部からアクセスする属性にはできる限り
_
や__
はつけない方が良いという意味)。単純に値を取得、セットしたい場合はプロパティは使用しない方がPythonicだと考えられていることに注意。それでも独自の処理を加えるなどprotected(_
)属性、private(__
)属性のアトリビュート(メンバ変数)を使いたい理由がある場合は従来のゲッターとセッターの使用を避けるで言及されている@property
(ゲッター)と@x.setter
(セッター)、もしくはC++、Java式のゲッター・セッターメソッドを作成・使用することが推奨されている。 ↩ -
_
のついていないアトリビュート(メンバ変数・関数)はJavaなどの他の言語でのアクセスレベルpublic
に、_
で修飾されたアトリビュートはprotected
に、__
で修飾されたメンバー変数がアクセスレベルprivate
に相当すると言えるが、記事に書かれている通り_
や__
で修飾されていてもアクセスしようとすればできてしまう。アトリビュートの変更もできてしまうため、これが「Pythonのプライベートなクラスメンバーは既存の動作を保持していることが保証されていません」という意味。 ↩