Python入門

【Python】Pythonicなプログラムコーディング

【Python】Pythonicなプログラムコーディング

Pythonには、Pythonらしいシンプルで読みやすいコードの書き方というものがあり、よく「Pythonic」であるといいます。Pythonicの考え方とPythonicなコーディングの例をいくつか紹介したいと思います。

Pythonicなプログラムコーディング

Pythonicとは

Pythonの学習をしていると「Pythonicなコーディング」というような言葉を聞いたことがあるのではないでしょうか。

Pythonicとは、プログラミングフィロソフィー、つまりはプログラミングの考え方について幅広い意味を持った言葉です。簡単に言うと「Pythonらしく、シンプルで読みやすいコーディングスタイル」のことです。

あるプログラムのコードに対して「Pythonicである」というときには「Pythonコードの書き方として自然で、シンプルかつ読みやすい」というようなことを意味します。Pythonicなコーディングと言っても、明確にこう書いたらPythonicだというようなものはないのかなと思っていますが、Pythonらしい書き方というのはあります。

私自身も自分のプログラムを見直すときの参考になるかなと思ったので、本記事では、Pythonicなコーディングについて考え方やコーディングの例をまとめてみようと思います。

The Zen of Python

Pythonicと関連して「The Zen of Python」というものがあるのでご紹介します。The Zen of Pythonでは、Pythonの「シンプルさ」や「読みやすさ」に関する考え方が19の言葉としてまとめられています。

The Zen of Pythonは、PEP(Python Enhancement Proposal)のPEP-20でまとめられています。以下に列挙してみます。

  1. Beautiful is better than ugly.
    「醜い」より「美しい」方がよい
  2. Explicit is better than implicit.
    「暗黙的」より「明示的」である方がよい
  3. Simple is better than complex.
    「複雑」より「シンプル」である方がよい
  4. Complex is better than complicated.
    「複雑」でも「難解」 であるよりはよい
    ※3とつながっている
  5. Flat is better than nested.
    「入れ子(ネスト)」よりも「平坦(フラット)」の方がよい
  6. Sparse is better than dense.
    「密」よりも「疎」である方がよい
  7. Readability counts.
    読みやすさは重要である
  8. Special cases aren’t special enough to break the rules.
    ルールを破るほどの特別なケースはない
  9. Although practicality beats purity.
    しかし「実用性」は「純粋さ」に勝る
    ※8とつながっている
  10. Errors should never pass silently.
    エラーが起こっているのに黙って見逃すべきではない
  11. Unless explicitly silenced.
    明示的にそのエラーは通知しなくてもよいと指定されない限り(10とつながっている)
  12. In the face of ambiguity, refuse the temptation to guess.
    「曖昧さ」に直面したら、「推測」で済ませたくなる誘惑に負けないで
  13. There should be one– and preferably only one –obvious way to do it.
    それをやるには、明白な方法な1つ、たった1つだけあるはず
  14. Although that way may not be obvious at first unless you’re Dutch.
    (あなたがオランダ人でない限り)その方法は、最初は分からないかもしれないけれど
    ※13とつながっている
    ※「オランダ人」というのはPythonの生みの親であるGuido van Rossumさんがオランダ人であるためとのことです
  15. Now is better than never.
    「やらない」よりも「今やる」
  16. Although never is often better than *right* now.
    「やらない」が「今すぐやる」よりいいことはよくあるけれど
    ※15とつながっている
  17. If the implementation is hard to explain, it’s a bad idea.
    説明が難しいのであれば、その実装はよくないということ
  18. If the implementation is easy to explain, it may be a good idea.
    説明が容易であるのであれば、その実装はよいのかもしれない
  19. Namespaces are one honking great idea — let’s do more of those!
    名前空間ってすごいアイデア

Pythonプログラミングだけに限らず、自分が何かの課題を解くときに考え直す際のよい指針になるなとすごく感じます。私もこれまでIT関連の仕事をしてきて思うのはプログラミングに限らず「シンプルでわかりやすい」っていうのは本当にとても難しいということだということです。

