【Python】良い書き方と悪い書き方を知って中級者を目指す🐍

:::note warn
私個人の話で恐縮ですが、「[ギークハウス新宿](https://geejuku.tokyo/)」というエンジニア中心のシェアハウスの運営に関わっています。

[勉強会・懇親会等のイベント](https://geejuku.connpass.com/)を開催しております。内見等興味ある方はお気軽に[こちら](https://x.com/geejuku)までご連絡ください。
:::

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

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

元記事：[Good and Bad Practices of Coding in Python](https://dev.to/duomly/the-best-practices-of-coding-in-python-and-what-to-avoid-3c65/comments)

**※ 記事の内容に注意すべき点と誤りがあるので、詳しくは注釈まで目を通すことをおすすめします。**

（以下、翻訳した本文）

---

![Cover image for Good and Bad Practices of Coding in Python](https://res.cloudinary.com/practicaldev/image/fetch/s--oloNCKzi--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://thepracticaldev.s3.amazonaws.com/i/ow6du7cadmzw716q9fhp.png)

この記事は元々 https://www.blog.duomly.com/good-and-bad-practices-of-coding-in-python/ に公開されたものです。

Pythonは可読性を重視した高水準のマルチパラダイムプログラミング言語です。Pythonは、「Pythonの禅」、別名ではPEP 20と呼ばれるルールに従って開発、保守され、幅広く使用されている言語です。

この記事では、頻繁に会う可能性が高いPythonでのコーディングの良い例と悪い例をいくつか示します。

## アンパック（unpacking）を使用して簡潔にコードを記述する

パック（packing）とアンパック（unpacking）は強力なPythonの特長です。アンパックを使用することで、複数の値を複数の変数に割り当てることが可能です[^1]。

[^1]: 他言語（RubyやJavaScript、C#など）にある分割代入と同じ機能です。

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

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

```python:良い例
>>> a, b = b, a
>>> a
'my-string'
>>> bf
2
```    

アンパックは、より複雑な場合の複数の変数への割り当てに使うことができます。たとえば、次のように変数へ値を割り当てることはできます。

```python:悪い例
>>> 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)
```    
  
しかし、代わりに、より簡潔で間違いなく読みやすいアプローチを使うことができます。

```python:良い例
>>> a, b, c, d, e = x
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
```    
 
イケてますよね？でも、これはさらにイケてます。

```python:さらに良い例
>>> a, *y, e = x
>>> a, e, y
(1, 16, [2, 4, 8])
```

ポイントは、`*`付きの変数が他に割り当てられていない値をまとめているという点です。

## チェーンを使用して簡潔にコードを記述する
 
Pythonでは、比較演算をチェーンさせることができます。したがって、2つ以上の比較演算が`True`であるかどうかを使用して確認する必要はありません。

```python:悪い例
>>> x = 4
>>> x >= 2 and x <= 8
True
```    
  
代わりに、数学者のように、これをよりコンパクトな形式で書くことができます。

```python:良い例
>>> 2 <= x <= 8
True
>>> 2 <= x <= 3
False
```    
 
Pythonは連鎖割り当てもサポートしています。したがって、複数の変数に同じ値を割り当てる場合は、簡単に行うことができます。

```python:悪い例
>>> x = 2
>>> y = 2
>>> z = 2
```    

よりエレガントな方法は、アンパックを使用することです。

```python:良い例
>>> x, y, z = 2, 2, 2
```    

ただし、連鎖割り当てを使用すると、状況はさらに改善されます。

```python:さらに良い例
>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)
```    

値がミュータブルな型の場合は注意してください。すべての変数は同じインスタンスを参照します。


## `None`のチェック

`None`はPythonでは特別でユニークなオブジェクトです。Cライクな言語での`null`と同じような目的があります。

変数が`None`を参照しているかは比較演算子の`==`および`！=`で確認することができます。

```python:悪い例
>>> x, y = 2, None
>>> x == None
False
>>> y == None
True
>>> x != None
True
>>> y != None
False
```    

しかし、よりPython的で望ましいのは`is`および`is not`を使うやり方です。

```python:良い例
>>> 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はそれを容易にするためにいくつかの組み込みクラスを提供しています。

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

```python:悪い例
>>> x = [1, 2, 4, 8, 16]
>>> for i in range(len(x)):
...     print(x[i])
... 
1
2
4
8
16
```    

ただし、シーケンスを繰り返すより良い方法があります。

```python:良い例
>>> for item in x:
...     print(item)
... 
1
2
4
8
16
```    

しかし、逆の順序で繰り返しをしたい場合はどうでしょうか？もちろん、範囲をまた使うことができます。

```python:悪い例
>>> for i in range(len(x) - 1, -1, -1):
...     print(x[i])
... 
16
8
4
2
1
```    

シーケンスを逆にする方がよりエレガントなやり方です。

```python:良い例
>>> for item in x[::-1]:
...     print(item)
... 
16
8
4
2
1
```

この場合、Python的なやり方は、`reversed`を使用して、シーケンスのアイテムを逆の順序で生成するイテレータを取得することです。

```python:さらに良い例
>>> for item in reversed(x):
...     print(item)
... 
16
8
4
2
1
```

シーケンスの要素と対応するインデックスの両方が必要になる場合があります。

```python:悪い例
>>> for i in range(len(x)):
...     print(i, x[i])
... 
0 1
1 2
2 4
3 8
4 16
```    

`enumerate`を使用して、インデックスとアイテムを含むタプルを生成する別のイテレーターを取得するやり方の方が良いとされています。

```python:良い例
>>> for i, item in enumerate(x):
...     print(i, item)
... 
0 1
1 2
2 4
3 8
4 16
```

イケてます。しかし、2つ以上のシーケンスを反復処理したい場合はどうでしょうか。もちろん、`range`をここでも使うことができます。

```python:悪い例
>>> 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`を適用して、対となる要素のタプルを取得できます。

```python:良い例
>>> for item in zip(x, y):
...     print(item)
... 
(1, 'a')
(2, 'b')
(4, 'c')
(8, 'd')
(16, 'e')
```    

アンパックと組み合わせることができます。

```python:さらに良い例
>>> for x_item, y_item in zip(x, y):
...     print(x_item, y_item)
... 
1 a
2 b
4 c
8 d
16 e
```    

範囲は非常に役に立つものであることを覚えておいてください。ただし、（上記のような）より便利な代替手段がある場合もあります。

辞書を反復処理すると、キーが生成されます。

```python:悪い例
>>> z = {'a': 0, 'b': 1}
>>> for k in z:
...     print(k, z[k])
... 
a 0
b 1
```

ただし、メソッド`.items()`を適用して、キーと対応する値を持つタプルを取得できます。

```python:良い例
>>> for k, v in z.items():
...     print(k, v)
... 
a 0
b 1
```    

また、メソッド`.keys()`を使うことでキーを、`.values()`を使うことで値を反復処理することもできます。


## `0`との比較

数値データがあり、数値がゼロに等しいかどうかを確認する必要がある場合は、比較演算子`==`および`！=`を使用できますが、そうする必要はありません。

```python:悪い例（注釈3に書いているように場面によってはこちらが良い例）
>>> x = (1, 2, 0, 3, 0, 4)
>>> for item in x:
...     if item != 0:
...         print(item)
... 
1
2
3
4
```
    
Python的なのは、ブール値のコンテキストで`0`が`False`として解釈される一方、他の全ての数字は`True`として見なされるという事実を利用するやり方です。

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

これを念頭に置いて、`if item != 0`の代わりにただ`if item`を使えば良いのです。（注意点に関して訳注あり[^2]）

[^2]: 一概に`if item`といった書き方が良いとは言えない。整数以外の値（例えばNoneや空文字など）が入る可能性がある場合は`if item != 0`というように明示的に書いた方が良い書き方といえる時もあることに注意。詳しくはコメントでのやり取りを参照。
 

```python:良い例
>>> for item in x:
...     if item:
...         print(item)
... 
1
2
3
4
```    

同じロジックに従い、`if item == 0`の代わりに`if not item`を使用できます。


## ミュータブルなオプション引数(デフォルト引数)を避ける

Pythonには、関数とメソッドに引数を提供するための非常に柔軟なシステムがあります。オプション引数はこのシステムの一部です。ただし、注意が必要です。通常、ミュータブルなオプション引数を使用しない方が賢明です。次の例について考えてみます。

```python:悪い例？

>>> def f(value, seq=[]):
...     seq.append(value)
...     return seq
```    

`seq`を指定しない場合、`f()`は空のリストに値を追加し、`[value]`のようなものを返します。これは一見すると、うまくいくように見えます。

```python
>>> f(value=2)
[2]
```

問題なさそうですね？そんなことはありません！次の例を検討してみましょう。

```python
>>> f(value=4)
[2, 4]
>>> f(value=8)
[2, 4, 8]
>>> f(value=16)
[2, 4, 8, 16]
```    

驚いたでしょうか？混乱していますか？もしそうなら、あなただけではありません。
オプション引数（この場合はリスト）の同じインスタンスが、関数が呼び出されるたびに使われているようです。時には上のコードがしていることと全く同じことをしたい場合があるかもしれません。しかし、それを回避する必要がある場合の方がはるかに多いことでしょう。いくつかの追加ロジックを使うと、これを避けることができます。方法のうちの1つは次です。

```python:良い例
>>> def f(value, seq=None):
...     if seq is None:
...         seq = []
...     seq.append(value)
...     return seq
```    

さらに短いバージョンは次のとおりです。（※注意点に関して訳注あり[^3]）

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

```python:さらに良い例
>>> def f(value, seq=None):
...     if not seq:
...         seq = []
...     seq.append(value)
...     return seq
```    

ようやく、異なる動作が得られます。

```python
>>> f(value=2)
[2]
>>> f(value=4)
[4]
>>> f(value=8)
[8]
>>> f(value=16)
[16]
``` 

ほとんどの場合、これが欲しい結果です。

## 慣例的なゲッターとセッターの使用を避ける

Pythonでは、C++やJavaと同様にゲッターメソッドとセッターメソッドを定義できます。

```python:悪い例
>>> class C:
...     def get_x(self):
...         return self.__x
...     def set_x(self, value):
...         self.__x = value
```

次が、ゲッターとセッターを使用してオブジェクトの状態を取得および設定する方法です。

```python:悪い例
>>> c = C()
>>> c.set_x(2)
>>> c.get_x()
2
```    

場合によっては、これがやりたいことを実現するための最良の方法です。ただし、特に単純なケースでは、プロパティを定義して使用する方が洗練されていることがよくあります。

```python:良い例
>>> class C:
...     @property
...     def x(self):
...         return self.__x
...     @x.setter
...     def x(self, value):
...         self.__x = value
```    

プロパティは、従来のゲッターやセッターよりもPython的と考えられています。C#と同様に、つまり通常のデータ属性と同じように使用できます。

```python:良い例
>>> c = C()
>>> c.x = 2
>>> c.x
2
```    
したがって、一般的には、可能な場合はプロパティを使用し、どうしても必要な場合はC++ライクなゲッターとセッターを使用することがグッドプラクティスとされています。（※訳注：**記事ではこのように書かれているがこの点は誤りと考えられる。Pythonでは一般的にはにはパブリックなアトリビュートを使い、必要な場合はプロパティやゲッター、セッターを使用することが慣例的に好まれている**。[^4]を参照）

[^4]: Pythonコミュニティ的にはPythonでは基本的にパブリックなアトリビュート(メンバ変数)を使うことが好まれている（クラス外部からアクセスする属性にはできる限り`_`や`__`はつけない方が良いという意味）。単純に値を取得、セットしたい場合はプロパティは使用しない方がPythonicだと考えられていることに注意。それでも独自の処理を加えるなどprotected(`_`)属性、private(`__`)属性のアトリビュート（メンバ変数）を使いたい理由がある場合は[従来のゲッターとセッターの使用を避ける](https://qiita.com/_masa_u/items/b21c493a82e8aba8993f#%E5%BE%93%E6%9D%A5%E3%81%AE%E3%82%B2%E3%83%83%E3%82%BF%E3%83%BC%E3%81%A8%E3%82%BB%E3%83%83%E3%82%BF%E3%83%BC%E3%81%AE%E4%BD%BF%E7%94%A8%E3%82%92%E9%81%BF%E3%81%91%E3%82%8B)で言及されている`@property`（ゲッター）と`@x.setter`（セッター）、もしくはC++、Java式のゲッター・セッターメソッドを作成・使用することが推奨されている。


## 保護されたクラスメンバーへのアクセスを避ける

Pythonには本当のプライベートなクラスメンバーはありません。ただし、インスタンスの外でアンダースコア（_）で始まるメンバーにアクセスしたり変更したりしてはならないという規約があります。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`の３つのデータメンバーが存在します。メンバーの名前が2つのアンダースコアで始まる場合は、難号化（mangled）され、変更されます。そのため、`.__z`の代わりに`._C__z`ができます。

`.x`には直接アクセスまたは変更しても問題ありません。

```python:良い例
>>> c.x  # OK
1
```   

インスタンスの外部から`._y`にアクセスまたは変更することもできますが、これはバッドプラクティスと見なされています。

```python:悪い例
>>> c._y  # 可能だが悪い
2
```
    
`.__z`にアクセスすることはできません。zは難号化されているからです。しかし、`._C__z`にアクセスまたは変更することはできます。

```python:さらに悪い例
>>> c.__z # エラー！
Traceback (most recent call last):
File "", line 1, in 
AttributeError: 'C' object has no attribute '__z'
>>> c._C__z # 可能だが、１個前の例よりさらに悪い！
4
>>>
```    

これは避けてください。クラスの作者は、おそらく名前をアンダースコアで始めて、「使用するな」と伝えています。（※訳注あり[^5]）

[^5]: `_`のついていないアトリビュート（メンバ変数・関数）はJavaなどの他の言語でのアクセスレベル`public`に、`_`で修飾されたアトリビュートは`protected`に、`__`で修飾されたメンバー変数がアクセスレベル`private`に相当すると言えるが、記事に書かれている通り`_`や`__`で修飾されていてもアクセスしようとすればできてしまう。アトリビュートの変更もできてしまうため、これが「Pythonのプライベートなクラスメンバーは既存の動作を保持していることが保証されていません」という意味。

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

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

```python:悪い例
>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file`
```    

メモリを適切に管理するには、ジョブ終了後にこのファイルを閉じる必要があります。

```python:悪い例
>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file and`
>>> my_file.close()
```    

ファイルを閉じることは、閉じないよりもマシです。しかし、ファイルの処理中に例外が発生した場合はどうでしょうか？その後、`my_file.close()`は決して実行されません。

この場合、例外処理構文または`with`コンテキストマネージャーで対応できます。後者の方法は、コードを`with`ブロック内に配置することを意味します。

```python:良い例
>>> 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で役立つ美しいコードを書くことを楽しみましょう！