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

Python には、Python らしいシンプルで読みやすいコードの書き方というものがあり「Pythonic」であるといいます。Pythonic の考え方と Pythonic なコーディングの例を紹介したいと思います。
Pythonic なプログラムコーディング
Pythonic とは
Python を学習していると「Pythonicなコーディング」という言葉を耳にすることが多いと思います。
Pythinic とは、Python らしく、シンプルで読みやすいコーディングスタイルを指し、自然で直感的な Python の書き方のことです。
この記事では、Pythonic なコーディングの考え方や例をまとめてみます。コードを見直す際の参考にしてみてください。
The Zen of Python
Python では「The Zen of Python」が PEP-20 に定義されています。The Zen of Python は、Python のシンプルさや読みやすさを 19 の原則でまとめたものです。
- Beautiful is better than ugly.
コードは美しく書くべきである - Explicit is better than implicit.
暗黙的よりも明示的であるほうがよい - Simple is better than complex.
複雑であるよりもシンプルである方がよい - Complex is better than complicated.
複雑でも難解であるよりはよい (3と関連) - 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.
実用性が純粋さより優先されることもある (8と関連) - Errors should never pass silently.
エラーは無視せず、適切に対処するべき - Unless explicitly silenced.
明示的に無視する指定がある場合を除く (10と関連) - In the face of ambiguity, refuse the temptation to guess.
曖昧な時には推測せず、明確さを求める - There should be one– and preferably only one –obvious way to do it.
物事を解決する方法は1つであるべきである - Although that way may not be obvious at first unless you’re Dutch.
(あなたがオランダ人※でない限り)その方法が明白ではないかもしれない (13と関連) - Now is better than never.
やらないよりは、今やる方がよい - Although never is often better than *right* now.
ただし、急いでやるよりも慎重に検討する方がよい場合もある (15と関連) - 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!
名前空間は素晴らしいアイデアだからもっと活用しよう
※「オランダ人」というのは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 冒頭の「A Foolish Consistency is the Hobgoblin of Little Minds (一貫性にこだわりすぎるのは、狭い心の現れである)」という言葉が示す通り、盲目的に従う必要はありません。
読みやすさを最優先にし、プロジェクトの既存の規約がある場合はそちらに従うべきです。PEP8 に従うことで逆に読みづらくなる場合も避けるべきでしょう。つまり、PEP8 を守ることが目的ではなく、チームの生産性やコードのメンテナンス性を高めるための参考とするという考え方が適切です。
また、世の中の多くのプロジェクトが PEP8 に従って作られています。そのため、プロジェクトの制約などがなく初心者がコーディングを学ぶときには、PEP8 を意識して学習するのがよいでしょう。
ツールの活用
PEP8 に基づく静的解析ツール(pep8、flake8、pylintなど)や、コードを自動で整形するフォーマットツール(black、isort など)も簡単に利用できるのでおすすめです。これらのツールをプロジェクトで使用することで、人に依存せずコードの品質を一定に保つ助けとなります。
Pythonic なコーディング例
以降では、Pythonicなコーディング例を紹介していきます。プログラミングを見直す際の参考にしてもらえると良いかと思います。
変数の操作
内包表記
Pythonic なコーディングの代表例の 1 つが内包表記です。内包表記はイテラブルなオブジェクトから新しいオブジェクトを生成する際にシンプルに記載する定義方法です。
リスト内包表記、辞書内包表記、集合内包表記、ジェネレータ内包表記といった種類がありますが、ここでは、リスト内包表記の例を見てみましょう。
# --- 良い例 --- 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 文による記載よりも処理速度が速いという特徴があります。ただし、処理速度に関しては状況によるため、必ずしも早くなるわけではないということに注意してください。
また、複雑な条件下でコードの可読性が悪くなる場合には、内包表記の使用を避けて 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 というリストの各要素を a ~ e までの変数に 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 に先頭と末尾を除くリストの値がまとめて割り当てられます。
getter や setter ではなく property を使う
様々なオブジェクト指向のプログラミング言語ではプライベートな変数にアクセスする方法として、getter や setter というメソッドを用意することが多いですが、Python のクラスで変数にアクセスする際にはプロパティ (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
Python のプロパティを用いる場合、値を取得するメソッドに @property デコレータを使用します。また、値を設定するメソッドは @<プロパティ名>.setter デコレータを使用します。Python でクラスを作成する際には積極的にプロパティを使用しましょう。
比較・判定
条件分岐などの比較・判定にも Python らしい書き方があります。
比較演算子のチェーン
if 文などで条件分岐の際には and、or などを使用しますが、範囲を表すような条件では以下のようにシンプルに記載できます。
# --- 良い例 --- a = 5 print(2 <= a <= 10)
# --- 悪い例 --- a = 5 print(2 <= a and a <= 10)
【実行結果】 True
「2 <= a <= 10」という記載は「2 以上 10 以下」というのが直感的に理解できます。
オブジェクトの判定 (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
ループ処理
インデックス付きのループは enumerate を使用する
C/C++ 言語等になじみがある人は、繰り返しでは for (i = 0; i < 10, i++) のようにインデックスを増やして処理することに慣れている人が多いかもしれません。
Python の for 文でインデックス付きで繰り返し処理をする場合は、enumerate を使用するのがおすすめです。
# --- 良い例 ---
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 を使用する方が読みやすくより Python らしい書き方です。
まとめて処理する場合は zip を使用する
Python の for 文の場合で、複数オブジェクトを使ってまとめて繰り返し処理をする場合は、zip を使用するのがおすすめです。
# --- 良い例 ---
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 なコーディングというのを意識しながらプログラミングをしてみてもらいたいと思います。