私も含めてですが多くの人は何かの問題を解くときになぜか複雑に考えてしまい、後で後悔するような保守性の悪いプログラムを作ってしまったりします。私も「The Zen of Python」については、折に触れて確認したいなと思います。

なお、Pythonコンソールで以下のように実行するとThe Zen of Pythonが表示されます。裏技的な隠し機能(イースターエッグ: Easter Egg)となっています。

import this
The Zen of Python, by Tim Peters 

Beautiful is better than ugly.   
Explicit is better than implicit.
Simple is better than complex.   
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

PEP8

Pythonicとは、少し違うのかもしれませんが、読みやすいコードという観点でPEP8についても紹介しておきます。PEP8とは、PEP(Python Enhancement Proposal)でまとめられているPythonのコーディング規約のことです。

以下が公式のドキュメントになります。pep8-jaの方は日本語ドキュメントです。

PEP8ではインデント、1行の長さ、演算子の位置、関数・変数・クラス名等の命名規則等々が記載されています。詳細は上記のドキュメントを見てみてください。

コーディング規約として紹介していますが、注意点としては、PEP8にこだわりすぎないということが重要です。実際にPEP8の冒頭に「A Foolish Consistency is the Hobgoblin of Little Minds (一貫性にこだわりすぎるのは、狭い心の現れである)」という項目が記載されており、PEP20の「“Readability counts” (読みやすさは重要である)」も引用されています。

プロジェクトで既にコーディング規約があるのであれば、無理にPEP8にあわせるのではなくプロジェクトのコーディング規約に従うべきですし、PEP8に従うことで読みやすさが失われるのであれば無理にPEP8にあわせる必要はありません。

とはいえ、PEP8がコードの品質を一定に保つためにも有用であることは事実です。もしコーディング規約などがプロジェクトで未定であれば、PEP8に従うとするのもいいでしょう。

文法上の問題を指摘してくれる静的解析ツールであるリンタ―(pep8, flake8, pylint)を使用したり、black等のフォーマットツールを使うのも便利ですので検討してみると良いかと思います。

Pythonicなコーディング例

以降では、Pythonicなプログラミングの例を紹介していきたいなと思います。プログラミングを見直す際の参考として使ってもらえるとよいかなと思います。

以下例がPythonicなコーディングの全てというわけではもちろんありません。また、良い例/悪い例という表現を使っていますが悪い例と書いている方でも動作はします。そして、必ずしも悪い例の方がダメというわけではなく場面によっては良い/悪いが異なる場合もあることは注意してください。

「シンプルさ」「読みやすさ」というのを意識して書くようにすることが重要です。

変数の操作

内包表記

Pythonicな書き方として特徴的なものの一つは内包表記かなと思います。以下の例で見てみましょう。内包表記とは、既存のリスト等のイテラブル(iterable)なオブジェクトから新しいオブジェクトを生成する際にシンプルに記載するための定義する方法のことを言います。

種類としては「リスト内包表記」「辞書内包表記」「集合内包表記」「ジェネレータ内包表記」といったものがあります。「内包表記まとめ」でまとめていますので興味があれば参考にしてください。

以下のリスト内包表記の例について見てみましょう。

# --- 良い例 ---
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
new_data = [i for i in data if i % 2 == 0]
print(new_data)
# --- 悪い例 ---
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
new_data = []

for i in data:
    if i % 2 == 0:
        new_data.append(i)
print(new_data)
【実行結果】
[2, 4, 6, 8, 10]

上記の例は、1~10までのリストから2で割り切れる数値のみで新しいリストを作成しています。良い例で記載しているリスト内包表記では、1行でシンプルに記載できていますが、悪い例の記載例では複数行で長くなってしまっています。

内包表記は、Pythonらしい書き方の代表的な例かなと思っていて、実際に内包表記を使用しない記載方法よりも内包表記を使用した方が処理速度が速いという特徴があります。そのため、内包表記は積極的に使用するように意識するとよいでしょう。

ただし、注意点として条件が複雑な場合等には必ずしも使うべきではないと言えます。チーム開発では他の人も読みやすいコード(可読性が高いコード)である方が適切です。複雑な場合は、悪い例で書いたようなfor文で書いた方が内容を理解しやすい場合があります。

