はじめに
Python 3.10 から使えるようになった match case 文で謎のバグに遭遇したので共有します。
コードレビューで「マジックナンバーやめて定数使おうぜ」って言われて素直に書き換えたら、まさかの動かなくなって焦った😇
TL;DR
-
match caseで定数を直接パターンに書くとキャプチャパターン扱いになって意図しない動作をする - 定数と比較したいなら
if文使うか、ガード (case x if x == 定数:) を使うか、Enum にまとめよう - リテラル(数字そのもの)やドット記法(
Enum.VALUE)は値パターンとして機能する
何が起きたか
こんなコードがあったとします。ビットフラグで権限管理してる感じのやつです。
from enum import IntEnum
class Permission(IntEnum):
READ = 1
WRITE = 2
DELETE = 4
# ビットフラグで複数指定
READ_AND_DELETE = Permission.READ | Permission.DELETE # 5
WRITE_AND_DELETE = Permission.WRITE | Permission.DELETE # 6
def check_permission(permission_code: int):
match permission_code:
case 5: # READ + DELETE
return "読み取りと削除が許可されています"
case 6: # WRITE + DELETE
return "書き込みと削除が許可されています"
case Permission.READ:
return "読み取りのみ許可"
case Permission.WRITE:
return "書き込みのみ許可"
case _:
return "権限なし"
レビューで「5とか6とかマジックナンバーやめて」って言われたので、こう書き換えました。
def check_permission(permission_code: int):
match permission_code:
case READ_AND_DELETE: # 5 の代わり
return "読み取りと削除が許可されています"
case WRITE_AND_DELETE: # 6 の代わり
return "書き込みと削除が許可されています"
case Permission.READ:
return "読み取りのみ許可"
case _:
return "権限なし"
一見よさそうですよね?
これが動きません。
何が問題だったのか
Python の match case はパターンマッチングなので、case の後ろに書いたものの扱いが特殊です。
値パターン vs キャプチャパターン
PEP 634(構造的パターンマッチング)の仕様により、Python は以下のように判断します
| 書き方 | 扱い | 意味 |
|---|---|---|
case 5: |
値パターン(リテラル) | 「値が5のとき」にマッチ |
case "hello": |
値パターン(リテラル) | 「値が"hello"のとき」にマッチ |
case Enum.VALUE: |
値パターン(属性アクセス) | 「Enum.VALUE の値」にマッチ |
case SOME_CONSTANT: |
キャプチャパターン | 「どんな値でも受け取ってSOME_CONSTANTに束縛」 |
つまり、case READ_AND_DELETE: と書くと
- 「READ_AND_DELETEという定数の値(5)と比較する」のではなく
- 「どんな値でも受け取って、READ_AND_DELETEという変数名に束縛する」
という意味になってしまうんです。
これは、単純な名前(ドットを含まない識別子)は常にキャプチャパターンとして扱われるという PEP 634 の仕様によるものです。
実際の動作
def check_permission(permission_code: int):
match permission_code:
case READ_AND_DELETE: # ← ここで何でもマッチしてしまう
return "読み取りと削除が許可されています"
case WRITE_AND_DELETE: # ← ここには絶対到達しない
return "書き込みと削除が許可されています"
# どんな値を渡しても最初のケースにマッチする
print(check_permission(1)) # "読み取りと削除が許可されています"
print(check_permission(999)) # "読み取りと削除が許可されています"
全部最初のケースで拾われます。完全にバグってますね。
正しい書き方
方法1: 素直に if 文を使う(推奨)
一番シンプルで分かりやすいのはこれ。
def check_permission(permission_code: int):
if permission_code == READ_AND_DELETE:
return "読み取りと削除が許可されています"
elif permission_code == WRITE_AND_DELETE:
return "書き込みと削除が許可されています"
# 残りは match で分岐
match permission_code:
case Permission.READ:
return "読み取りのみ許可"
case Permission.WRITE:
return "書き込みのみ許可"
case _:
return "権限なし"
複雑な分岐が必要ないなら、全部 if-elif でもいいと思います。
def check_permission(permission_code: int):
if permission_code == READ_AND_DELETE:
return "読み取りと削除が許可されています"
elif permission_code == WRITE_AND_DELETE:
return "書き込みと削除が許可されています"
elif permission_code == Permission.READ:
return "読み取りのみ許可"
elif permission_code == Permission.WRITE:
return "書き込みのみ許可"
else:
return "権限なし"
方法2: ガード条件を使う
どうしても match を使いたい場合は、ガード(if による条件)をつけます。
def check_permission(permission_code: int):
match permission_code:
case x if x == READ_AND_DELETE: # ガードで等値判定
return "読み取りと削除が許可されています"
case x if x == WRITE_AND_DELETE:
return "書き込みと削除が許可されています"
case Permission.READ:
return "読み取りのみ許可"
case Permission.WRITE:
return "書き込みのみ許可"
case _:
return "権限なし"
case x if x == READ_AND_DELETE: の部分がガード条件です。
- まず
xに値を束縛(キャプチャ) - その後
if条件で実際の判定
という2段階になっています。
方法3: Enum にまとめる
複数の定数を管理する場合、Enum にまとめるのもいい選択肢です。
from enum import IntEnum
class Permission(IntEnum):
READ = 1
WRITE = 2
DELETE = 4
class CombinedPermission(IntEnum):
READ_AND_DELETE = 5
WRITE_AND_DELETE = 6
def check_permission(permission_code: int):
match permission_code:
case CombinedPermission.READ_AND_DELETE: # Enum ならOK
return "読み取りと削除が許可されています"
case CombinedPermission.WRITE_AND_DELETE:
return "書き込みと削除が許可されています"
case Permission.READ:
return "読み取りのみ許可"
case Permission.WRITE:
return "書き込みのみ許可"
case _:
return "権限なし"
Enum にすることで、ドット記法(CombinedPermission.READ_AND_DELETE)が使えるようになり、値パターンとして正しく動作します。
なぜ IntEnum は直接使えるのか?
ちなみに Permission.READ は問題なく動きます。
match permission_code:
case Permission.READ: # これはOK
...
これは、Permission.READ が属性アクセス(ドット記法) を含んでいるため、PEP 634 の仕様により「値パターン」として扱われるからです。
単純な名前(READ_AND_DELETE)だと「キャプチャパターン」と解釈されますが、ドット記法を使うことで「この値と比較する」という意味になります。
まとめ
- Python の
match caseで定数を使うときは注意が必要 - 単純な名前(
case 定数名:)はキャプチャパターン扱いになるので、そのままだと動かない - 実務では素直に
if文使うのが一番わかりやすい - どうしても
match使いたいなら以下の方法がある- ガード条件:
case x if x == 定数: - Enum にまとめる:
case Enum.VALUE:
- ガード条件:
コードレビューで「マジックナンバーやめろ」って言われたときは、無理に match で解決しようとせず、普通の条件分岐にリファクタリングするか、Enum にまとめるのが良いですね。