きれいなPythonプログラミング（パイソニックなコードを書こう）を読んでみた

# はじめに

日頃、Pythonを使う機会があるのですが、「もう一歩詳しくなり、中級者を目指したい！」という思いから、2022/2/15に発売された書籍「[きれいなPythonプログラミング ~クリーンなコードを書くための最適な方法](https://www.amazon.co.jp/%E3%81%8D%E3%82%8C%E3%81%84%E3%81%AAPython%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%81%AA%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%9B%B8%E3%81%8F%E3%81%9F%E3%82%81%E3%81%AE%E6%9C%80%E9%81%A9%E3%81%AA%E6%96%B9%E6%B3%95-Al-Sweigart/dp/4839977402)」を読みました。

特に、第6章にある「**パイソニックなコードを書こう**」が非常に勉強になったので自分なりの解釈／調査結果を含めてメモを残しておきます。
※解釈が誤っている箇所もあるかと思います。誤りがあればご指摘いただけると幸いです。

# 誤用の多い構文

Python以外の言語を使ったことがある人は、その言語と同じ考え方／手法でコードを書くかもしれません。
Pythonにおける標準的なアプローチを学ぶことで時間と労力を削減することができます。

## ループ処理では`range()`ではなく`enumerate`を使う

慣習的に`range(len())`とインデックス番号でループを回すのは単純ですが、これは読みにくく理想的ではありません。

```python:パイソニックでない例
fruits = ["orange", "banana", "apple"]
for i in range(len(fruits)):
  print(i, fruits[i])

#...出力結果...
0 orange
1 banana
2 apple
```

代わりに`enumerate()`を使うとスッキリします。

```python:パイソニックな例
fruits = ["orange", "banana", "apple"]
for i, fruit in enumerate(fruits):
  print(i, fruit)

#...出力結果...
0 orange
1 banana
2 apple
```

なお、インデックス番号が不要でリストの要素だけ必要な場合は直接リストを走査することができます。

```python:パイソニックな例
fruits = ["orange", "banana", "apple"]
for fruit in fruits:
  print(fruit)

#...出力結果...
orange
banana
apple
```

## `open()`と`close()`ではなくwith文を使う

pythonでファイル操作を行う際は以下の順序で処理を記述します。

1. `open()`関数によるファイルオープン
1. ファイルに対するなんらかの処理
1. `close()`関数によるファイルクローズ

※`open()`関数と`close()`関数は必ずセットで利用する。

しかし、`close()`メソッドを記述し忘れてしまったり、以下のようにtryブロック内でエラーが発生して`close()`処理がスキップされてしまうとファイルが閉じられなくなってしまい、他のプログラムがファイルにアクセスできない状態となってしまいます。

```python:パイソニックでない例
try:
  f = open("sample.txt", "w")
  print(20 / 0)  # <- ZeroDivisionError発生により、処理がexceptブロックに移る
  f.close()      # <- 実行されない
except:
  print("エラー発生")
```

`with`文を使えば、**withブロックを抜けるときにcloseメソッドを自動的に呼び出してくれます。**

```python:パイソニックな例
with open("sample.txt", "w") as f:
  f.write("hogehoge")
```

## `None`との比較は`==`ではなく`is`を使う

pythonで2つの値を比較する際は、等号演算子`==`と恒等演算子`is`を利用することができます。

### 等号演算子`==`とは？

2つのオブジェクトの値（変数の値）を比較します。

```python
# fruit_1とfruit_2に代入されている値は同じ
fruit_1 = "banana"
fruit_2 = "banana"

print(fruit_1 == fruit_2) # <- True
```

### 恒等演算子`is`とは？

2つのオブジェクトの同一性を比較します。同一性とは、変数が持っている参照先（メモリのポインタ）が同じかどうかを意味します。

```python
fruit_1 = "banana"
fruit_2 = fruit_1

# fruit_1とfruit_2は同じ参照先を持っているので同一オブジェクト
print(id(fruit_1)) # <- ID：22550541970544
print(id(fruit_2)) # <- ID：22550541970544

print(fruit_1 is fruit_2) # <- True
```

2つが演算子の違いがわかったところで、`None`のID（参照先）ついて確認してみます。

```python
fruit_1 = None
fruit_2 = None
 
print(id(fruit_1)) # <- ID：9484816
print(id(fruit_2)) # <- ID：9484816

print(fruit_1 is fruit_2) # <- True
```

`None`はNoneTypeデータ型の唯一の値であるため、Pythonプログラム内では`None`オブジェクトが**1つだけ存在します。**
そのため、変数の値がNoneに設定されている場合、上記の例のように`is None`の比較は常に`True`と評価されます。

一方、等号演算子による`== None`での比較の場合、以下のように比較ダンダーメソッドのオーバーロードを使うことで`==`の挙動を変更することができます。

```python
class BananaClass:
    pass

class AppleClass:
    # 比較ダンダーメソッドのオーバーロード
    def __eq__(self, other):
        if other is None:
            return True

banana = BananaClass()
print(banana == None) # <- False

apple = AppleClass()
print(apple == None)  # <- True
```

上記のように`==`演算子をオーバーロードしている場合、正しくNoneか否かの判定ができない可能性があります。
よって結論としては、`== None`ではなく、念のため`is None`で比較するのが慣習となっています。

また、値がBoolean値の場合は`is`演算子を使ってはいけません。
Booleanの場合は`==`を使って判定する、もしくは、`if apple:` や `if not apple`のように記述するのが一般的です。

# 文字列のフォーマット

## バックスラッシュ（\\）が多い文字列の場合はraw文字列を使う

raw文字列とは、文字列の先頭に`r`を付けたものであり、バックスラッシュをエスケープ文字として扱いません。
例えば、以下のようなWindowsのファイルパスを記述する場合は`\\`と書いてバックスラッシュ文字をエスケープする必要があります。

```python:パイソニックでない例
# バックスラッシュ文字を文字列に含めたい場合は、\\と記述してエスケープする必要がある
print("C:\\Users\\admin\\Desktop\\sample.txt")

#...出力結果...
C:\Users\admin\Desktop\sample.txt
```

raw文字列を使えばエスケープ不要なのでコードが読みやすくなります。

```python:パイソニックな例
# 文字列の前にr接頭辞を付与
print(r"C:\Users\admin\Desktop\sample.txt")

#...出力結果...
C:\Users\admin\Desktop\sample.txt
```

## f-stringによる文字列のフォーマット

変数の値を含めた文字列を生成する手法として、元々は以下2つがありました。

```python:
# ① %sを使う
test1 = "apple"
test2 = "banana"
print("私は %s と %s が好きです。" % (test1, test2))

# ② format()メソッドを使う
test1 = "apple"
test2 = "banana"
print("私は {0} と {1} が好きです。".format(test1, test2))
```

python3.6以降では、**f-string(フォーマットストリング)** を使うことで文字列フォーマットが直感的で読みやすくなります。
前述のrow文字列が先頭に`r`を付けるように、f-stringでは文字列の先頭に`f`を付けます。

```python:
test1 = "apple"
test2 = "banana"

# 文字列の前にf接頭辞を付与
print(f"私は {test1} と {test2} が好きです。")
```

# Listのコピーについて

以下のようにスライス構文を使うことで、既存のListから新しいListを作成することができます。

```python:
fruits = ["apple", "orange", "banana", "grape"]

# 既存のListから新しいListを作成
fruits_copy = fruits[2:] # <- 終了インデックスを省略すると、終了インデックスはデフォルトでListの最後になる。
print(fruits_copy)

#...出力結果...
# ['banana', 'grape']
```

スライス構文にて、開始インデックス/終了インデックスの両方を省略すると、実質的にリストのコピーを作成することと同じになります。

```python:
fruits = ["apple", "orange", "banana", "grape"]

# 既存のListから新しいListを作成
fruits_copy = fruits[:] # <- 開始インデックス/終了インデックスを省略
print(fruits_copy)

#...出力結果...
# ['apple', 'orange', 'banana', 'grape']

# （※1）2つのListは異なるオブジェクトであることがわかる
print(id(fruits) == id(fruits_copy)) # <- False
```

（※1）`fruits_copy = fruits[:]`の行は`fruits`の内容をコピーしますが、`fruits_copy = fruits`は**Listの参照** をコピーする点に注意です。

ただ、`[:]`によるListのコピーはあまり見た目がよくない（わかりづらい）ため、以下のように`copy`モジュールの`copy()関数`を使うのがパイソニックなコードです。

```python:パイソニックな例
import copy
fruits = ["apple", "orange", "banana", "grape"]

# 既存のListから新しいListを作成
fruits_copy = copy.copy(fruits) # <- copy()関数を使うとコピー処理を実行していることが一目瞭然
print(fruits_copy)

#...出力結果...
# ['apple', 'orange', 'banana', 'grape']

# 2つのListは異なるオブジェクトであることがわかる
print(id(fruits) == id(fruits_copy)) # <- False
```

## 浅いコピーと深いコピー

上記で紹介されている`[:]`および`copy.copy()`を使ったListのコピーはいずれも**浅いコピー**になります。
浅いコピーの場合、以下のように**多次元配列のように入れ子になったオブジェクト**はうまくコピーできません。

```python:浅いコピー
import copy

fruits = [["banana", "apple"], ["orange", "grape"]]

# 浅いコピーを実行
fruits_copy = copy.copy(fruits)

# 2次元リストの"banana"を"Lime"に変更する
fruits_copy[0][0] = "Lime"

# コピー元の「fruits」も"Lime"になっている
print(fruits)      # [['Lime', 'apple'], ['orange', 'grape']]
print(fruits_copy) # [['Lime', 'apple'], ['orange', 'grape']]

# 内包されているオブジェクトは同じ
print(id(fruits[0]))        # ID：22461722900352
print(id(fruits_copy[0]))   # ID：22461722900352
```

上記のように、多次元配列に対して浅いコピーを行うと要素が**同じインスタンス**となってしまいます。（浅いコピーは1次元のリストに対してのみ有効）

多次元配列を独立した異なるオブジェクトとしてコピーする場合は、**深いコピー**を行う必要があります。
深いコピーは`copy.deepcopy() `を使います。

```python:深いコピー
import copy

fruits = [["banana", "apple"], ["orange", "grape"]]

# 浅いコピーを実行
fruits_copy = copy.deepcopy(fruits)

# 2次元リストの"banana"を"Lime"に変更する
fruits_copy[0][0] = "Lime"

# コピー元は変わらない
print(fruits)      # [['banana', 'apple'], ['orange', 'grape']]
print(fruits_copy) # [['Lime', 'apple'], ['orange', 'grape']]

# 内包されているオブジェクトは異なる（同じインスタンスではない）
print(id(fruits[0]))        # ID：22601618573056
print(id(fruits_copy[0]))   # ID：22601618667328
```

@shiracamusさん、ご指摘ありがとうございます。


# パイソニックな辞書の使い方

## `get()`を使う

Pythonでは、存在しない辞書のキーにアクセスした場合、`KeyError`が発生します。
try-catch等でエラーハンドリングを実装していないとそこで処理が強制終了されてしまいます。

```python
fruits = {"banana" : 1}
print(f"私はorangeを{fruits['orange']}つ持っています") # <- KeyError: 'orange'
```

これを回避するため、以下のように`if`文を使って特定の値が辞書に含まれているか確認することがありますが、これはパイソニックではありません。

```python:パイソニックでない例
fruits = {"banana" : 1}
if "orange" in fruits: # <- キーに"orange"が含まれているかを確認する
    print(fruits["orange"]) 
else:
    print("not orange")
```

上記のように、キーが存在しないパターンが頻繁に発生することから、Pythonの辞書には`get()`メソッドが用意されています。
`get()`メソッドでは、キーが存在しない場合に返すデフォルト値を指定することができます。

```python:パイソニックな例
fruits = {"banana" : 1}
print(f"私はorangeを{fruits.get('orange', 2)}つ持っています")
```

`get()`メソッドでは、まず`get()`の第1引数の値（`orange`）が辞書（`fruits`）内のキーに存在するかどうかをチェックします。
存在する場合、辞書内の第1引数の値（`orange`）に対応する値を返します。
存在しない場合、第2引数に指定された値（`2`）を返します。
これにより、if-else文で事前チェックするよりも短く読みやすくなります。

## `setdefault()`を使う

`get()`メソッドと同様に`setdefault()`メソッドでもキーが存在しなかった場合にデフォルト値を返すことができます。

```python
fruits = {}
print(fruits.setdefault("banana", 1)) # <- "banana"は辞書内に存在しないので第2引数の"1"を返す
```

ここで`get()`メソッドとの違いですが、**キーが存在しなかった場合、`setdefault()`内で指定したkey-valueが新しい要素として辞書にセットされます。**

```python
fruits = {}

# get()メソッドの場合
print(fruits.get("banana", 1)) # <- 1
print(fruits) # <- {}

# setdefault()メソッドの場合
print(fruits.setdefault("banana", 1)) # <- 1
print(fruits) # <- {'banana': 1}
```

辞書にキーが存在するか否かをチェックし、キーがない場合はデフォルト値を設定するような処理がある場合は`setdefault()`で代用してみてください。

## `collections.defaultdict`を使う

`collections.defaultdict`クラスを使用すると、**KeyErrorを完全になくすことができます。**
`defaultdict`を使うためには、標準collectionsライブラリから別途importする必要があります。

```python
from collections import defaultdict
```

`defaultdict`にデフォルト値で使用するデータ型を渡すことで、デフォルトの辞書を作成します。
例えば、以下のように`defaultdict`にintを渡すことで、キーが存在しない場合のデフォルト値として`0`を返す辞書を作ることができます。

```python
from collections import defaultdict

num_dict = defaultdict(int) # <- intを渡す
print(num_dict) # <- defaultdict(<class 'int'>, {})

num_dict["banana"] += 1 # <- キー"banana"に対する値をセットする必要がない
print(num_dict["banana"]) # <= 1

num_dict["orange"] # <- キー"orange"に対する値をセットする必要がない
# "orange"のvalueにデフォルト値"0"がセットされている
print(num_dict["orange"]) # <- 0
```

なお、`defaultdict`にはint以外にもlistやdict、boolを渡すことができます。

考えられるすべてのキーに対してデフォルト値が必要な場合は、通常の辞書を使用して逐一`setdefault()`メソッドを呼び出すよりも、`collections.defaultdict`を使用する方がはるかに簡単です。

## switch文の代わりに辞書を使う

Java等の言語にはswitch文がありますが、~~Pythonにはswitch文がありません。~~
⇒Python3.10から[match文](https://peps.python.org/pep-0634/)が使えるようになったようです。

Pythonでswitch文と同様の処理を行う場合はif-elif-else文を使って以下のように記述することがあります。

```python
if fruits == "apple":
    color = "red"
elif fruits == "banana":
    color = "yellow"
elif fruits == "lime":
    color = "green"
else:
    color = "no color"
```

上記コードはパイソニックでないとまでは言えませんが、少し冗長です。
そこで、if-elif-else文の代わりに辞書を使って以下のように記述するのがパイソニックです。

```python:パイソニックな例
color= {
    "apple"  : "red",
    "banana" : "yellow",
    "lime"   : "green"
}.get(fruits, "no color")
```

上記コードは代入文が1つあるだけで非常に簡潔です。
`get()`の第1引数に指定したキーの値を返しますが、キーが存在しない場合は第2引数にセットしたデフォルト値を返します。

しかしながら簡潔になる一方、コードが直感的ではなく読みづらくなることがあります。この方法を使うかどうかは検討の余地があります。

# Pythonの三項演算子
条件に基づいて2つの値のどちらかを取得するような場合、通常は`if-else`を使います。これはパイソニックです。

```python:パイソニックな例
is_apple = True

message = ""
if is_apple:
    message = "これはappleです"
else:
    message = "これはappleではありません"

print(message) # <- これはappleです
```
**三項演算子**を使うことで上記パターンに当てはまるコードを1行で簡潔に表現できるのですが、Pythonの三項演算子は他のほとんどのプログラミング言語とは書き方が異なります。

```java:Javaの三項演算子の書き方
変数 = 条件式 ? 条件が成立したときの値 : 条件が成立しなかったときの値
```

```python:Pythonの三項演算子の書き方
変数 = 条件が成立したときの値 if 条件式 else 条件が成立しなかったときの値
```

上記のように、Pythonでは`if`と`else`の間に条件式を挟む独特な配置で実装されています。具体的な記述例は以下です。

```python
is_apple = True

true_value = "これはappleです"
false_value = "これはappleではありません"

message = true_value if is_apple else false_value

print(message) # <- これはappleです
```

なお、以下のように記述することで他のプログラミング言語のような擬似三項演算子を作ることができるのですが、これはパイソニックではありません。

```python:パイソニックでない例
is_apple = True

true_value = "これはappleです"
false_value = "これはappleではありません"

message = is_apple and true_value or false_value

print(message) # <- これはappleです
```

パイソニックでない理由として、上記の擬似三項演算子には微妙なバグがあります。
それは、以下のように`true_value`が偽の場合（`0`, `False`, `None`, 空白文字など）、条件式（`is_apple`）が`True`であっても`false_value`と評価されます。

```python:擬似三項演算子の微妙なバグ
is_apple = True

# 例として空白文字をセット
true_value = ""
false_value = "これはappleではありません"

message = is_apple and true_value or false_value

print(message) # <- これはappleではありません
```

よって、擬似三項演算子は使わないようにしましょう。

上記を踏まえた結論として、三項演算子はパイソニックというほどでもないですが、パイソニックでないというわけでもありません。
使用する場合は、**条件式の中に条件式を入れるといったネスト構造にしないようにしましょう。**

```python:パイソニックでない例
age = 30

age_range = "child" if age < 13 else "teenager" if age >= 13 and age < 18 else "adult"

print(age_range) # <- adult
```

上記のように、三項演算子のネストはコードの可読性が悪くなり、読んでいてイライラします。

# 変数の扱い

変数の値を確認したり変更したりすることはよくありますが、Pythonではこれを行う方法がいくつかあります。

## 比較演算子の連結

特定の数値が範囲内に収まっているかどうかを確認する場合は、以下のように比較演算子と`and`演算子を使います。

```python:パイソニックでない例
num = 50
if 30 < num and num < 100:
```

しかし、Pythonでは比較演算子を連鎖させることができるため、`and`演算子を使う必要がありません。

```python:パイソニックな例
num = 50
if 30 < num < 100:
```

## 代入演算子の連結

代入演算子`=`も連鎖させることが可能です。
1行のコードの中で複数の変数に同じ値を代入することができます。

```python:パイソニックな例
banana = apple = orange = "fruits"
print(banana, apple, orange)

#...出力結果...
fruits fruits fruits
```

上記3つの変数がすべて同じかどうかを確認するには`and`演算子を使ってもよいですが、`==`を連鎖する方法もあります。

```python:パイソニックな例
banana = apple = orange = "fruits"
print(banana == apple == orange == "fruits")

#...出力結果...
True
```

演算子の連鎖はちょっとしたことですが便利なコード短縮方法です。

:::note warn
注意


`banana = apple = orange = ["banana", "apple"]`

上記のように連鎖を使ってリスト等のミュータブルな値を代入した場合、同じインスタンスとなってしまうため注意が必要です。
詳しくは本記事のコメント欄、@shiracamusさんの解説をご参照ください。
:::



## 変数の値が複数の値のどれと等しいかを調べる

変数の値が複数候補のどれと等しいか調べることがあります。この場合、`or`演算子使って以下のように書くこともできますが、パイソニックではありません。

```python:パイソニックでない例
if fruits == "banana" or fruits == "apple" or fruits == "orange": # <- # "fruits ==" を複数記述する必要があり冗長 
```

そこで、以下のように比較対象の値をタプルにまとめ、そのタプル内に変数の値が存在するかどうかを`in`演算子でチェックするのがパイソニックです。

```python:パイソニックな例
fruits = "banana"
print(fruits in ("apple", "orange", "banana"))

#...出力結果...
True
```

# さいごに

本書での学習を通じて今まで曖昧だった使い方をしっかり理解することができました。
本書では他にも、

- Pythonのよくある落とし穴
- Pythonの要注意コード
- 良い関数の書き方

など、Pythonの基礎的な使い方をある程度理解した人が中級者へステップアップするためのTipsを学ぶことができると思います。
一度手にとって読んでみることをお勧めします！