内包表記を使うことでコードが複雑になってしまう恐れがある場合には、内包表記を使用するべきかはよく検討するようにしましょう。

アンパックの使用

アンパック(またはアンパック代入)とは、リストやタプルの勉強をしていると出てくるのですが、タプルの要素を分割して別々の変数に代入できることを言います。アンパックは色々と便利な使い方があるので、いくつかの例を紹介します。

アンパックについては「タプル(tuple)の基本と使いどころ」でも少し触れていますので興味があれば参考にしてください。

値を代入する

複数の変数に値を代入する場合には、以下の例のように”,”(カンマ)で要素を列挙するだけで代入ができます。

# --- 良い例 -----
a, b = 1, 2
print(a, b)
# --- 悪い例 -----
a = 1
b = 2
print(a, b)
【実行結果】
1 2

Pythonを勉強し始めたころに、上記の良い例のように列挙して値を代入できることを知りますが、実態としては=の右側でタプルが生成されて、それがアンパックされて各変数に代入されています。上記の例でいうと「1, 2」の部分は、「(1, 2)」と同じです。

上記の例のように少ない変数であれば、良い例のように1行でシンプルに書くのが読みやすいですが、変数が増えてきた場合には、悪い例として書いた順に列挙する方が読みやすい場合があります。その時の状況に応じて判断するようにしてください。

値を交換する

アンパックの便利な使用例として有名なのが値の交換です。アンパックを利用すると、変数の値の交換は以下のように簡単にできます。

# --- 良い例 ---
a, b = 1, 2
a, b = b, a
print(a, b)
# --- 悪い例 ---
a, b = 1, 2
tmp = a
a = b
b = tmp
print(a, b)
【実行結果】
2 1

一般に値の交換というと、悪い例で記載したように一度値を別の変数に退避して交換するような方法を考えるかと思います。

しかし、Pythonのアンパックを使用すると良い例に記載したように「a, b = b, a」とするだけで簡単に値の交換が可能です。

複数の変数に値を割り当てる

アンパックを使用すると以下の例のように複数の変数に簡単に割り当てることができます。

# --- 良い例 ---
data = [1, 2, 3, 4, 5]
a, b, c, d, e = data
print(a, b, c, d, e)
# --- 悪い例 ---
data = [1, 2, 3, 4, 5]
a = data[0]
b = data[1]
c = data[2]
d = data[3]
e = data[4]
print(a, b, c, d, e)
【実行結果】
1 2 3 4 5

上記の例では、a~eまでの変数に1行でdataの各要素を割り当てることができており、とてもシンプルに記載できます。

割り当て時に値をまとめる

複数の変数に値を割り当てる方法のもう一つの便利な方法としては、以下の例のように*(アスタリスク)を使って割り当て時に値をまとめる方法があります。

# --- 良い例 ---
data = [1, 2, 3, 4, 5]
a, *b, c = data
print(a, b, c)
# --- 悪い例 ---
data = [1, 2, 3, 4, 5]
a = data[0]
b = data[1:4]
c = data[4]
print(a, b, c)
【実行結果】
1 [2, 3, 4] 5

上記例では、*bとすることで、先頭と末尾を除くリストをまとめてbに取り込むことができています。

getter, setterではなくpropertyを使う

他のプログラミング言語等でクラスを勉強するとプライベートな変数にアクセスする方法としてgetterやsetterというメソッドを用意してアクセスするのが一般的かと思います。

Pythonのクラスでのクラス変数へのアクセスでは、プロパティ(property)というデコレータを使うことができるので、プロパティを使用するのが適切です。

プロパティについては「クラスのプロパティ(property)の使い方」にまとめているので興味があれば参考にしてください。

# --- 良い例 ---
class ClassGood:
    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value


c_good = ClassGood(1)
print(c_good.x)
c_good.x = 2
print(c_good.x)
# --- 悪い例 ---
class ClassBad:
    def __init__(self, x):
        self._x = x

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value


c_bad = ClassBad(1)
print(c_bad.get_x())
c_bad.set_x(2)
print(c_bad.get_x())
【実行結果】
1
2

