【Python小技】マイナーだけど便利なPythonの小技5つ

自分があんまり見たことないPythonのコーディングテクニックを紹介しようと思います。予想外にメジャーだったらごめんなさい。出来るだけサンプルコードを載せて、実際にどう役に立つのかも示したいと思います。ニッチな技術をまとめているので、ある程度Pythonが書けることが前提です。

#`functools.partial`
これを使うと、ある関数について、**引数を固定した関数を作る**ことができます。コードを見た方が早いと思います。

```python
import functools

def add(x, y):
    return x + y

add_fixed_y = functools.partial(add, y=5)
print(add_fixed_y(2)) # 7
```

これだけだと何に役立つかわからないと思うので、自分の使い道を紹介したいと思います。自分はファイルの**`open`関数**に使っています。

```python
open_utf_8 = functools.partial(open, encoding="utf-8")
r_open_utf_8 = functools.partial(open_utf_8, mode="r")
w_open_utf_8 = functools.partial(open_utf_8, mode="w")

with w_open_utf_8("something_great.txt") as wf:
    wf.write("Hello World!")
```

こうすることで、コード内から定数を減らすことができ、`encoding`の部分のタイポを防ぐことができます。

# `next`とジェネレータ
リスト内包表記の内側の式はジェネレータ式に使えて、ジェネレータオブジェクトを生成するというのは他の記事でも紹介されています。

```python
gen = (i ** 2 for i in range(10))
print(gen) # <generator object <genexpr> at 0x~~>
```

これ、実際のコーディングではどう役に立つのでしょうか。自分は、**条件を満たす最初のインデックス**を探すのに使っています。**文字列を指定バイト数以下に丸める**という処理に使っています。

```python
from itertools import accumulate

def str_clamp_bytes(value: str, max_bytes: int) -> str:
    byte_count = (len(s.encode("utf-8")) for s in value)
    try:
        end_slice = next(i for i, v in enumerate(accumulate(byte_count)) if v > max_bytes)
    except StopIteration:
        return value
    return value[:end_slice]
```

`(i for i, v in enumerate(accumulate(byte_count)) if v > max_bytes)`がジェネレータで、`if v > max_bytes`が条件式です。`end_slice`には`v > max_bytes`を満たす最初のインデックスが代入されます。もし、**条件を満たすインデックスが無かった場合は、`StopIteration`例外が発生する**のでキャッチして元の文字列を返してあげます。`byte_count`は`accumulate`に使うだけなのでジェネレータで十分です。

## 【2021/02/16追記】`next`関数について補足
ドキュメントを読んでいたら新しい情報を手に入れたので補足しておきます。
**`next`関数は第二引数に`StopIteration`例外時のデフォルト値をとります**。なので上記のコードは次のように、より簡潔に書くことができます。

```python
def str_clamp_bytes(value: str, max_bytes: int) -> str:
    byte_count = (len(s.encode("utf-8")) for s in value)
    gen_first_index = (i for i, v in enumerate(accumulate(byte_count)) if v > max_bytes)
    end_slice = next(gen_first_index, None)
    return value[:end_slice]
```

スライスのうしろ側に`None`を渡すと文字列の最後まで取得することができます。

# グローバル変数`__debug__`
起動時に`-O`(not zero)オプションをつけると`False`になる変数です。`-O`オプションをつけると`assert`文が無効になりプログラムの高速化が狙えるので、製品版ではつけても問題ないでしょう。自分は**製品版の時のみ配信をする**という処理に使っています。

```python
def broadcast_debug():
    pass

def broadcast_prod():
    pass

(broadcast_debug if __debug__ else broadcast_prod)()
```

# `lru_cache`デコレータ
関数の引数と戻り値のペアを辞書で覚えておくことで、以前渡された引数なら処理を行わずに以前の戻り値を返します。

```python
from functools import lru_cache

@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(10))
```

辞書なのでアクセス速度は純粋なメモ化には劣りますが、手軽さは素晴らしいです。あとは、メモ化では対応できない**文字列などが引数になった場合**でも対応できます。

# `inspect.getmembers`
**モジュールからすべてのメンバを取得**できます。第二引数でメンバのタイプを指定することができます。

```python
import inspect
import copy

print(inspect.getmembers(copy, inspect.isclass))
# >> [('Error', <class 'copy.Error'>), ('error', <class 'copy.Error'>)]
```

**モジュールからクラスだけを全て抽出できる**ということは、`loaded_classes = [class_A, class_B, ...]`なんて面倒くさいことやらなくても、一気に取得できます。しかも、追加し忘れる心配がありません。クラスオブジェクトには`__bases__`メンバがあり、継承しているすべてのクラスオブジェクトがタプルとして入っているので、この辺を使えばフィルタ出来ると思います。

# 最後に
自分が「便利だけどあんまり見ないなぁ」と思う小技を紹介してみました。タイポや間違いがあったら是非教えてください。