Help us understand the problem. What is going on with this article?

【Python】Pythonicなコードの書き方🐍

Pythonでコードを書くときのGood/Badプラクティス

こちらの記事は、DuomlyによりDev.to上で公開された『 Good and Bad Practices of Coding in Python 』の邦訳版です(原著者から許可を得た上での公開です)

元記事:Good and Bad Practices of Coding in Python

(以下、翻訳した本文)


Cover image for 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の特長です。アンパックを使用することで、複数の値を複数の変数に割り当てることが可能です。

良い例
>>> a, b = 2, 'my-string'
>>> a
2
>>> b
'my-string'

この動作を利用して、コンピュータープログラミングの世界全体でおそらく最も簡潔でエレガントな変数スワップを実装することができます。

良い例
>>> a, b = b, a
>>> a
'my-string'
>>> b
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はそれを容易にするためにいくつかの組み込みクラスを提供しています。

ほとんどすべての場合、範囲を使用して整数を生成するイテレータを取得できます。

悪い例
>>> 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つ以上のシーケンスを反復処理したい場合はどうでしょうか。もちろん、範囲をここでも使うことができます。

悪い例
>>> 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的なのは、ブール値のコンテキストで0Falseとして解釈される一方、他の全ての数字はTrueとして見なされるという事実を利用するやり方です。

0以外の数字は全て真
>>> bool(0)
False
>>> bool(-1), bool(1), bool(20), bool(28.4)
(True, True, True, True)

これを念頭に置いて、if item ! = 0の代わりにただif itemを使えば良いのです。(注意点に関して訳注あり1

良い例
>>> 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

さらに短いバージョンは次のとおりです。(※注意点に関して訳注あり2

さらに良い例
>>> 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には本当のプライベートなクラスメンバーはありません。ただし、インスタンスの外でアンダースコア(_)で始まるメンバーにアクセスしたり変更したりしてはならないという規約があります。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ができます。(※訳注あり3

.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
>>>

これは避けてください。クラスの作者は、おそらく名前をアンダースコアで始めて、「使用するな」と伝えています。

コンテキストマネージャーを使用してリソースを解放する

リソースを適切に管理するためのコードを記述する必要がある場合があります。これは、ファイル、データベース接続、または管理されていないリソースを持つ他のエンティティを操作する場合によく見られます。

たとえば、ファイルを開いて次のように処理することができます。

悪い例
>>> 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コンテキストマネージャーで対応できます。2番目の方法は、コードを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で役立つ美しいコードを書くことを楽しみましょう!


  1. 一概にif itemといった書き方が良いとは言えない。整数以外の値(例えばNoneや空文字など)が入る可能性がある場合はif item ! = 0の方が良い書き方といえる時もあることに注意。詳しくはコメントでのやり取りを参照。 

  2. この書き方だとユーザが引数seqに空のリストを与えた場合もTrue判定されてしまうため、ユーザーの意図しない結果が生じる可能性がある。詳しくはコメントを参照。 

  3. _のついていないメンバー変数はJavaなどの他の言語でのアクセスレベルpublicに、_が着いたメンバー変数はprotectedに__が着いたメンバー変数はprivateに相当すると言えるが、記事に書かれている通り___で修飾されていてもアクセスしようとすればできてしまう。また、Pythonのクラスでは基本的にパブリックな属性が好まれる(クラス外部からアクセスする属性には___はつけない方が良いという意味)ため、属性に追加の処理を加えたいときは従来のゲッターとセッターの使用を避けるで言及されている@property@x.setterをパブリックな属性に対して使用することが好ましいとされる。 

_masa_u
都内のスタートアップで働くプログラマーです。 仕事で学んだことを中心に備忘録として記事を書いています。 それ以外に海外の技術ブログの記事を邦訳したり、@baby-degu のメンバーの一員として記事選定や翻訳などのお手伝いをしています。
https://www.wantedly.com/users/42256816
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away