上記の良い例のようにプロパティを用いる場合は、値を取得するためのgetterに相当するメソッドに@propertyデコレータをつけます。一方で、値を設定するためのsetterに相当するメソッドでは@<プロパティ名>.setterデコレータをつけて定義します。

Pythonでクラスを作成するときにはプロパティを使うようにするとよいでしょう。

なお、デコレータはメソッドを装飾して機能を付与する仕組みです。これにより各種メソッドがプロパティとして動作するようになります。デコレータの考え方や基本については「デコレータ(decorator)の基本的な使い方」でまとめていますので興味があれば参考にしてください。

比較・判定

条件分岐などで使う比較・判定についてもいくつかPythonらしい書き方というのがあります。いくつか紹介していきます。

比較演算子のチェーン

if文等で条件分岐をする際に複数条件で判定する場合には、and, or等を使うと思いますが、条件によっては、以下の良い例のようにつなげることが可能です。

# --- 良い例 ---
a = 5
print(2 <= a <= 10)
# --- 悪い例 ---
a = 5
print(2 <= a and a <= 10)
【実行結果】
True

上記の良い例では、aが2以上で10以下というのが直感的に把握しやすくなっていると思います。

オブジェクトの判定 (is, is notの使用)

値の判定をする場合には、「==」や「!=」を使うと思いますが、「is」, 「is not」という判定方法もあります。それぞれの違いは簡単に言うと以下の通りです。

  • 「==」「!=」:オブジェクトの値が同じかどうかを判定
  • 「is」「is not」:オブジェクトが同じかどうかを判定

つまり、同値性を判定する(==, !=)と、同一性を判定する(is, is not)という点で違いがあります。

特に、まだ値が設定されていない場合にNoneを使うことがよくあると思いますが、Noneの判定では、以下の良い例のようにisやis notを使用する方が適切です。

# --- 良い例 ---
a = None
print(a is None)
print(a is not None)
# --- 悪い例 ---
a = None
print(a == None)
print(a != None)
【実行結果】
True
False

「==」や「!=」の場合、変数のaが、代入や関数呼び出しもとでどんな値となっているかで挙動が不明確になるため、「is None」や「is not None」とした方が確実にaがNoneであるという事が明確にできます。

ループ処理

インデックス付きのループはenumerateを使用する

C言語などでforループをする場合には、for (i = 0; i < 10, i++)のような書き方でインデックスを増やしつつ処理することに慣れている人が多いかもしれません。

Pythonのfor文の場合で、インデックス付きでループをする場合には、以下のようにenumerateを使用します。enumerateについては「enumerateを用いたfor文の使い方」に詳細をまとめています。

# --- 良い例 ---
data = [1, 2, 3, 4, 5]
for i, d in enumerate(data):
    print(i, d)
# --- 悪い例 ---
data = [1, 2, 3, 4, 5]
for i in range(len(data)):
    print(i, data[i])
【実行結果】
0 1
1 2
2 3
3 4
4 5

悪い例として記載したようなrangeを使ってシーケンスを作って使用することも可能ですが、見てもわかるように可読性が良いとは言えません。enumerateを使用することで読みやすくなっていることが分かるかと思います。

まとめて処理する場合はzipを使用する

Pythonのfor文の場合で、複数オブジェクトを使ってまとめてループをする場合には、以下のようにzipを使用します。zipについては「zipを用いたfor文の使い方」に詳細をまとめています。

# --- 良い例 ---
data1 = [1, 2, 3, 4, 5]
data2 = "abcde"
for d1, d2 in zip(data1, data2):
    print(d1, d2)

# --- 良い例 ---
data1 = [1, 2, 3, 4, 5]
data2 = "abcde"
for d in zip(data1, data2):
    print(d)
# --- 悪い例 ---
data1 = [1, 2, 3, 4, 5]
data2 = "abcde"
for i in range(len(data1)):
    print(data1[i], data2[i])
【実行結果】
1 a
2 b
3 c
4 d
5 e

※zipの結果をtupleで扱ったときの場合
(1, 'a')
(2, 'b')
(3, 'c')
(4, 'd')
(5, 'e')

