Python入門

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

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

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

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

Pythonicとは

Pythonを学習していると「Pythonicなコーディング」という言葉を耳にすることがあります。

Pythinicとは、Pythonらしく、シンプルで読みやすいコーディングスタイルを指します。「Pythonicである」とは、自然で直感的なPythonの書き方であり、具体的な定義はありませんが、Pythonらしい書き方とされています。

この記事では、Pythonicなコーディングの考え方や例をまとめてみます。コードを見直す際の参考にしていただければ幸いです。

The Zen of Python

Pythonでは「The Zen of Python」がPEP-20に定義されています。The Zen of Pythonは、Pythonのシンプルさや読みやすさを19の原則としてまとめたものです。

  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つであるべきである
  14. Although that way may not be obvious at first unless you’re Dutch.
    (あなたがオランダ人※でない限り)その方法が明白ではないかもしれない (13と関連)
  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の生みの親であるGuido van Rossumがオランダ人であるため

The Zen of Pythonの趣旨を端的に言うと「シンプルで明快なコードが良いコード」という考え方です。この考え方は、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

PEP8の概要

Pythonicなコーディングをするには、PEP8についての理解も重要です。PEP8は、Pythonのコーディング規約をまとめたドキュメントで、公式のガイドラインとして提供されています。日本語版の「pep8-ja」も参考になります。

PEP8の主な内容

PEP8の主な内容としては、以下のような項目が挙げられます。

  • インデントや1行の長さ(例:インデントは4スペース、1行は79文字以内など)
  • 演算子の位置
  • 関数・変数・クラス名の命名規則  など

詳細は公式ドキュメントを参照してください。また、命名規則の部分は「コーディング規約 PEP8の命名規則」で整理してみているので参考にしてください。

PEP8の使い方と注意点

PEP8は、コーディングの一貫性を保つために有用ですが、PEP8冒頭の「A Foolish Consistency is the Hobgoblin of Little Minds (一貫性にこだわりすぎるのは、狭い心の現れである)」という言葉が示す通り、盲目的に従う必要はありません。

読みやすさを最優先にし、プロジェクトの既存の規約がある場合はそちらに従うべきです。PEP8に従うことで逆に読みづらくなる場合も避けるべきでしょう。つまり、PEP8を守ることが目的ではなく、チームの生産性やコードのメンテナンス性を高めることを目的としてください。

また、プロジェクトの制約などがなく初心者がコーディングを学ぶときには、PEP8を意識して学習するのがよいでしょう。

ツールの活用

PEP8に基づく静的解析ツール(pep8、flake8、pylintなど)や、コードを自動で整形するフォーマットツール(black、isortなど)も簡単に利用できるのでおすすめです。これらのツールをプロジェクトで使用することで、人に依存せずコードの品質を一定に保つ助けとなります。

Pythonicなコーディング例

以降では、Pythonicなコーディング例を紹介していきます。プログラミングを見直す際の参考にしてもらえると良いかと思います。

以下例で、良い例/悪い例という表現を使っている部分がありますが、全てのケースで当てはまるわけではないことに注意してください。シンプルさと読みやすさを重視した時には、現在の場面でどういったコーディングが良いかという視点を持つことが重要です。

変数の操作

内包表記

Pythonicなコーディングの代表例の一つが内包表記です。内包表記はイテラブルなオブジェクトから新しいオブジェクトを生成する際にシンプルに記載する定義方法です。

種類は、リスト内包表記、辞書内包表記、集合内包表記、ジェネレータ内包表記があります。詳細は「内包表記まとめ」を参考にしてください。

ここでは、リスト内包表記の例を見てみましょう。

# --- 良い例 ---
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までの既存リスト(data)から偶数の数値のみで新しいリスト(new_data)を作っています。内包表記での記載は1行で非常にシンプルに記載できていることが分かるかと思います。

内包表記は、Pythonicな書き方の代表例でかつPython内部で最適化されるため一般的には処理速度が速いという特徴があります。ただし、処理速度に関しては状況により、必ずしも早くなるわけではないということは注意してください。

また、複雑な条件下でコードの可読性が悪くなる場合には、内包表記の使用を避けてfor文を使用する方が適切です。

