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の特長です。アンパックを使用することで、複数の値を複数の変数に割り当てることが可能です。
>>>a,b=2,'my-string'>>>a2>>>b'my-string'
この動作を利用して、コンピュータープログラミングの世界全体でおそらく最も簡潔でエレガントな変数スワップを実装することができます。
>>>a,b=b,a>>>a'my-string'>>>b2
アンパックは、より複雑な場合の複数の変数への割り当てに使うことができます。たとえば、次のように変数へ値を割り当てることはできます。
>>>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>=2andx<=8True
代わりに、数学者のように、これをよりコンパクトな形式で書くことができます。
>>>2<=x<=8True>>>2<=x<=3False
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==NoneFalse>>>y==NoneTrue>>>x!=NoneTrue>>>y!=NoneFalse
しかし、よりPython的で望ましいのはis
およびis not
を使うやり方です。
>>>xisNoneFalse>>>yisNoneTrue>>>xisnotNoneTrue>>>yisnotNoneFalse
さらに、より可読性の低い代替手段のnot (x is None)
よりも、is not構文であるx is not None
を使用することをお勧めします。
シーケンスと連想配列の繰り返し
Pythonでは、いくつかのやり方で繰り返しとforループを実装できます。Pythonはそれを容易にするためにいくつかの組み込みクラスを提供しています。
ほとんどすべての場合、範囲を使用して整数を生成するイテレータを取得できます。
>>>x=[1,2,4,8,16]>>>foriinrange(len(x)):...print(x[i])...124816
ただし、シーケンスを繰り返すより良い方法があります。
>>>foriteminx:...print(item)...124816
しかし、逆の順序で繰り返しをしたい場合はどうでしょうか?もちろん、範囲をまた使うことができます。
>>>foriinrange(len(x)-1,-1,-1):...print(x[i])...168421
シーケンスを逆にする方がよりエレガントなやり方です。
>>>foriteminx[::-1]:...print(item)...168421
この場合、Python的なやり方は、reversed
を使用して、シーケンスのアイテムを逆の順序で生成するイテレーターを取得することです。
>>>foriteminreversed(x):...print(item)...168421
シーケンスの要素と対応するインデックスの両方が必要になる場合があります。
>>>foriinrange(len(x)):...print(i,x[i])...01122438416
enumerate
を使用して、インデックスとアイテムを含むタプルを生成する別のイテレーターを取得するやり方の方が良いとされています。
>>>fori,iteminenumerate(x):...print(i,item)...01122438416
イケてます。しかし、2つ以上のシーケンスを反復処理したい場合はどうでしょうか。もちろん、範囲をここでも使うことができます。
>>>y='abcde'>>>foriinrange(len(x)):...print(x[i],y[i])...1a2b4c8d16e
この場合、またPythonはより良いソリューションを提供しています。zip
を適用して、対となる要素のタプルを取得できます。
>>>foriteminzip(x,y):...print(item)...(1,'a')(2,'b')(4,'c')(8,'d')(16,'e')
アンパックと組み合わせることができます。
>>>forx_item,y_iteminzip(x,y):...print(x_item,y_item)...1a2b4c8d16e
範囲は非常に役に立つものであることを覚えておいてください。ただし、(上記のような)より便利な代替手段がある場合もあります。
辞書を反復処理すると、キーが生成されます。
>>>z={'a':0,'b':1}>>>forkinz:...print(k,z[k])...a0b1
ただし、メソッド.items()
を適用して、キーと対応する値を持つタプルを取得できます。
>>>fork,vinz.items():...print(k,v)...a0b1
また、メソッド.keys()
を使うことでキーを、.values()
を使うことで値を反復処理することもできます。
0
との比較
数値データがあり、数値がゼロに等しいかどうかを確認する必要がある場合は、比較演算子==
および!=
を使用できますが、そうする必要はありません。
>>>x=(1,2,0,3,0,4)>>>foriteminx:...ifitem!=0:...print(item)...1234
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
を使えば良いのです。(注意点に関して訳注あり1)
>>>foriteminx:...ifitem:...print(item)...1234
同じロジックに従い、if item == 0
の代わりにif not item
を使用できます。
ミュータブルなオプション引数を避ける
Pythonには、関数とメソッドに引数を提供するための非常に柔軟なシステムがあります。オプション引数はこのシステムの一部です。ただし、注意が必要です。通常、ミュータブルなオプション引数を使用しない方が賢明です。次の例について考えてみます。
>>>deff(value,seq=[]):...seq.append(value)...returnseq
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つは次です。
>>>deff(value,seq=None):...ifseqisNone:...seq=[]...seq.append(value)...returnseq
さらに短いバージョンは次のとおりです。(※注意点に関して訳注あり2)
>>>deff(value,seq=None):...ifnotseq:...seq=[]...seq.append(value)...returnseq
ようやく、異なる動作が得られます。
>>>f(value=2)[2]>>>f(value=4)[4]>>>f(value=8)[8]>>>f(value=16)[16]
ほとんどの場合、これが欲しい結果です。
従来のゲッターとセッターの使用を避ける
Pythonでは、C++やJavaと同様にゲッターメソッドとセッターメソッドを定義できます。
>>>classC:...defget_x(self):...returnself.__x...defset_x(self,value):...self.__x=value
次が、ゲッターとセッターを使用してオブジェクトの状態を取得および設定する方法です。
>>>c=C()>>>c.set_x(2)>>>c.get_x()2
場合によっては、これがやりたいことを実現するための最良の方法です。ただし、特に単純なケースでは、プロパティを定義して使用する方が洗練されていることがよくあります。
>>>classC:...@property...defx(self):...returnself.__x...@x.setter...defx(self,value):...self.__x=value
プロパティは、従来のゲッターやセッターよりもPython的と考えられています。C#と同様に、つまり通常のデータ属性と同じように使用できます。
>>>c=C()>>>c.x=2>>>c.x2
したがって、一般的には、可能な場合はプロパティを使用し、どうしても必要な場合はC++ライクなゲッターとセッターを使用することがグッドプラクティスとされています。
保護されたクラスメンバーへのアクセスを避ける
Pythonには本当のプライベートなクラスメンバーはありません。ただし、インスタンスの外でアンダースコア(_)で始まるメンバーにアクセスしたり変更したりしてはならないという規約があります。Pythonのプライベートなクラスメンバーは既存の動作を保持していることが保証されていません。
たとえば、次のコードを考えます。
>>>classC:...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(mostrecentcalllast):File"",line1,inAttributeError:'C'objecthasnoattribute'__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
ブロック内に配置することを意味します。
>>>withopen('filename.csv','w')asmy_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で役立つ美しいコードを書くことを楽しみましょう!
一概に
if item
といった書き方が良いとは言えない。整数以外の値(例えばNoneや空文字など)が入る可能性がある場合はif item ! = 0
の方が良い書き方といえる時もあることに注意。詳しくはコメントでのやり取りを参照。 ↩この書き方だとユーザが引数
seq
に空のリストを与えた場合もTrue
判定されてしまうため、ユーザーの意図しない結果が生じる可能性がある。詳しくはコメントを参照。 ↩_
のついていないメンバー変数はJavaなどの他の言語でのアクセスレベルpublic
に、_
が着いたメンバー変数はprotectedに
、__
が着いたメンバー変数はprivate
に相当すると言えるが、記事に書かれている通り_
や__
で修飾されていてもアクセスしようとすればできてしまう。また、Pythonのクラスでは基本的にパブリックな属性が好まれる(クラス外部からアクセスする属性には_
や__
はつけない方が良いという意味)ため、属性に追加の処理を加えたいときは従来のゲッターとセッターの使用を避けるで言及されている@property
と@x.setter
をパブリックな属性に対して使用することが好ましいとされる。 ↩