zipの場合では、タプルでデータのセットを取得することができるため、タプルとして処理しても構いませんし、アンパックでタプルのデータをそれぞれ変数に割り当てても構いません。

なお、zipはデータ数が少ない方にあわせて処理されるのが特徴です。データが多い方にあわせて処理する場合には、itertoolsパッケージのzip_longestを使用します。zip_longestについても「zipを用いたfor文の使い方」に記載していますので興味があれば参考にしてください。

関数の引数

ミュータブルなものを引数には指定しない

以下の例はPythonの関数で注意すべき事項として挙げておきます。

関数にはデフォルト引数を指定することができますが「デフォルト値にミュータブル(mutable)な型を使うべきではない。」という注意点があります。

以下の例で見てみましょう。

# --- 良い例 ---
def sample_function_good(in1, tmp_list=None):
    if tmp_list is None:
        tmp_list = []
    tmp_list.append(in1)
    return tmp_list


result1 = sample_function_good("A")
print(result1)
result2 = sample_function_good("B")
print(result2)
# --- 悪い例 ---
def sample_function_bad(in1, tmp_list=[]):
    tmp_list.append(in1)
    return tmp_list


result3 = sample_function_bad("A")
print(result3)
result4 = sample_function_bad("B")
print(result4)
【実行結果】
※良い例の場合
['A']
['B']

※悪い例の場合
['A']
['A', 'B']

この例は、値とリストを受け取ってリストにappendメソッドで追加するような関数になっています。悪い例の方では、デフォルト値として空のリスト[]を指定しており、リストが与えられない時には、新しくリストを作成してほしいと思っているのだと読み取れます。

呼び出しでは、いずれもリストを指定していないため、[‘A’]や[‘B’]といった一つだけのリストが返ってきてほしいのですが、悪い例の2回目の呼び出しでは[‘A’, ‘B’]と直前の呼び出しの値’A’が含まれてしまっています。

これは、関数のデフォルト値が最初に1回だけ評価されるためで、2回目以降に呼び出されたとしても最初のリストを使ってしまうことが理由です。

対処方法としては、良い例の方に書いたようにNoneを引数に指定し、関数内でNoneであった場合に初期化するようにするのが適切です。

関数については「関数の定義と呼び出し方法の基本」にまとめていますので興味があれば参考にしてください。

資源管理

with…as命令をリソースを適切に開放する

ファイルの入出力時には、openを使ってファイルを開きますが、使い終わった場合にはファイルをcloseする必要があります。しかし、closeは往々にして忘れられがちです。

そういったミスを回避するためには以下の良い例のようにwith…as命令を使用します。

# --- 良い例 ---
with open("temp_file.txt", "w", encoding="utf-8") as f:
    f.write("test")
# --- 悪い例 ---
tmp_file = open("temp_file.txt", "w", encoding="utf-8")
tmp_file.write("test")
tmp_file.close()
【実行結果】(temp_file.txtの中身)
test

上記のようにしておくと、withブロックを抜けた際に自動的にファイルがcloseされるので、資源の開放忘れを防止することができます。

ファイルの入出力関連については「ファイル入出力の基本」に詳細をまとめています。また、with…as命令が使えるかどうかはコンテキストマネージャーに対応しているかで決まります。コンテキストマネージャーについても「コンテキストマネージャーの基本」にまとめていますので興味があれば参考にしてください。

まとめ

Pythonには、Pythonらしいシンプルで読みやすいコードの書き方というものがあり、よく「Pythonic」であるといいます。Pythonicの考え方と実例として良い例/悪い例のいくつかについて紹介しました。

今回紹介した例がPythonicなコーディングの全てというわけではもちろんありません。また、良い例/悪い例という表現を使っていますが悪い例と書いている方でも動作はします。そして、必ずしも悪い例の方がダメというわけではなく場面によっては良い/悪いが異なる場合もあることは注意してください。

また、Pythonicのいい例があったらリライトしていこうと思います。

Pythonプログラミングでは「シンプルさ」「読みやすさ」というのを意識して書くようにすることが重要です。皆さんもPythonicなコーディングというのを意識しながらプログラミングをしてもらえるとプログラムの質が上がっていくのかなと思います。