アンパックの使用

アンパック(またはアンパック代入)とは、タプルの要素を分割して別々の変数に代入できることを言います。「タプルの基本と使いどころ」も参考にしてください。

アンパックには便利な使い方があるので例を紹介します。

値を代入する

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

# --- 良い例 -----
a, b = 1, 2
print(a, b)
# --- 悪い例 -----
a = 1
b = 2
print(a, b)
【実行結果】
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

一般に値の交換では、一度別の変数に退避しての交換を考えることが多いです。しかし、アンパックを使用することで、上記例のように簡単に値の交換ができます。

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

アンパックを利用すると既存リストなどから複数の変数へ簡単に割り当てができます。

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

上記例では、dataというリストの各要素をaeまでの変数に1行で割り当てができており、とてもシンプルです。

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

複数の変数に値を割り当てる際に、*(アスタリスク)を使用すると、割り当て時に値をまとめてしまうことができます。

# --- 良い例 ---
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に先頭と末尾を除くリストの値がまとめて割り当てられます。

gettersetterではなくpropertyを使う

様々なオブジェクト指向のプログラミング言語を学んでいるとプライベートな変数にアクセスする方法として、gettersetterというメソッドを用意することが多いです。

Pythonのクラスで変数にアクセスする際にはプロパティを使用するのが適切です。プロパティについては、「クラスのプロパティの使い方」を参考にしてください。

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

Pythonのプロパティを用いる場合に、値を取得するメソッドには@propertyデコレータを使用します。また、値を設定するメソッドには@<プロパティ名>.setterデコレータを使用します。Pythonでクラスを作成する際には積極的にプロパティを使用すると良いでしょう。

なお、デコレータの考え方や基本については「デコレータ(decorator)の基本的な使い方」を参考にしてください。

比較・判定

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

比較演算子のチェーン

if文などで条件分岐の際には、andorなどを使用しますが、範囲を表すような条件では以下のようにシンプルに記載することが可能です。

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

上記例の2 <= a <= 10のように記載してあると、2以上で10以下というのが直感的に把握しやすいです。

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

値の判定には「==」や「!=」を使用しますが、「is」「is not」という判定方法もあります。それぞれの違いは以下の通りです。

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

変数の値を代入する前にNoneを設定しておくことがありますが、Noneの判定では以下のようにisis notを使用する方が適切です。

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

ループ処理

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

C/C++言語などになじみがある人は、繰り返しで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')

enumerateの時の説明と同様にrangeを使うことも可能ですが、zipを使用する方が読みやすいです。

また、zipの返却値はタプルとなっていますが、アンパックと組み合わせてそれぞれ変数に割り当てて使用する方法がよく行われます。状況に応じて使い分けてもらえると良いと思います。

関数の引数

ミュータブルな型を引数に指定しない

Pythonの関数において、引数のデフォルト値にミュータブルな型を使うべきではありません。

以下はリストに値を追加する関数の例ですが、リストが渡されなかった場合には、新規リストを作成して返却する関数を想定しています。ミュータブルな型をデフォルト値に使うとどうなるか見てみましょう。

# --- 良い例 ---
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']

Pythonでは、関数のデフォルト値は最初に1回だけ評価されます。そのため、デフォルト値として空のリスト[]を指定すると、2回目以降は既に作成されているリストを使用するため、新規リストは作成されず、想定外の動作となってしまいます。

このような場合には、引数のデフォルト値にNoneを指定し、関数内で初期化するのが適切です。関数については「関数定義の基本」も参考にしてください。

資源管理

with...as命令でリソースを適切に開放する(コンテキストマネージャー)

ファイルの入出力時には、openでファイルを開きますが、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らしいシンプルで読みやすい「Pythonicなコーディング」について、考え方と実例を紹介してきました。

「Pythonicである」とは、自然で直感的なPythonの書き方であり、今回紹介して実例がPythonicなコーディングの全てというわけではありません。また、今回紹介した例も状況により良い/悪いが異なる場合があります。

Pythonicの趣旨としては、シンプルさと読みやすさを意識するということだと思っています。皆さんもPythonicなコーディングというのを意識しながらプログラミングをしてみてもらいたいと思います。