Effective Pythonまとめ 1. Pythonic Thinking (Item1~Item10)

Pythonのコーディング力を引き上げるために、[Effective Python Second Edition（英語版）](https://www.amazon.co.jp/dp/B07ZG18BH3/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)を用いて学習しています。
（日本語版は訳が機械的でわかりづらいというレビューが多かったので、英語版を利用することにしました。）
今回は、第一章のPythonic Thinking (Item1~Item10まで)について、学習の備忘も兼ねて要点をメモしていきます。

:::note warn
個人的に解釈した内容を記述しているため、本の内容と厳密には一致しない表現が含まれる可能性があります。
:::

#  Item1: Know which version of Python You're Using
pythonのバージョンを確認する方法は2種類ある。

### `--version`フラグによる確認

```
$ python --version
Python 2.7.16

$ python3 --version
Python 3.8.2

```

### `sys`モジュールによる確認

```
>>> import sys
>>> print(sys.version_info)
sys.version_info(major=3, minor=8, micro=2, releaselevel='final', serial=0)
>>> print(sys.version)
3.8.2 (default, Jun  8 2021, 11:59:35) 
[Clang 12.0.5 (clang-1205.0.22.11)]
```

python2系は2020年1月1日にEOLを迎え、公式なメンテナンスが終了する。


#  Item2: Follow the PEP 8 Style Guide 
[PEP8](https://pep8-ja.readthedocs.io/ja/latest/#)のコード規約に従った一貫性のあるコードは、コードを扱いやすくしたり、可読性を向上させる。
他のプログラマーと協働しやすくなるだけでなく、自分自身が後でコードに変更を加えたり、エラーを回避するためにも役立つ。

### 空白
エディターのフォーマッターで自動整形できるので割愛

### ネーミング
既知の内容だったため割愛

### 式と文
- 式全体を否定する`if not a is b`の代わりに、インライン否定`if a is not b`を使う
- 空のコンテナや文字列の判定を`if len(somelist) == 0`のように長さで判定せず、`if not somelist`のように判定する
- 空でないコンテナや文字列の判定も同様
- if文、forやwhileループ、exceptの複合文を一行で書かず、複数行に分ける
- if文の式が1行で収まらない場合、式全体を括弧で括り、改行とインデントを追加する
    
```python
if (this_is_one_thing
    and that_is_another_thing):
do_something()
```

### インポート
- 現モジュールのパスからの相対名でインポート(例：`import foo`)するのではなく、常に絶対名(例：`from bar import foo`)でインポートする
- 相対インポートをしなければならない場合にも、明示的な構文を利用する(例：`from . import foo`)
- 標準ライブラリモジュール、サードパーティモジュール、独自モジュールの順番でセクションを分けて、各セクション内ではアルファベット順で記述する
	
# Item3: Know the Differences Between bytes and str

- bytesは8ビット値の列が含まれて、strはUnicode文字列を含み、それぞれのインスタンスは>, ==, +, %などの演算子で一緒に使えない
- 操作しているインプットが期待するシーケンスであること（8ビット値なのか、UTF8にエンコードされた文字列なのか、Unicodeなのか等）を保証するために、ヘルパー関数を利用する
- バイナリデータを読み書きする際には常にバイナリモード(読み`rb`、書き`wb`)を使ってファイルを開く
- Unicodeデータを読み書きする際にはシステムのデフォルトエンコーディングに注意し、意図しないエンコーディングを避けるために明示的に`encoding`パラメータを渡すことも検討する

# Item4: Prefer Interpolated F-Strings Over C-style Format Strings and str.format
python3.6で追加された、interpolated format strings(f-strings)は、表現のしやすさ、簡潔さ、明快さの組み合わせによって、組み込みの選択肢の中でベストと言える。

### F-strings, C-Style, str.formatの記述量の比較
それぞれを記述量の短い順に並べると以下のようになり、f_stringが最短となる。

```python
key = 'my_var'
value = 1.234

f_string = f'{key:<10} = {value:.2f}'
c_tuple = '%-10s = %.2f' % (key, value)
str_args = '{:<10} = {:.2f}'.format(key, value)
str_kw = '{key:<10} = {value:.2f}'.format(key=key, value=value)
c_dict = '%(key)-10s = %(value).2f' % {'key': key, 'value': value}

assert f_string == c_dict == c_tuple == str_args == str_kw
```

### 表現のしやすさの比較
F-Stringsはプレースホルダー内に、Pythonの式を自由に入れることができる。
それにより、他の方法では複数行にまたがった記述になるところが、F-Stringsでは一行で収まる。

```python
pantry = [
    ('avocados', 1.25),
    ('bananas', 2.5),
    ('cherries', 15),
]

for i, (item, count) in enumerate(pantry):
    c_style = '#%d: %-10s = %d' % (
        i + 1,
        item.title(),
        round(count)
    )
    
    format_style = '#{}: {:<10s} = {}'.format(
        i + 1,
        item.title(),
        round(count)
    )
    
    f_string = f'#{i+1}: {item.title():<10s} = {round(count)}'

    assert c_style == format_style == f_string
```



# Item5: Write Helper Functions Instead of Complex Expressions

複雑な式、特に同じロジックを繰り返す必要がある場合には、ヘルパー関数に式を移すことを検討する。
可読性によって得られるものは、常に簡潔さによって得られるものよりも重要である。


例えば、以下のような要件の処理を行いたい場合、

- 値にリスト型のデータを持つ、辞書型のデータ（例：`my_values = {'red': ['5'], 'green': [''], 'blue': ['0']}`）からリストの一つ目の値を取得したい
- 値がなかったり、空白の場合には0を返したい
- 常に整数であることを保証したい

1行で書く場合、以下のように記述することができる。

```python
red = int(my_values.get('red', [''])[0] or 0)
```
または、複雑さを軽減するために、2行で次のように書くこともできる。

```python
red_str = my_values.get('red', [''])
red = int(red_str[0]) if red_str[0] else 0
```

まだ読みにくいため、if/else条件式を利用して書き換えるとより明快になる。

```python
red = my_values.get('red', [''])
if red[0]:
    red = int(red[0])
else:
    red = 0
```

そして、このようなロジックを繰り返し使う必要がある場合には、ヘルパー関数の利用を検討する。

```python
def get_first_int(values, key, default=0):
    found = values.get(key, [''])
    if found[0]:
        found = int(found[0])
    else:
        found = default
    return found


get_first_int(my_values, 'red')
```
呼び出し時のコードは、1行や2行で書いたコードと比べてずっと明確になる。

# Item6: Prefer Multiple Assignment Unpacking Over Indexing

pythonでは、リストやシーケンス、iterables内の複数レベルの任意のiterablesにおいて、複数の値を一行で代入すること(unpacking)ができる。

```python
favorite_snacks = {
    'salty': ('pretzels', 100),
    'sweet': ('cookies', 180),
    'veggie': ('carrots', 20),
}

((type1, (name1, cals1)),
 (type2, (name2, cals2)),
 (type3, (name3, cals3))) = favorite_snacks.items()
```

通常、一時的な変数を用意して順番に代入していく必要のあるindexの入れ替えもunpackingを利用すると一行で行える。

```python
def bubble_sort(a):
    for _ in range(len(a)):
        for i in range(1, len(a)):
            if a[i] < a[i-1]:
                a[i-1], a[i] = a[i], a[i-1]


names = ['pretzels', 'carrot', 'arugula', 'bacon']
bubble_sort(names)
print(names) # ['arugula', 'bacon', 'carrot', 'pretzels']
```
この方法は内包表記や、generatorの値を変数に代入する際にも利用できる。

# Item7: Prefer enumerate Over range

組み込み関数enumerateは与えられたイテレーターから、インデックスと値のペアを生成する。
第二引数に最初のインデックスを指定することもできる。(デフォルトは`0`)

```python
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i, flavor in enumerate(flavor_list, 1):
    print(f'{i}: {flavor}')
```
これにより、組み込み関数rangeにイテレーターの長さを渡してループを回すよりもずっと簡潔に書くことできる。

# Item8: Use zip to Process Iterators in Parallel

組み込み関数zipを利用することで、同時に複数のイテレーターをループすることができる。
また、組み込み関数zipはtupleを生成するlazy generatorを生成するため、長いインプットにも対応できる。

```python
names = ['Cecillia', 'Lise', 'Marie']
counts = [len(n) for n in names]
longest_name = None
max_count = 0

for name, count in zip(names, counts):
    if count > max_count:
        longest_name = name
        max_count = count

print(longest_name, max_count) # Cecillia 8

```

ただし、zip関数の仕様上、ループするイテレーターの長さが異なる場合、短い方のイテレーターの長さ分でtupleの生成が終了し、長い方の残りのtupleは切り捨てられる。
長さの違うイテレーターで切り捨てせずにzip関数を利用したい場合は、組み込みモジュールのitertoolsの`zip_longest`関数を利用する。


# Item9: Avoid else Blocks After for and while Loops
if/elseやtry/except/elseの感覚で考えると、`ループが完了しなければこれを実行しなさい`だと思うが、実際には逆である。
実際には、forやwhileループの後のelseは、`ループ中にbreak文が実行されなかった場合`にのみ実行される。

また、空のリストや`while False`でループを回しても、elseブロックは実行される。

```python
for x in []:
    print('Never runs')
else:
    print('For Else block!')  # For Else block!
```

```python
while False:
    print('Never runs')
else:
    print('For Else block!')  # For Else block!
```

振る舞いが直感的でなく誤解を生みやすいので、利用を避けるべきである。
elseを利用せずに、同様のことを実行したい場合、helper関数を利用する。

# Item10: Prevent Repetition with Assignment Expressions


python3.8で導入された`:=`（walrus operator）を使えば、式の途中結果を名前にひもづける（≒代入する）ことができる。
`:=`がセイウチのように見えるためこのような名前が付けられており、`a := b`を（a walrus b）と読む。

以下のような、条件式に利用した値を条件式内でも利用したい場合などに、繰り返しを減らし可読性を向上させることができる。

```python
def make_lemonade(count):
    ...


def out_of_stock():
    ...


fresh_fruit = {
    'apple': 10,
    'banana': 8,
    'lemon': 5,
}
if count := fresh_fruit.get('lemon', 0):
    make_lemonade(count)
else:
    out_of_stock()
```

walrus operatorは代入をしてから、評価を行うという順序で処理される。
そのため、例えば　`if (count := fresh_fruit.get('apple', 0)) >= 4: `のような式の場合には、左側の式を括弧で囲む必要がある。

また、pythonでは、switch/case文が利用できないため、以下のようなif/else文をを利用した深いネストを記述することがよくある。

```python
count = fresh_fruit.get('banana', 0)
if count >= 2:
    pieces = slice_bananas(count)
    to_enjoy = make_smoothies(pieces)
else:
    count = fresh_fruit.get('lemon', 0)
    if count:
        to_enjoy = make_lemonade(count)
    else:
        to_enjoy = 'Nothing'
```

このような場合にもwarlus operatorを利用することでネストを浅くすることができる。

```python
if (count := fresh_fruit.get('banana', 0)) >= 2:
    pieces = slice_bananas(count)
    to_enjoy = make_smoothies(pieces)
elif (count := fresh_fruit.get('apple', 0)) >= 4:
    to_enjoy = make_cider(count)
elif count := fresh_fruit.get('lemon', 0):
    to_enjoy = make_lemonade(count)
else:
    to_enjoy = 'Nothing'
```

また、同様にpythonではdo/while文が利用できないため、くどい記述を余儀なくされることがあるが、このような場合にも上手にwarlus operatorを活用することで可読性を向上させることができる。


***


参考文献
[Effective Python: 90 Specific Ways to Write Better Python (Effective Software Development Series) (English Edition)](https://www.amazon.co.jp/dp/B07ZG18BH3/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)