Pythonユーザー必見！知らなきゃ損する小ネタ集



# はじめに

Python のあまり知られていない便利な小ネタを筆者の独断と偏見でとりとめなくまとめました。標準だけどドキュメントを隅々まで読まないと見落としてしまうような機能を、周辺知識にも触れながら紹介していきます。

ちなみに記事のタイトルは ChatGPT 先生に考えてもらいました。知らなくても特に損はしないと思います。

:::note info
本記事の内容は Python 3.8 以降を想定しています。また、3.9 以降で新しく追加された機能については、追加されたバージョンを記載しています。
:::


# スワップ

変数のスワップには専用の記法があります。

```python
a = 1
b = 2
a, b = b, a
print(a, b)  # 2 1
```

余計なタプルが作られそうな見た目をしていますが、実際は下記の動作に置き換わるため、パフォーマンスの心配はありません。

1. 右辺の変数をスタックに一つずつロードする
2. スタックを [スワップ](https://docs.python.org/ja/3.11/library/dis.html#opcode-SWAP)（または [ローテーション](https://docs.python.org/ja/3.10/library/dis.html#opcode-ROT_TWO)）する
3. スタックから左辺の変数に一つずつストアする

3つ組でも同様です（4以上は見た目通りにタプルが作られます）。

```python
# 下記どれでもOK
a, b, c = c, b, a
a, b, c = b, c, a
a, b, c = c, a, b
```

スタックのスワップなので、右辺は演算結果でもOKです。

```python
a = 1
b = 2
a, b = b + 10, a + 10
print(a, b)  # 12 11
```

辞書やリストに対しても使えます。

```python
d = {"x": 1, "y": 2}
d["x"], d["y"] = d["y"], d["x"]
print(d)  # {'x': 2, 'y': 1}

a = [1, 2]
a[0], a[1] = a[1], a[0]
print(a)  # [2, 1]
```

スライスでも使えますが、numpy などのスライスがビューを返すケースでは注意が必要です。

```python
a = list(range(10))
a[2:4], a[6:8] = a[6:8], a[2:4]
print(a)
# [0, 1, 6, 7, 4, 5, 2, 3, 8, 9]

a = np.arange(10)
a[2:4], a[6:8] = a[6:8], a[2:4]
print(a.tolist())
# [0, 1, 6, 7, 4, 5, 6, 7, 8, 9]
#                    ^  ^
```

また、長さが違うスライスのスワップもできますが、直感に反する挙動になるのでお勧めしません。

```python
a = list(range(10))
a[1:2], a[4:8] = a[4:8], a[1:2]
print(a)
# [0, 4, 5, 6, 1, 5, 6, 7, 8, 9]
```

これは下記と等価です。

```python
a = list(range(10))
t1 = a[1:2]
t2 = a[4:8]
a[1:2] = t2  # a = [0, 4, 5, 6, 7, 2, 3, 4, 5, 6, 7, 8, 9]
a[4:8] = t1  # a = [0, 4, 5, 6, 1, 5, 6, 7, 8, 9]
```

# 連鎖比較

比較演算子は連鎖できます。

```python
if 0 <= left < right < len(arr):
    ...
```

これは下記と等価です。

```python
if 0 <= left and left < right and right < len(arr):
    ...
```

あくまでも隣接する比較が `and` で連結されるだけで、全体として評価される訳ではないことに注意です。

```python
start, left, right, end = 0, 7, 2, 3
print(start < left < right < end)  # False
print(start < left != right < end)  # True
# (start < left) and (left != right) and (right < end) であり、
# (start < left < end) and (start < right < end) and (left != right) ではない。
```

連鎖と明示的 `and` の最大の違いは、連鎖すると各オペランドが1回しか評価されない点です。つまり、上記の `and` を使う例では `left`、`right` がそれぞれ2回ずつロードされるのに対して、連鎖するとロード回数は1回ずつになります。

この特性は、何らかの計算結果を比較したい時に特に有効です。

```python
if 0 <= (left - margin) < (right + margin) < len(arr):
    ...
```


# in range

`in` 演算子は `range` オブジェクトにも使えます。

```python
target_range = range(len(arr))
if i in target_range:
    ...
```

これは下記と等価であり、`O(1)` の処理です。

```python
if 0 <= i < len(arr):
    ...
```

ただし、`range` オブジェクトの生成自体のオーバーヘッドはあるので注意です。

```python
for i in range(n):
    if i in range(len(arr)):  # これは range オブジェクトが毎回作られる。
        ...
```

つまり、チェックしたい範囲が固定な時に有効ですね。これはチェックしたい範囲が複雑な時に使うと可読性に貢献すると思います。

```python
for x, y in points:
    if (x in a_range or y in b_range) and (x not in c_range and y not in d_range):
        ...
```

# range のスライス

`range(10)` の逆順の `range` をそらで書けますか？

正解は↓

```python
list(range(9, -1, -1))
# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
```

書けたあなたは `range` マスターです。えらい！

書けなかったあなたに朗報です。`range` オブジェクトはスライスできます。

```python
range(10)[::-1]
# range(9, -1, -1)
```

`range` オブジェクトが返ってくるので、繰り返しスライスすることもできます。

```python
range(100)[::-2][:10]
# range(99, 79, -2)
```

これを利用すればリストに対する複数回のスライス操作を1つにまとめるのも簡単です。

```python
r = range(len(arr))[::-2][:10]
arr[r.start:r.stop:r.step]
```


# イテレータのアンパック

タプルなどにパックされた要素を展開することをアンパックと言います。スワップ記法も論理的にはアンパックの一種と言えます。

```python
t = (1, 2)  # パッキング（packing）
a, b = t  # アンパッキング（unpacking）
print(a, b)  # 1 2
```

上記は要素数が固定のアンパックですが、要素数が不明な時はイテラブルアンパックが使えます。

```python
[*a] = range(5)
print(a)  # [0, 1, 2, 3, 4]
```

これは下記と等価です。

```python
a = list(range(5))
```

ちなみに上記ではリスト `[]` 形式で受け取っていますが、左辺はリストでもタプルでもバイトコードは一緒になるので好きな方が使えます。個人的には、1要素の時は末尾 `,` の有無で挙動が変わるのは嫌なのでリストを、複数ある時は括弧を省略したいのでタプルを使っています。

さて、ここで紹介したいのが、イテレータの固定長アンパックというテクニックです。

```python
from pathlib import Path

[path] = Path("log").glob("*.txt")
```

これは `log` ディレクトリ直下に該当するファイルが 1 個だけあることを要求し、0 個または 2 個以上あると例外になります。つまり下記と等価です。

```python
path_iterator = log_dir.glob("*.txt")
try:
    path = next(path_iterator)
except StopIteration:
    raise ValueError("not enough values to unpack")
try:
    _ = next(path_iterator)
    raise ValueError("too many values to unpack")
except StopIteration:
    pass
```

例えば「ファイルがそこにあることが分かっているけど、タイムスタンプ等が付いていてファイル名が固定ではない時」などで有効です。

他にも下記のようなケースもあります。

```python
text = "20240101_123456_00001"
date, time, number = text.split("_")
```

正規表現で実装するならこんな感じでしょうか。

```python
import re

text = "20240101_123456_00001"
match = re.match(r"(\d{8})_(\d{6})_(\d{5})", text)
assert match
date, time, number = match.groups()
```

正規表現の方が上記例のように厳格なチェックができますが、`str.split` で十分なケースも多々あるはずです。


# アンパックのネスト

アンパックはネストできます。

```python
t1 = (1, 2)
t2 = (3, 4)
t = (t1, t2)

(a, b), (c, d) = t  # ネストされたアンパック
print(a, b, c, d)  # 1 2 3 4
```

よく使うのはループ変数のアンパックです。

```python
d = {"a": 1, "b": 2, "c": 3}

for i, (key, value) in enumerate(d.items()):
    ...
```

```python
xy_list1 = [(1, 1) for _ in range(5)]
xy_list2 = [(1, 1) for _ in range(5)]

for (x1, y1), (x2, y2) in zip(xy_list1, xy_list2):
    ...
```

イテラブルアンパックをネストすることもできます。

```python
from itertools import groupby

for key, [*group] in groupby(items, key=lambda e: e.key):
    ...
```

これは下記と等価です。

```python
for key, group in groupby(items, key=lambda e: e.key):
    group = list(group)
    ...
```

これは内包表記で書きたい時にたまに役立ちます。

```python
value_range_map = {
    key: (min(group), max(group))  # イテレータのままだと複数回イテレートできない。
    for key, [*group] in groupby(items, key=lambda e: e.key)
}
```

# アンパックによるマージ

アンパックはリストやタプルのマージにも使えます。

```python
a = [1, 2]
b = (3, 4)
c = {6, 5}  # set の順番は未定義（結果が入れ替わっていることに注目）
d = range(7, 9)
merged = [0, *a, *b, *c, *d, 9]
print(merged)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

型がバラバラですが変換等は不要なところに注目です。アンパックはイテラブルなら何にでも使えます。

内包表記でも同様に使えます。

```python
merged = [
    *[i for i in range(3)],
    *(i for i in range(3)),
    *{i for i in range(3)},
]
```

上記の例では結果はリストになりますが、タプルや set にすることもできます。

```python
merged = (*a, *b, *c)
merged = {*a, *b, *c}
```

辞書も同様にアンパックでマージできます。辞書の場合、キーが重複すると後（右）側で上書きになります。

```python
default_config = {"a": 1, "b": 2, "c": 3}
user_config = {"a": 10, "c": 20}
merged = {**default_config, **user_config}
print(merged)  # {'a': 10, 'b': 2, 'c': 20}
```

ちなみに、Python 3.9 以降なら `|` 演算子でも同じことができます。

```python
merged = default_config | user_config
```

アンパックが ugly だから導入されたらしいですが、アンパックの方が汎用性が高いのでアンパックだけ覚えておけば十分と思います。

辞書が巨大でコピーしたくないなら [ChainMap](https://docs.python.org/ja/3/library/collections.html#collections.ChainMap) も便利かもしれません。


# 空コンテナのアンパック

空のリストやタプルをアンパックしても何も起こりません。（エラーになりません）

```python
a = []
b = [0, *a, 1]
print(b)  # [0, 1]
```

これを利用して、[Dart の collection if](https://dart.dev/language/collections#control-flow-operators) のように条件を満たす時だけリストに追加する操作が書けます（お勧めはしません）。

```python
condition: bool = False
items = [
    1,
    *([2] if condition else []),
    3,
]
print(items)  # [1, 3]
```

これ自体は正直あんまり読みたくないコードですが、空コンテナのアンパック自体は、例えば次のようにインターフェースを少し拡張して定義すれば十分に実用的なテクニックです。

```python
def get_items(config) -> list[Item]:
    ...


items = [
    *get_items(config_a),
    *get_items(config_b),
]
```

# without with

Python で JSON などのファイルを読み書きする時、`with` 構文を使うと思います。

```python
import json

with open("a.json", mode="r") as fo:
    data = json.load(fo)
```

この書き方も間違ってはいないのですが、代わりに `pathlib` の [Path.read_text](https://docs.python.org/ja/3/library/pathlib.html#pathlib.Path.read_text) を使って一括読み込みにした方がコードは単純になります（`Path` は普段から使っている前提です）。

```python
import json
from pathlib import Path

path = Path("a.json")
data = json.loads(path.read_text())
```

テキスト読み込み以外の API も一式揃っています。

```python
path.read_text()    # mode='r'
path.read_bytes()   # mode='rb'
path.write_text()   # mode='w'
path.write_bytes()  # mode='wb'
```

これらの API も内部的には普通に `with` 構文使って読み書きしているだけで、いわば Python におけるファイル操作の高レベル API と言っても差し支えないでしょう。言い換えると `with` 構文はもはや低レベル API であり、具体的にはファイルストリームが必要な時しか使わないと思います。

ちなみに最初の例の `json.load`（ファイルオブジェクトを受け取る方）も内部的には [メモリ上に一括読み込み](https://github.com/python/cpython/blob/be88b3b996dd74a9c3e5caada59064ee1681a2b9/Lib/json/__init__.py#L293) しています。なので `json` モジュールの利用時に `with` 構文を使うのは無意味です。今まで気にしてこなかった方はこれを機に移行してはいかがでしょうか。

また、`open` は `mode` 引数次第で様々なオブジェクトを返しますが、ルートクラスである `IOBase` は [オブジェクトの削除時にファイルを閉じる](https://docs.python.org/ja/3/library/io.html#io.IOBase.__del__) ので、実は下記のように書くこともできます。

```python
data = json.load(open("a.json"))
```

ただし、Python 界隈では一般的にこのやり方は好まれていません。この理由はいくつかありますが、根本的にはPython の言語仕様として [オブジェクトが削除されるタイミングが未定義](https://docs.python.org/ja/3/reference/datamodel.html) であることに由来すると思います。CPython の実装仕様としては参照カウンタを使っているので予測可能な挙動をしますが、例えば [PyPy](https://doc.pypy.org/en/latest/cpython_differences.html) などの他のインタプリタでは挙動が異なってしまいます。


# ヒアドキュメント

docstring でお馴染みの `"""` または `'''` で囲った文字列は docstring 専用の構文ではなく、普通の文字列です。ただし、文字列の途中で改行もできる、いわゆるヒアドキュメントになります。

```python
CODE = """\
for i in range(10):
    if i % 2:
        print(i)
"""
print(CODE)
```

```none
for i in range(10):
   if i % 2:
       print(i)

```

上の例でも使っていますが、ヒアドキュメント内でも行末に `\` を追加すれば改行が消えます。

```python
text = """\
a\
b\
c\
"""
print(text)
```

```none
abc
```

ヒアドキュメントにはインデント（空白）もそのまま含まれてしまうので、関数やクラスメソッドの中では扱いにくいです。

```python
class Gen:
    def gen_code(self):
        code = """\
            for i in range(10):
                if i % 2:
                    print(i)
        """
        return code


print(Gen().gen_code())
```

```none
            for i in range(10):
                if i % 2:
                    print(i)
        
|<-------->| インデントの空白を含んでしまう
```

こういう時は `textwrap` の [dedent](https://docs.python.org/ja/3.12/library/textwrap.html#textwrap.dedent) を使うと、最初の行を基準にインデントが解除されます。

```python
from textwrap import dedent


class Gen:
    def gen_code(self):
        code = dedent(
            """\
            for i in range(10):
                if i % 2:
                    print(i)
            """
        )
        return code


print(Gen().gen_code())
```

```none
for i in range(10):
    if i % 2:
        print(i)

```

ちなみにヒアドキュメントも f-string にできます。

```python
class Gen:
    def gen_code(self, n):
        code = dedent(
            f"""\
            for i in range({n}):
                if i % 2:
                    print(i)
            """
        )
        return code
```

# r-string

正規表現でパターン文字列の先頭に `r` を付けているのを見たことがありませんか？

```python
import re

re.match(r"\d+", "123")
```

これは Python で正規表現（**r**egular expression）を書くための機能、ではありません。

正しくは **r**aw string と言って、エスケープシーケンス（`\n` など）を無視すること以外は普通の文字列です。正規表現以外でも使えますし、ヒアドキュメントや f-string と組み合わせたりもできます。

```python
text = rf"""# サンプルコード
with open("{input_path}", "rt") as fi, \
    open("{output_path}", "wt") as fo:
    for line in fi:
        fo.write(line)
        fo.write("\n")
"""
```

この機能が正規表現の次に輝くのが、Windows 環境でパスをコピペしてきた時です。

```python
path = r"C:\Users\nyancat\Documents\temp\x001.txt"
```

これは raw string にしないとパスとして機能しません。

```python
path = "C:\Users\nyancat\Documents\temp\x001.txt"
#         ^^    ^^                ^^   ^^^^
#         │     │                 │    └ ナル文字
#         │     │                 └ タブ文字
#         │     └ 改行文字
#         └ ユニコード文字（構文エラー）
```

私は書き捨てのスクリプトとかでよくコピペするんですが、これに気付くまでは毎回 `\` を `/` に置換してました……。

ただし raw string でも末尾を `\` で終わらせることはできないので注意です。

```python
path = r"C:\Users\"
# SyntaxError: unterminated string literal (detected at line 1)
```

これは、raw string は厳密には「エスケープシーケンスを無視する」ではなく「エスケープシーケンスをエスケープする」からです。例えば下記は構文エラーになりません。

```python
text = r"abc\"def"
```

エスケープシーケンスが無視されるなら真ん中の `\"` は「`\` に続く `"` の2文字」になり、トータルで `"` が3つある文字列（閉じていない文字列）になってしまいますが、そうはなりません。

これは「`\` に続く `"` の2文字」ではなく「`\"` というエスケープシーケンス」がエスケープされて格納されるので正しい構文です。逆に `\"` で終わっている時は文字列が閉じていないので構文エラーになるという理屈ですね。


# f-string のネスト

f-string のフォーマットはネストできます。

```python
print(f"{1:0{3}d}")  # 001
```

ファイル群を連番出力したい時に便利ですね。

```python
n_digits = len(str(len(images)))
for i, image in enumerate(images):
    cv2.imwrite(f"{i:0{n_digits}d}.jpg", image)
```

簡易的なテーブル出力とかにも使えます。

```python
table = [
    ["The Dark Knight", "July 2008"],
    ["Inception", "July 2010"],
    ["Interstellar", "November 2014"],
    ["Dunkirk", "July 2017"],
    ["Tenet", "August 2020"],
]

t_max_len, r_max_len = (max(len(cell) for cell in column) for column in zip(*table))
for t_cell, r_cell in table:
    print(f"| {t_cell:<{t_max_len}} | {r_cell:>{r_max_len}} |")
```

```none
| The Dark Knight |     July 2008 |
| Inception       |     July 2010 |
| Interstellar    | November 2014 |
| Dunkirk         |     July 2017 |
| Tenet           |   August 2020 |
```

# format template

[str.format](https://docs.python.org/ja/3/library/stdtypes.html#str.format) でも f-string のように名前付きプレースホルダーを使えます。

```python
formatted = "{key}: {value}".format(key="foo", value="bar")
```

この時、余分な引数を指定してもエラーになりません。

```python
formatted = "{key}: {value}".format(key="foo", value="bar", x="y")
```

これを利用すると、「ユーザーが任意のフォーマット文字列を指定できる機能」を簡易的に実現できます。

```python
params = {
    "title": "Last Night",
    "artist": "Morgan Wallen",
    "duration": "2:43",
    "released": "January 31, 2023",
    "year": "2023",
    "genre": "Country pop",
}

# どれを使うかはユーザーが決められる。一部しか使わなくてもOK。
formatted_title = user_template.format(**params)
```

dataclass やビルトイン関数の [vars](https://docs.python.org/ja/3/library/functions.html#vars) を上手く使うと辞書の作成もスキップできるかもしれません。

```python
from dataclasses import asdict, dataclass


@dataclass
class Item:
    x: int
    y: int


item = Item(1, 2)
print("x is {x}, y is {y}.".format(**asdict(item)))
```

```python
def func(x):
    y = x + 1
    print("x is {x}, y is {y}.".format(**vars()))  # 引数なしで呼び出すとローカル変数が取れる。
```

また、[str.format_map](https://docs.python.org/ja/3/library/stdtypes.html#str.format_map) の方を使えば遅延評価も可能です。

```python
class LazyMapping:
    def __getitem__(self, key):
        value = map_key_to_value(key)
        return value


"{foo} {bar}".format_map(LazyMapping())
```

# _

f-string のフォーマットに `,` を指定すると3桁区切りの `,` が追加されるのはご存じと思います。

```python
n = 1234567890
print(f"{n:,}")  # 1,234,567,890
```

しかし、Python は `,` 区切りの数値を「入力」として扱えないので、出力された文字列はそのままでは Python コードに入力できません。

```python
value = 1,234,567,890  # タプルになる

value = int("1,234,567,890")
# ValueError: invalid literal for int() with base 10: '1,234,567,890'
```

計算に時間のかかる定数を事前に計算して埋め込みたい時とか、一度ログ等で出力したものを読み込んで使いたい時とかにちょっと不便です。

こういう用途の場合、代わりに `_` 区切りのフォーマットを利用することができます。

```python
n = 1234567890
print(f"{n:_}")  # 1_234_567_890
```

`_` 区切りのフォーマットはそのまま入力として扱えます。

```python
value = 1_234_567_890  # OK
value = int("1_234_567_890")  # OK
```

ちなみに `_` 区切りのフォーマットは 10 進数以外にも使えます。むしろこちらが主用途でしょうか。

```python
n = 1234567890
print(f"{n:_b}")  # 100_1001_1001_0110_0000_0010_1101_0010
```

# コンパイル時定数

計算に時間のかかる定数を事前に計算して埋め込む話をしましたが、これは必ずしも必要なことではないので注意です。

例えば次のコードを見てください。

```python
ms = year * 365 * 24 * 60 * 60 * 1000
```

「年数をミリ秒換算に直す式」と一目で分かりますが、これだと見た目通り無駄な掛け算が4回発生してしまいます。これをループ内で繰り返し計算したり、`year` が巨大な DataFrame のカラムだったりすると無視できないコストになります。

こういう時、括弧を付けて定数部分を先に計算させると、コンパイル時に 1 回だけ計算され、バイトコードに定数が埋め込まれます。

```python
ms = year * (365 * 24 * 60 * 60 * 1000)
```

どのようなバイトコードが生成されるかは [dis](https://docs.python.org/ja/3/library/dis.html#dis.dis) モジュールを使って調べます。

```python
import dis

dis.dis("ms = year * (365 * 24 * 60 * 60 * 1000)")
```

```none
  1           2 LOAD_NAME                0 (year)
              4 LOAD_CONST               0 (31536000000)  <-- 計算されている
              6 BINARY_OP                5 (*)
             10 STORE_NAME               1 (ms)
```

この機能を知った時は「Python でコンパイル！？」と思いましたが、そもそもバイトコードにはコンパイルされるんだからこのくらいのことはやってくれても不思議ではなかったですね。

定数計算はイミュータブルなリテラルであれば様々なオブジェクトに対して行われます。

```python
def func(x):
    return x == (
        1.1 * 2.0,  # float の掛け算
        (3 % 4) / 5,  # 割り算
        (6 << 1) & 7,  # ビット演算
        (  # タプルのネスト
            "a" + "b",  # 文字列の結合
            b"c" + b"d",  # バイト列の結合
            (1, 2) + (3,),  # タプルの結合
        ),
    )
```

```none
  5           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 ((2.2, 0.6, 4, ('ab', b'cd', (1, 2, 3))))
              4 COMPARE_OP               2 (==)
              6 RETURN_VALUE
```

反対に対象外なのは、比較演算や `in` 演算、ミュータブルなオブジェクトや、`frozenset` などの関数呼び出しが必要なオブジェクトなどです。また、あくまでもリテラル同士の演算だけが対象になります。

```python
def func(x):
    return x == (1 == 2)  # 比較演算は対象外


def func(x):
    return x == [1, 2]  # ミュータブルなオブジェクトは対象外


def func(x):
    return x == frozenset((1, 2, 3))  # 関数呼び出しを伴うものは対象外


def func(x):
    i = 0
    return x == i + 1  # リテラル同士の演算ではないので対象外
```

# glob パターン

ドキュメントではすごくあっさりと流されていますが、pathlib の [Path.glob](https://docs.python.org/ja/3/library/pathlib.html#pathlib.Path.glob) はめちゃくちゃ複雑なクエリも扱えます。

```python
from pathlib import Path

for path in Path(".").glob("**/foo/**/ba[rz]/*/*/*_qux_???.png"):
    ...
```

| Pattern  | 意味                  |
|----------|---------------------|
| `*`      | すべてにマッチします          |
| `?`      | 任意の一文字にマッチします       |
| `[seq]`  | seq にある任意の文字にマッチします |
| `[!seq]` | seq にない任意の文字にマッチします |

参考: https://docs.python.org/ja/3/library/fnmatch.html#module-fnmatch

Python 3.11 からは末尾に `/` を付けるとディレクトリだけ探索できるようになりました。これは [os.DirEntry](https://docs.python.org/ja/3/library/os.html#os.DirEntry) を利用した実装になっており、自分でディレクトリ判定をするより高速（らしい）です。

```python
for directory in Path(".").glob("*/"):
    ...
```

ファイルだけ探索する方法はまだないようです。拡張子を付けて検索するか、[Path.is_file](https://docs.python.org/ja/3/library/pathlib.html#pathlib.Path.is_file) でフィルタする必要があります。

```python
files = (path for path in Path(".").glob("*.json") if path.is_file())
```

`Path.is_file` はシステムコールが発生してしまうので、対象が多数ある時は [os.walk](https://docs.python.org/ja/3/library/os.html#os.walk)（Python 3.12 以降なら [Path.walk](https://docs.python.org/ja/3/library/pathlib.html#pathlib.Path.walk)）を使った方が良いかもしれません。

```python
# 注意：glob の挙動を完全に再現するものではありません。
def rglob_files(root_dir: Path, pattern: str):
    pattern = Path(pattern)
    return (
        path
        for dirpath, _, filenames in Path(root_dir).walk()
        for name in filenames
        if (path := dirpath / name).match(pattern)
    )
```

# dataclass な Enum

Python の Enum は各列挙子の値を明示的に設定する必要があります。[auto](https://docs.python.org/ja/3/library/enum.html#enum.auto) を使うと自動でいい感じにやってくれるので、普段はこれを使っています。

```python
from enum import Enum, auto


class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()
```

逆にこればっかり使っていると忘れてしまうのですが、Enum には任意の値を指定できます。特に公式ドキュメントにある [フィールドを生やす例](https://docs.python.org/ja/3/howto/enum.html#planet) はシンプルながらとても便利です。

```python
from __future__ import annotations  # Python 3.9 以降は不要
from enum import Enum, unique


@unique
class Color(Enum):
    RED = ((255, 0, 0), "赤")
    GREEN = ((0, 255, 0), "緑")
    BLUE = ((0, 0, 255), "青")

    def __init__(self, rgb: tuple[int, int, int], title: str):
        # name, value など一部のフィールド名は使えないので注意。
        self.rgb = rgb
        self.title = title


color = Color.RED
print(color.rgb, color.title)
```

これは列挙子の値としては見た目通りタプルですが、それをパースした `rgb` フィールドと `title` フィールドが列挙子に追加されます。

これを利用すれば、Enum から値へのマッピングは基本的に不要になります。

```python
# rgb フィールドで取れるのでこれは不要。
RGB_MAP = {
    Color.RED: (255, 0, 0),
    Color.GREEN: (0, 255, 0),
    Color.BLUE: (0, 0, 255),
}
```

なお、[pickle](https://docs.python.org/ja/3/library/pickle.html) で列挙子をシリアライズすると列挙子の名前ではなく値を出力してしまうため、値を変えると後方互換性が失われることに注意です。これは `auto` を使ってても起こり得る問題ですが、頻繁に値が変わるマッピングに使うのは避けた方が良いかもしれません。


# dataclass な tuple

`typing` モジュールにある [NamedTuple](https://docs.python.org/ja/3/library/typing.html#typing.NamedTuple) というクラスをご存じですか？NamedTuple はタプルでありつつ dataclass っぽくもあるクラスを作れる便利クラスです。

```python
from typing import NamedTuple


class ItemTuple(NamedTuple):
    """ItemTuple is a tuple of name and value."""

    name: str
    """name is a string."""

    value: float
    """value is a float."""


item = ItemTuple("pi", 3.14)
print(item[0])  # タプルのようにもアクセスできる。
print(item.value)  # dataclass のようにもアクセスできる。
```

以前は「普通に dataclass 使えばよくない……？」と思ってたんですが、API の返り値に使うとユーザーの自由度が広がります。

```python
from typing import NamedTuple


class FuncResult(NamedTuple):
    x: int
    y: int


def func() -> FuncResult:
    """ NamedTuple を返す関数。"""
    return FuncResult(1, 2)


# 普通のタプルのようにアンパックできる。
x, y = func()

# namedtuple として受け取ればフィールドアクセスもできる。
result = func()
print(result.x, result.y)
```

numpy も [np.unique_counts](https://numpy.org/doc/stable/reference/generated/numpy.unique_counts.html) などの一部の API でこの形式を使うようになりました。

他にも、タプルしか受け付けないサードパーティーライブラリの引数に情報を付加する用途にも使えますね。下記は紛らわしい OpenCV の引数を直感的に扱えるようにした例です。

```python
from typing import NamedTuple

import cv2
import numpy as np


class CVPoint(NamedTuple):
    x: int
    y: int


class CVSize(NamedTuple):
    height: int
    width: int
    channels: int = 3


class CVColor(NamedTuple):
    b: int
    g: int
    r: int


size = CVSize(width=400, height=300)
center = CVPoint(x=50, y=150)
radius = 50
color = CVColor(r=0, g=0, b=255)

image = np.zeros(size, dtype=np.uint8)
cv2.circle(image, center, radius, color, -1)
```

ただ、個人的にはこれをやるなら dataclass に明示的にタプルを作って返すプロパティを付けた方がより安全で良い気がします。


# byte is int

ASCII 文字列を「ASCII コードの `int` の `list`」に変換してください。

```python
original_str = "123ABCxyz"
# expected   = [49, 50, 51, 65, 66, 67, 120, 121, 122]
```

愚直に実装するとこうですね。

```python
int_values = [ord(c) for c in original_str]
print(int_values)  # [49, 50, 51, 65, 66, 67, 120, 121, 122]
```

ところで、上記のように `str` オブジェクトをイテレートすると各要素は `str` ですが、`bytes` オブジェクトをイテレートすると各要素が何になるか分かりますか？

正解は `int` です。

```python
print([b for b in b"123"])  # [49, 50, 51]
```

ということで ASCII 文字列を `int` の `list` にするには、これで十分です。

```python
int_values = list(original_str.encode("ASCII"))
print(int_values)  # [49, 50, 51, 65, 66, 67, 120, 121, 122]
```

文字列に戻すのも同様です。

```python
s = bytes(int_values).decode("ASCII")
print(s)  # '123ABCxyz'
```

`bytes` はイテレートもインデックスアクセスもスライスもできるので、要素にアクセスできれば良いなら `list` に変換する必要もないでしょう。

```python
int_values = original_str.encode("ASCII")
print(int_values[0])  # 49
```

日本語を含むマルチバイト文字を扱う場合は少し複雑です。恐らく一番高速なのは、固定長の UTF-32 にエンコードして、`array` でパースする方法です。

```python
from array import array

original_str = "あいabcうえお☺"

int_values = array("I", original_str.encode("utf-32le"))  # エンディアン注意

print(int_values)  # array('I', [12354, 12356, 97, 98, 99, 12358, 12360, 12362, 9786])
print(int_values.tolist() == [ord(c) for c in original_str])  # True
```

ただし、numpy 配列にしたい場合は直接 numpy の機能を使って変換した方が速いでしょう。

```python
import numpy as np

# ASCII 文字列
int_values = np.array([original_str], dtype="S").view(np.uint8)

# Unicode 文字列
int_values = np.array([original_str], dtype="U").view(np.uint32)
```

# int is bytes

あまり使うことはないんですが、Python ならではの面白い小技を一つ。

次の 1KB のバイト列を 1 ビット左シフトしてください。

```python
b = b"\x01" * 1024
print(b[:3])  # b'\x01\x01\x01'
```

下記のようにしたいところですが、残念ながらこれはできません。

```python
b << 1  # TypeError: unsupported operand type(s) for <<: 'bytes' and 'int'
```

Python の `bytes` オブジェクトは文字列と似たカテゴリのオブジェクトで、算術・論理演算はサポートしていません。

愚直にこれを実装すると結構面倒くさいのですが 、「Python の `int` は上限値がない」という性質を利用すると、任意長のバイト列を **1つの** `int` 型変数で表現できます。

すると、ビットシフトはこうなります。

```python
# バイト列から int 値を生成する。
int_value = int.from_bytes(b, "big")

# 普通の int 変数なので任意の算術・論理演算を適用できる。
int_value = int_value << 1

# バイト列に戻す。 ※注意：バイト長が変わるケースを考慮していない。
new_bytes = int_value.to_bytes(len(b), "big")
print(new_bytes[:3])  # b'\x02\x02\x02'
```

ちなみに上記例だと `int_value` 変数には 10 進数で 2464 桁の整数が格納されています。

ただし、パフォーマンス的には numpy を使って頑張って計算した方が速いので、実際に使うことはまずないです……。


# 例外処理の使い分け

最後は例外処理についてです。意外と多機能な Python の例外処理、基本ですがおさらいしていきましょう。

## 基本形

基本はこれですね。

```python
try:
    d["x"] += 1
except KeyError as e:
    # 例外処理
```

`try` 節に例外を監視したいコードを書き、`expect 例外クラス as 変数名` で該当の例外をキャッチできます。

## 複数種類の例外をキャッチしたい時

`except` を並べて書くか、

```python
try:
    d["x"] += 1
except KeyError as e:
    # 例外処理
except TypeError as e:
    # 例外処理
```

タプル形式でも書けます。

```python
try:
    d["x"] += 1
except (KeyError, TypeError) as e:
    # 例外処理
```

（推奨されませんが）あらゆる例外をキャッチしたい時は `Exception` をキャッチするようにします。

```python
try:
    d["x"] += 1
except (KeyError, TypeError) as e:
    # 例外処理
except Exception as e:
    # 例外処理
```

## 例外を消したい時

`expect` 節で例外をキャッチした後、例外を投げ直さなければ無視（処理を継続）します。

```python
try:
    d["x"] += 1
except KeyError:
    pass
```

上記のように何もしなくて良い時は `contextlib` の [suppress](https://docs.python.org/ja/3/library/contextlib.html#contextlib.suppress) を使うとシンプルに書けます。

```python
from contextlib import suppress

with suppress(KeyError):
    d["x"] += 1
```

## 例外を素通りさせたい時

`expect` 節で例外をキャッチした後、引数なしで `raise` を実行するとキャッチした例外をそのまま投げ直します。

```python
try:
    d["x"] += 1
except KeyError:
    raise
```

```none
Traceback (most recent call last):
  File "...", line 4, in <module>
    d["x"] += 1
    ~^^^^^
KeyError: 'x'
```

例外に情報を付け足す必要はないけれど、中断処理などを挟みたい時に便利ですね。

```python
try:
    d["x"] += 1
except KeyError:
    abort()
    raise
```

出力する情報を付け足したい場合は `print` で吐き出すこともできますが、スタックトレースの前に表示されるので見落とされたりする可能性があります。

この時、Python 3.11 で追加された [add_note](https://docs.python.org/ja/3/library/exceptions.html#BaseException.add_note) というメソッドが便利です。

```python
try:
    d["x"] += 1
except KeyError as e:
    e.add_note(f"Existing Keys: {list(d)}")
    raise
```

```none
Traceback (most recent call last):
  File "...", line 4, in <module>
    d["x"] += 1
    ~^^^^^
KeyError: 'x'
Existing Keys: ['y', 'z']
```

## 例外を上乗せしたい時

上記例では `KeyError` を素通りさせましたが、`KeyError` のような汎用的な例外だけだと状況が伝わらない可能性があります。この場合、`raise...from` を使って適切な例外を上乗せします。

```python
class UnexpectedState(Exception):
    def __str__(self):
        return "Did you forget to initialize?"


try:
    d["x"] += 1
except KeyError as e:
    raise UnexpectedState from e
```

```none
Traceback (most recent call last):
  File "...", line 10, in <module>
    d["x"] += 1
    ~^^^^^
KeyError: 'x'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "...", line 12, in <module>
    raise UnexpectedState from e
UnexpectedState: Did you forget to initialize?
```

`from e` を付けなくても `except` 節の中で `raise` すれば同様に例外が2つ出ますが、2つの例外の間に出力されるメッセージの内容が変わります。

`from e` を付けた時：

```none
The above exception was the direct cause of the following exception:
（上記の例外が次の例外の直接の原因です。）
```

`from e` を付けなかった時：

```none
During handling of the above exception, another exception occurred:
（上記の例外処理中に、別の例外が発生しました。）
```

微妙な違いですが、後者は「例外処理中に意図せずして別の例外が発生してしまった時」と区別できません。そのため、「意図的に発生させた別の例外である」ことを明示するために `from e` を付けて出すべきです。

## 例外を差し替えたい時

もう一つのオプションとして、`from None` を付けて古い例外を非表示にすることができます。

```python
try:
    d["x"] += 1
except KeyError as e:
    raise UnexpectedState from None
```

```none
Traceback (most recent call last):
  File "...", line 12, in <module>
    raise UnexpectedState from None
UnexpectedState: Did you forget to initialize?
```

`KeyError` が表示されなくなったことに注目してください。

一般的に、プログラムがエラーで落ちた時はスタックトレースを辿って一番最初に発生したエラーを調査すると思います。この時、最初に発生した例外がエラーの直接の原因でないと混乱を招くので、あえて前の例外を揉み消すという選択肢が出てきます。特に Python は `StopIteration` などのいわゆる制御フロー系の例外もあるため、これは重要なテクニックです。

なお、どの投げ方でも例外の `__context__` フィールドに一つ前の例外が入っていますので、必要ならそこから辿れます。あくまでも表示する内容を制御する機能ですね。

```python
try:
    try:
        d["x"] += 1
    except KeyError as e:
        raise UnexpectedState from None
except Exception as e2:
    print(repr(e2))  # UnexpectedState()
    print(repr(e2.__context__))  # KeyError('x')
```

# おわりに

これで今回の紹介は終わりです。これらのテクニックを活用することで、あなたのコードが効率化され、生産性が向上すること間違いなしです。ぜひ、今日からでも試してみてください。

ただし、便利なテクニックが必ずしも最善の選択とは限らないことを忘れないでください。コードの読みやすさや保守性、そしてチーム全体の理解を考慮した選択が、最終的にはプロジェクトの成功に繋がります。皆さんがPythonを駆使して、素晴らしいプロジェクトを生み出していくことを願っています。

ちなみにこの最後のまとめは ChatGPT 先生に考えてもらいました。先生も仰る通り、筆者もチーム開発の場では可読性を優先して使わなかったり、冗長なやり方を選択することも多々あります。他にも「そのやり方はあかん！」というのがあればコメント欄でご指摘お待ちしてます。ご清覧ありがとうございました。