Pythonの達人を目指して～Effective Pythonで学ぶコーディングテクニック～（前編）

はじめまして、中山と申します。<br>
業務ではディープラーニングのモデルを主にPythonを使って実装しています。

先日、別のメンバーが [「ロバストPython」の勉強会についての記事](https://qiita.com/NSS_FS_ENG/items/1f4243f83e5a7fa8cf37) を投稿をしましたが、<br>
それとは別に、[Effective Python 第2版](https://www.oreilly.co.jp/books/9784873119175/)という本の勉強会を行っておりました。

この記事では、この本から勉強したPythonのコーディングテクニックのうち、本の前半（1～5章）で取り上げられているものをいくつか紹介します。<br>
理解しやすく、割とすぐに使えそうなものをピックアップしました。<br>
もちろん本を全部読んで理解するに越したことはありませんが、ここで紹介したことを知るだけでも、Pythonの実装スキルがだいぶ上がるのではないかと思います。

# Effective Python（第2版） について
JavaやC++のプログラムを書いている方は、[Effective Java](https://www.maruzen-publishing.co.jp/item/?book_no=303054)や[Effective C++](https://www.maruzen-publishing.co.jp/item/b294734.html)という本をご存じの方もいらっしゃるかと思いますが、<br>
Effective Pythonも同様の位置づけで、基本的な文法・書き方を理解した人が、さらにレベルアップするためのテクニックが90個紹介されています。<br>
決して入門書ではないので、やや難しい内容もありますが、Pythonを仕事で扱うなら一度は読んでおくべき本ではないでしょうか。

# コーディングテクニックの紹介

ここからは、具体的なテクニックを紹介していきます。

見出しの「（項目xx）」は、それぞれのテクニックが紹介されている Effective Python 第2版の項目番号を表しています。<br>
以下で紹介するコードは、Python 3.11.11で動作確認しています。

## 1. enumerate（項目7）
リストなどをループでまわして処理するとき、次のように、インデックスと値の両方が欲しいということがよくあります。このようなケースで`enumerate`は有効です。

```python
names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
for i in range(len(names)):
    print(f'{i+1}: {names[i]}')
```
と書く代わりに、
```python
names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
for i, name in enumerate(names):
    print(f'{i+1}: {name}')
```
と書くことができます。

ほかのプログラミング言語でもぜひ欲しい機能ですね。

## 2. zip（項目8）
1回のループ内で複数のリストを並列に扱いたいことがよくあると思います。そんなときに`zip`が役に立ちます。

たとえば、
```python
names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
scores = [90, 65, 70, 85, 80]  # 各人のテストの点数
```
という2つのリストがあって、1行に一人の名前と点数を出力したいとき、
```python
for i, name in enumerate(names):
    score = scores[i]
    print(f'{name}: {score}')
```
と書くよりは、`zip`を使って、
```python
for name, score in zip(names, scores):
    print(f'{name}: {score}')
```
と書いたほうが簡単です。

:::note info
補足
Python 3.10から`strict`引数が追加され、これを`True`にすると、要素数が異なるときに`ValueError`が発出されます。<br>
上記の例は、
```python
for name, score in zip(names, scores, strict=True):
    print(f'{name}: {score}')
```
と書くのがよさそうです。
:::

## 3. catch-allアンパック（項目13）
```python
test_score = ('Alice', 90)
```
というタプルがあるときに、次のように書くと、タプルの要素を別々の変数に格納することができます。これを **アンパック** と呼びます。
```python
name, score = test_score  # name = 'Alice', score = 90
```

**catch-allアンパック** は、これの発展形で、「一部と残り」という形でアンパックできます。

たとえば、名前のリストを並べ替えて、最初と最後だけ取り出したいときに、catch-allアンパックを使って次のように書くことができます。
```python
name_list = ['Eve', 'Bob', 'Dave', 'Alice', 'Carol']
first, *other, last = sorted(name_list)   # first = 'Alice', other = ['Bob', 'Carol', 'Dave'], last = 'Eve'
print(f'{first} | {last}')   # 出力： Alice | Eve
```

## 4. defaultdict（項目17）
辞書を使って「ある文書内の単語の出現頻度を調べる」のようなことをする場合に、
```python
word_count_dict = {}
for word in text:
    if word in word_count_dict:  # "word"のキーがあるかどうか調べる
        word_count_dict[word] += 1  # すでに"word"のキーがあれば、キーの値に1を加算したものを代入する
    else:
        word_count_dict[word] = 1  # "word"のキーがなければ、新しくキーを作って、1を代入する
```
のように、辞書にキーがすでにあるかないかで処理を分ける必要がでてきます。<br>

このようなとき、defaultdictにしておくと、
```python
word_count_dict = defaultdict(0)  # この0がデフォルト値
for word in text:
    word_count_dict[word] += 1  # "word"のキーがあればキーの値に1を加算、なければデフォルト値0に1を加算して代入
```
というように、キーが辞書にない場合を考慮せずに書くことができます。

（なお、Pythonには、このようなユースケースで使える `Counter` という便利なクラスが存在します。）

## 5. 位置引数とキーワード引数（項目22～25）

### 位置引数とは、キーワード引数とは
Pythonの関数の引数には、 **位置引数** と **キーワード引数** があります。<br>
ほかのプログラミング言語ではなかなか見かけない、かなりユニークな特徴ではないでしょうか。

位置引数は、決められた順番にしたがって引数を渡します。多くのプログラミング言語がこの形ではないかと思います。<br>
一方、キーワード引数は、`name=value`のように、名前と値をセットで引数を渡します。

たとえば、次のような関数があったとき、
```python
def func(arg1, arg2, arg3=10) :
    ...
```

呼び出す側は、引数を位置引数として渡すことも、キーワード引数として渡すこともできます。組み合わせもOKです。<br>
キーワード引数の場合は、引数の順番は問われません。
```python
func(1, 2, 3)  # 位置引数として渡す (arg1=1, arg2=2, arg3=3)
func(arg3=5, arg2=2, arg1=0)  # キーワード引数として渡す (arg1=0, arg2=2, arg3=5)
func(0, arg3=5, arg2=2)  # 位置引数とキーワード引数の組み合わせ (arg1=0, arg2=2, arg3=5)
```

キーワード引数は辞書型で渡すこともできます。
```python
args = {'arg1':0, 'arg2':2, 'arg3':5}
func(**args)  # ** で辞書型をキーワード引数にアンパックしている
```

:::note info
補足
辞書型でキーワード引数を渡す場合、辞書に余計なキーワードを入れることはできません。
```python
args = {'arg1':0, 'arg2':2, 'arg3':5, 'arg4':10}
func(**args)  # 'arg4'が余計なのでエラー
```
:::

### *args, **kwargs
Pythonのコードを読んでいると、`func(*args, **kwargs)`というスタイルの関数を良く見かけます。

これは、位置引数として渡された引数は`args`に、キーワード引数として渡された引数は`kwargs`に格納されることを意味します。

次のコードでは、`func`関数が呼ばれたとき、`args`引数に`[1, 2]`、`kwargs`引数に`{'kwarg1':10, 'kwarg2': 20}`が格納されます。
```python
def func(*args, **kwargs):
    print('args = ', args)
    print('kwargs = ', kwargs)
func(1, 2, kwarg1=10, kwarg2=20)
```

### 位置専用引数、キーワード専用引数

関数定義で引数に`/`や`*`を入れることで、引数の渡し方に制限をかけることができます。

 * `/`よりも前の引数は、位置引数としてのみ受け付ける（**位置専用引数**）
 * `*`よりも後ろの引数は、キーワード専用引数としてのみ受け付ける（**キーワード専用引数**）

位置専用引数はなかなか使い道が思いつかないですが、キーワード専用引数は、どの引数を何番目に指定するのかを関数の利用者に意識させずに済み、混乱を減らすのに役に立ちそうです。

次の例では、`arg1`が位置専用引数、`arg3`がキーワード専用引数です。`arg2`はどちらでも渡せます。
```python
def func(arg1, /, arg2, *, arg3):
    # 関数の中身は省略

func(1, 2, arg3=3)
func(1, arg2=2, arg3=3)
func(1, 2, 3)  # 'arg3'はキーワード専用引数で、位置引数として渡すことができないため、エラー
func(arg1=1, arg2=2, arg3=3)  # 'arg1'は位置専用引数で、キーワード引数として渡すことができないため、エラー
```

## 6. 内包表記（項目27）
こちらもPython独特の書き方で、最初は非常にとっつきづらいと思います（私もそうでした）が、慣れるとコードが短くて読みやすくて便利な文法です。
C#がわかる方は、LINQのクエリ構文を思い出していただけると、イメージがわくと思います。

たとえば、ある数値のリストが与えられて、「そのリストから奇数だけ取り出して2倍したリストを作る」という操作したいとき、愚直に書くと次のようなコードになりますが、
```python
value_list = [32, 17, 23, 6, 45, 10]
new_list = []
for value in value_list:
    if value % 2 != 0:
        new_list.append(value * 2)
```

内包表記を使うと次のように書けます。
```python
value_list = [32, 17, 23, 6, 45, 10]
new_list = [value * 2 for value in value_list if value % 2 != 0]
```

セットや辞書でも同じように書けます。
「ある辞書の値が100以上のものを取り出して新しい辞書に格納する」ということをやるには、次のように書きます。
```python
new_dict = {key: value for key, value in org_dict.items() if value >= 100}
```

ただし、本にも項目28に書かれていますが、過度な多用は禁物です。
内包表記の中にさらに内包表記を入れ子にすると、一気にコードが読みにくくなります。

:::note info
追加テクニック
ちょっとトリッキーで読みづらい書き方ですが、内包表記でこんなコードを書けます。
```python
names = ['Alice', 'Bob', 'Carol']
combinations = [(x, y) for x in names for y in names if x != y]
print(combinations)
```
上記のコードの実行結果は以下です。
```
[('Alice', 'Bob'), ('Alice', 'Carol'), ('Bob', 'Alice'), ('Bob', 'Carol'), ('Carol', 'Alice'), ('Carol', 'Bob')]
```
:::

## 7. ジェネレータ（項目30）
たとえば、ある数値データをファイルから読んで返すとき、次のような実装が考えられます。
```python
def load_value(path):
    value_list = []
    with open(path, 'r') as f:
        for line in f:
            value_list.append(int(f))
    return value_list

# load_value関数を使って1個ずつ値を処理する
for value in load_value(file_path):
    # なんらかの処理
```

この実装では、読んだデータを全部リストに格納して返します。
データの個数が数十個や数百個ならあまり気にならないかもしれませんが、数億とか数十億とかいう単位になると、それがすべてメモリに載らないかもしれません。

そんなときに、ジェネレータを使ってデータを1個ずつ読んで返すことで、メモリ効率を上げることができます。<br>
C#にも `yield return` がありますね。
```python
def load_value_gen(path):
    value_list = []
    with open(path, 'r') as f:
        for line in f:
            yield int(line)
```

呼び出し方法は load_value のときと変わりません。
```python
# load_value_gen関数を使って1個ずつ値を処理する
for value in load_value_gen(file_path):
    # なんらかの処理
```

## 8. __call__ (項目38)

特殊メソッド`__call__`を定義すると、インスタンスを呼び出し可能にすることができます。<br>
ディープラーニング関係のPythonのコードでは、forward処理（データをモデルに渡して出力を得るところ）で割と良く見かけるやり方です。

```python
class Counter():
    def __init__(self):
        self.count = 0
    
    def __call__(self):
        self.count += 1

    def __repr__(self):
        return str(self.count)

counter = Counter()
counter()   # インスタンスを関数のように呼び出し
counter()
counter()
print(counter)  # 出力： 3
```

# まとめ

ここでは、Effective Python 第2版の前半（1～5章）で触れられているテクニックを8個紹介しました。<br>
後半（6～10章）については、改めて別の記事で紹介したいと思います。