構造的パターンマッチ(python3.10〜)
構造的パターンマッチングについて
多くの言語で実装されているパターンマッチがPython 3.10 から構造的パターンマッチングとしてが追加されました。これまでは _isinstance()やkey in objなどの条件判定を繰り返す必要がありましたが、構造的パターンマッチを使うとデータ構造や型によるる分岐をシンプルな構文(matchとcase)で記述できるようになります。
構造的パターンマッチは、match文やcase文で与えたパターンとそれに関連するアクションという形で行われます。パターンは、次のもので構成されます。
literal_pattern
capture_pattern
wildcard_pattern
value_pattern
group_pattern
sequence_pattern
mapping_pattern
class_pattern
パターンマッチングにより、プログラムは複雑なデータタイプから情報を抽出したり、データの構造を分岐したり、さまざまな形式のデータに基づいて特定のアクションを適用することができます。詳細な仕様は PEP-634 で定義されています。
構文と操作
パターン・マッチングの一般的な構文は次のとおりです。
code: python
match subject:
case <pattern_1>:
<action_1>
case <pattern_2>:
<action_2>
case <pattern_3>:
<action_3>
case _:
<action_wildcard>
match と case は、パターンマッチ構文の中でだけ使えるキーワードで、他の箇所ではこれまで通り変数名などに使用することができます。
match文は式を受け取り、その値を1つまたは複数の case文で記述した連続したパターンと比較します。具体的には、パターンマッチングは以下のように動作します。
型(type)と形(shape)を持つデータ(subject)を使用する。
マッチステートメントでサブジェクトを評価する
一致が確認されるまで、サブジェクトをcase文の各パターンと上から下へ比較してゆく
一致が確認されたパターンに関連するアクションを実行する。
完全に一致することが確認されなかった場合、最後のcaseにワイルドカード(_) が指定されていれば、それが一致するパターンとして使用されます。いずれのパターンにも一致しない場合は、matchブロック全体がno-opとなります。
宣言的アプローチ
パターン・マッチングについては、CやJava、JavaScript、その他多くの言語でのswitch文を使った、サブジェクト(データ・オブジェクト)をリテラル(パターン)にマッチさせるという単純な例を見ると理解が早いはずです。switch文は、オブジェクトや式と、リテラルを含むcase文との比較に使われることが多いです。
パターンマッチングのより強力な例は、ScalaやElixirなどの言語に見られます。構造的パターンマッチングでは、データがマッチする条件(パターン)を明示する宣言的(declarative)なアプローチをとります。
if文を入れ子にした命令的(imperative)な表記でも構造的パターンマッチングと同様のことができますが、宣言的のアプローチに比べると明確さに欠けます。その代わり、宣言的アプローチでは、マッチするための条件を記述し、パターンを明示することで読みやすくしています。構造的パターンマッチは、case文の中で変数とリテラルを比較する最も単純な形で使用できますが、Pythonにおけるその真価は、対象のタイプと形状の取り扱いにあります。
単純なパターン:リテラルへのマッチ(literal_pattern)
この例を、パターンマッチングの最も単純な形として見てみましょう。対象となる値が、パターンとなるいくつかのリテラルにマッチします。文字列、数値、複素数、ブール値、None が使えます。
次の例では、statusがmatch文の対象となります。パターンはそれぞれのcase文で、リテラルはリクエストのステータスコードを表します。一致した後、caseに関連するブロックのアクションが実行されます。
code: python
In 2: # %load 01_literal_pattern.py ...: def http_error(status):
...: match status:
...: case 400:
...: return "Bad request"
...: case 401 | 403:
...: return "Not allowed"
...: case 404:
...: return "Not found"
...: case 418:
...: return "I'm a teapot"
...: case ( 500 | 503 ):
...: return "Internal error"
...: case _:
...: return "Something's wrong with the Internet"
...:
...:
...: if __name__ == '__main__':
...: msg = http_error(400)
...: assert msg == "Bad request"
...: msg = http_error(401)
...: assert msg == "Not allowed"
...: msg = http_error(403)
...: assert msg == "Not allowed"
...: msg = http_error(404)
...: assert msg == "Not found"
...: msg = http_error(418)
...: assert msg == "I'm a teapot"
...: msg = http_error(500)
...: assert msg == "Internal error"
...: msg = http_error(800)
...: assert msg == "Something's wrong with the Internet"
...:
この例では、関数 http_error() にstatusとして 418 が渡された場合、"I'm a teapot" が返されます。この関数にstatus として 800 が渡された場合、単一のアンダースコア(_)を含むcase文がワイルドカードとしてマッチし、"Something's wrong with the Internet" が返されます。単一のアンダースコア(_)の変数名がワイルドカードの役割を果たし、常に一致することを保証しています。ワイルドカード使用は任意です。
複数のリテラルを1つのパターンにまとめるためには、 パイプ記号(|) を使用します。これは、orの意味を持ちます(or_pattern)。orと記述するとSyntaxError になります。
パターンを丸括弧で囲むとパターンをグループ化します。(group_pattern)
group_pattern は or_pattern を見やすくするために使用されます。
ワイルドカードを使用しない場合の動作
上記の例を修正して、最後のcase節を削除すると、例は次のようになります。
code: python
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case文で_を使わないときは、一致するものが存在しない場合があります。その場合の動作は no-op (No Operation) となります。たとえば、statusが 500 であれば、no-op となります。
code: python
In 2: # %load 02_without_wildcard.py ...: def http_error(status):
...: match status:
...: case 400:
...: return "Bad request"
...: case 401 | 403:
...: return "Not allowed"
...: case 404:
...: return "Not found"
...: case 418:
...: return "I'm a teapot"
...:
...:
...: if __name__ == '__main__':
...: msg = http_error(500)
...: print(msg)
...:
None
値の比較は基本的に == 演算子で評価されます。そのため、 case 1: と case 1.0: は同じものとして扱われます。
値が True、False、 None の場合では、is 演算子で評価されます。
capture_pattern
case 節に変数名を指定すると、そのパターンには常に一致します。また、値を変数名に バインド (bind)します。
バインドは代入とよく似た動作で、変数に値が設定されます。データ構造の一部の値をバインドし、case節で使用することができます。パターンでは変数しか記述できません。つまり、インデックス(error[0]) や属性(error.message)を使用することはできません。
code: python
In 2: # %load 03_capture_pattern.py ...: def greeting(message):
...: match message:
...: case name:
...: return f"Hello {name}!"
...:
...: def read_scroll(scroll):
...: return "Enchant weapon."
...:
...: def command_parse(command):
...: match command.split():
...: return read_scroll(item)
...:
...: if __name__ == '__main__':
...: msg = greeting("Python")
...: assert msg == "Hello Python!"
...:
...: msg = command_parse("read scroll")
...: assert msg == 'Enchant weapon.'
...:
ひとつのパターンのなかで同じ変数名を 2回以上指定することはできません。
上記の例では、case name: を case (name, name): のように記述することはできません。
value_pattern
value_pattern はリテラルと変数を使ったパターンです。重要なことはcase文の中で変数名を列挙しても、それは名前の付いた変数の内容に対してマッチを行うことを意味しません。caseの中の変数は、マッチする値を捉えるために使われます。代入をイメージすると理解しやすいでしょう。つまり、パターンは、変数をアンパックするように動作し、パターンを使用して変数をバインドすることができます。この例では、データポイントはx座標とy座標にアンパックされます。`
code: python
In 2: # %load 04_literal_variable.py ...: def point_check(point):
...: # point は (x, y) となるタプル
...: match point:
...: case (0, 0):
...: return "Origin"
...: case (0, y):
...: return f"Y={y}"
...: case (x, 0):
...: return f"X={x}"
...: case (x, y):
...: return f"X={x}, Y={y}"
...: case _:
...: raise ValueError("Not a point")
...:
...: if __name__ == '__main__':
...: msg = point_check((0, 0))
...: assert msg == "Origin"
...: msg = point_check((0, 1))
...: assert msg == "Y=1"
...: msg = point_check((1, 0))
...: assert msg == "X=1"
...: msg = point_check((1, 1))
...: assert msg == "X=1, Y=1"
...: try:
...: msg = point_check("Python")
...: except ValueError as e:
...: print(e)
...:
Not a point
最初のパターンは、(0, 0)という2つのリテラルを持ち、上記のリテラルパターンを拡張したものと考えることができます。次の2つのパターンは、リテラルと変数を組み合わせたもので、変数にはサブジェクト(point)からの値がバインドされます。4つ目のパターンは2つの値を束ねるもので、概念的にはアンパック代入(x, y) = pointと似ています。
caseに与えるパターンにドット(.`)を使用すると caputure_pattern ではなくて value_pattern となり値として比較されます。
変数の内容にマッチさせたい場合は、その変数をenumのようにドット付きの名前で表現する必要があります。
以下はその例です。
code: python
In 2: # %load 05_value_pattern.py ...: import logging
...:
...: def check_level(level):
...: match level:
...: case logging.CRITICAL:
...: return "Log level: CRITICAL"
...: case logging.ERROR:
...: return "Log level: ERROR"
...: case logging.WARNING:
...: return "Log level: WARNING"
...: case logging.INFO:
...: return "Log level: INFO"
...: case logging.DEBUG:
...: return "Log level: DEBUG"
...: case logging.NOTSET:
...: return "Log level: NOTSET"
...:
...: if __name__ == '__main__':
...: msg = check_level(50)
...: assert msg == "Log level: CRITICAL"
...: msg = check_level(40)
...: assert msg == "Log level: ERROR"
...: msg = check_level(30)
...: assert msg == "Log level: WARNING"
...: msg = check_level(20)
...: assert msg == "Log level: INFO"
...: msg = check_level(10)
...: assert msg == "Log level: DEBUG"
...:
code: pytohn
In 2: # %load 06_value_pattern_enum.py ...: from enum import Enum
...:
...: class Command(Enum):
...: QUIT = 0
...: RESET = 1
...:
...: def command_parser(command):
...: match command:
...: case Command.QUIT:
...: return 'quit'
...:
...: case Command.RESET:
...: return 'reset'
...:
...: if __name__ == '__main__':
...: print(command_parser(Command.QUIT))
...: print(command_parser(Command.RESET))
...:
...:
quit
reset
パターンとクラス
データの構造化にクラスを使用している場合、クラス名の後にコンストラクタのような引数リストが続くパターンを使用することができます。このパターンは、クラスの属性を変数に取り込むことができます。
code: python
In 2: # %load 07_class_pattern.py ...: class Point:
...: x: int
...: y: int
...:
...: def location(point):
...: match point:
...: case Point(x=0, y=0):
...: return "Origin is the point's location."
...: case Point(x=0, y=y):
...: return f"Y={y} and the point is on the y-axis."
...: case Point(x=x, y=0):
...: return f"X={x} and the point is on the x-axis."
...: case Point():
...: return "The point is located somewhere else on the plane."
...: case _:
...: return "Not a point"
...:
...: if __name__ == '__main__':
...: point = Point()
...: point.x, point.y = (0, 0)
...: msg = location(point)
...: assert msg == "Origin is the point's location."
...: point.x, point.y = (0, 1)
...: msg = location(point)
...: assert msg == "Y=1 and the point is on the y-axis."
...: point.x, point.y = (1, 0)
...: msg = location(point)
...: assert msg == "X=1 and the point is on the x-axis."
...: msg = location(Point())
...: assert msg == "The point is located somewhere else on the plane."
...: msg = location("Python")
...: assert msg == "Not a point"
...:
Pythonの構造的パターンマッチングシステムの機能のうち特筆するべきものは、特定のプロパティを持つオブジェクトに対してマッチングを行う機能です。例えば、media_object という名前のオブジェクトを扱い、それを.jpg ファイルに変換して返したいという処理があるとします。この場合次のようなコードになるでしょう。
code: python
match media_object:
case Image(type="jpg"):
# そのまま返す
return media_object
case Image(type="png") | Image(type="gif"):
# JPEG変換をして返す
return render_as(media_object, "jpg")
case Video():
raise ValueError("Can't extract frames from video yet")
case other_type:
raise Exception(f"Media type {media_object} can't be handled yet")
この例では、ImageクラスとVideoクラスのオブジェクトのマッチングをしています。最初のパターンでは、type属性が "jpg "に設定されているImageオブジェクトにマッチします。2番目のケースは、type が "png" または "gif"の場合にマッチします。3番目のケースは、属性に関係なく、Videoタイプのオブジェクトにマッチします。そして最後のケースは、他のすべてが失敗した場合のキャッチオールです。
位置パラメータを使ったパターン
位置パラメータは、属性の順序付けを行ういくつかの組み込みクラス(dataclassなど)で使用できます。
code: python
In 2: # %load 08_dataclass.py ...: from dataclasses import dataclass
...:
...: @dataclass
...: class Point:
...: x: int
...: y: int
...:
...: def location(point):
...: match point:
...: case Point(x=0, y=0):
...: return "Origin is the point's location."
...: case Point(x=0, y=y):
...: return f"Y={y} and the point is on the y-axis."
...: case Point(x=x, y=0):
...: return f"X={x} and the point is on the x-axis."
...: case _:
...: return "Not a point"
...:
...: if __name__ == '__main__':
...: msg = location(Point(0,0))
...: assert msg == "Origin is the point's location."
...: msg = location(Point(0,1))
...: assert msg == "Y=1 and the point is on the y-axis."
...: msg = location(Point(1,0))
...: assert msg == "X=1 and the point is on the x-axis."
...: msg = location("Python")
...: assert msg == "Not a point"
...:
dataclass でデコレートされたPoinクラスは、次の__init__()が追加されます。(PEP-557)
code: python
def __init__(self, x: int, y: int):
self.x = x
self.y = y
位置パラメータを使ったパターン
位置パラメータは、属性の順序付けを行ういくつかの組み込みクラス(dataclassやnamedtutpleなど)で使用できます。また、クラスに__match_args__という特別な属性を設定することで、パターン内の属性の特定の位置を定義することができます。この属性を ("x", "y") に設定すると、以下のパターンはすべて等価になります (y 属性を var 変数にバインドすることも同じです)。
code: python
Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)
code: python
In 2: # %load 09_match_args.py ...: class Point:
...: __match_args__ = ('x', 'y')
...: def __init__(self, x, y):
...: self.x = x
...: self.y = y
...:
...: def location(point):
...: match point:
...: case Point(0, 0):
...: return "Origin is the point's location."
...: case Point(0, y):
...: return f"Y={y} and the point is on the y-axis."
...: case Point(x, 0):
...: return f"X={x} and the point is on the x-axis."
...: case Point(x, y):
...: return f"positional: point is on the ({x}, {y})."
...: case _:
...: return "Not a point"
...:
...:
...: if __name__ == '__main__':
...: msg = location(Point(0,0))
...: assert msg == "Origin is the point's location."
...: msg = location(Point(0,1))
...: assert msg == "Y=1 and the point is on the y-axis."
...: msg = location(Point(1,0))
...: assert msg == "X=1 and the point is on the x-axis."
...: msg = location(Point(1,1))
...: assert msg == "positional: point is on the (1, 1)."
...: msg = location(Point(x=1, y=1))
...: assert msg == "positional: point is on the (1, 1)."
...: msg = location("Python")
...: assert msg == "Not a point"
...:
ネストされたパターン
パターンは任意に入れ子にすることができます。例えば、データがポイントの短いリストであれば、次のようにマッチさせることができます。
code: python
In 2: # %load 10_nested_pattern.py ...: from dataclasses import dataclass
...:
...: @dataclass
...: class Point:
...: x: int
...: y: int
...:
...: def location(points):
...: match points:
...: case []:
...: return "No points in the list."
...: return "The origin is the only point in the list."
...: return f"A single point {x}, {y} is in the list."
...: return f"Two points on the Y axis at {y1}, {y2} are in the l
...: ist."
...: case _:
...: return "Something else is found in the list."
...:
...:
...: if __name__ == '__main__':
...: msg = location([])
...: assert msg == "No points in the list."
...: assert msg == "The origin is the only point in the list."
...: assert msg == "A single point 0, 1 is in the list."
...: assert msg == "Two points on the Y axis at 1, 2 are in the list."
...: msg = location(Point(0,0))
...: assert msg == "Something else is found in the list."
...:
sequence_pattern
リストやタプルなどのシーケンスデータにマッチするパターンです。
シーケンスパターンに一致するためには、サブジェクトがシーケンスである必要があります。
丸括弧((...))や角括弧([...]) でパターンを記述します。ただし、丸括弧で囲まれたパターンで末尾にコンマがないものは、sequence_patternではなく、group_pattern です。しかし、角括弧([...])で囲まれた単一のパターンはsequence_patternのままです。sequence_pattern と group_pattern には構文上の違い(末尾にコンマの有無)があるだけです。
code: python
In 2: # %load 11_sequence_pattern.py ...: def command_parse(seq):
...: match seq:
...: case ('read', scroll):
...: return f'read {scroll}'
...: case ('drop', item, ):
...: return f'drop {item}'
...: return f'drink {portion}'
...:
...: if __name__ == '__main__':
...: command = ('read', 'show_room')
...: print( command_parse(command))
...: command = ('drop', 'dagger')
...: print( command_parse(command))
...: command = 'drink healing_portion'.split()
...: print( command_parse(command))
...:
read show_room
drop dagger
drink healing_portion
複雑なパターンとワイルドカード
ここまでの例では、最後のcase文に_だけを使っていました。ワイルドカードは、('error', code, _) のように、より複雑なパターンに使用することができます。
code: python
In 2: # %load 12_complex_wildcard.py ...: def test_variable(var):
...: match var:
...: case ('forbidden', 403, *rest):
...: return f"403, forbideen: {rest}."
...: case ('not found', 404, 40):
...: return "A warning has been received."
...: case ('error', 503, _):
...: return f"An error 503 occurred."
...:
...: if __name__ == '__main__':
...: msg = test_variable(('not found', 404, 40))
...: print(msg)
...: msg = test_variable(('error', 503, 80))
...: print(msg)
...: msg = test_variable(('error', 503, 120))
...: print(msg)
...: msg = test_variable(('forbidden', 403, "auth=python"))
...: print(msg)
...:
A warning has been received.
An error 503 occurred.
An error 503 occurred.
シーケンスパターンはワイルドカードをサポートしています。[x, y, *rest]および(x, y, *rest) は、アンパック代入する際のワイルドカードと同様の働きをします。(x, y, *_)は、少なくとも2つの項目からなるシーケンスにマッチし、残りの項目をバインドしません。
この例の場合、test_variable()に与えるタプルのはじめの2つの要素が、('forbidden', 403, ...)であるすべての値に一致し、3つ目以降の要素が 変数restにセットされます。同様に、('error', 503,...) であるすべての値も一致します。ワイルドカードはすべてのオブジェクトにマッチするため、case 節の並びの最後に配置することで else のように使用することができます。
ガード(Guard)
ガードと呼ばれるif節をパターンに追加することができます。ガードが偽の場合、matchは次のcaseブロックの試行に進みます。値の取得はガードが評価される前に行われることに注意してください。
code: python
In 2: # %load 13_guard.py ...: from dataclasses import dataclass
...:
...: @dataclass
...: class Point:
...: x: int
...: y: int
...:
...: def location(point):
...: match point:
...: case Point(x=0, y=0):
...: return "Origin is the point's location."
...: case Point(x, y) if x == y:
...: return f"The point is located on the diagonal Y=X at {x}."
...: case Point(x, y):
...: return "Point is not on the diagonal."
...: case _:
...: return "Not a point"
...:
...: if __name__ == '__main__':
...: msg = location(Point(0,0))
...: assert msg == "Origin is the point's location."
...: msg = location(Point(2,2))
...: assert msg == "The point is located on the diagonal Y=X at 2."
...: msg = location(Point(1,0))
...: assert msg == "Point is not on the diagonal."
...:
パターンの例
パターンは単純な値の場合もあれば、より複雑なマッチングロジックを含む場合もあります。次にいくつかのパターンの例をしめします。
case "a": 単一の値 "a"にマッチします。
case ["a", "b"]: コレクション["a", "b"]にマッチします。
case ["a", value1]: 2つの値を持つコレクションにマッチし、2つ目の値をキャプチャして変数value1にセットします。
case ["a", *values]: 少なくとも1つの値を持つコレクションにマッチします。その他の値がある場合は、valuesに格納されます。なお、スター付きのアイテムは、1つのコレクションにつき1つしか含めることができません。これは、Python関数のスター付き引数の場合と同じです。
case ("a"|"b"|"c"): or演算子(|)を使うと、複数のケースを1つのケースブロックで処理することができます。ここでは、"a"、"b"、"c "のいずれかにマッチします。
case ("a"|"b"|"c") as letter: 上の例と同じですが、マッチした項目を変数 letter に入れています。
case ["a", value] if <expression>: expression が真の場合にのみキャプチャにマッチします。キャプチャー変数は式の中で使用できます。例えば、if value in valid_valuesを使用した場合、キャプチャした値valueが実際にコレクションvalid_valuesに含まれている場合にのみ、caseが有効になります。
case ["z", _]: "z"で始まるアイテムのコレクションはすべてマッチします。
パターンマッチのあるとき/ないとき
他の言語で見られるswitchのような使い方をしているだけではパターンマッチの真価を発揮させることができません。PEP-636のチュートリアルには、シンプルなテキストベースのゲームでコマンドとその引数をパターンマッチで実装する例がいくつかあります。
code: python
command = input("What are you doing next? ")
match command.split():
print("Goodbye!")
quit_game()
current_room.describe()
character.get(obj, current_room)
for obj in objects:
character.drop(obj, current_room)
current_room = current_room.neighbor(direction)
print("Sorry, you can't go that way")
case _:
print(f"Sorry, I couldn't understand {command!r}")
これをパターンマッチを使わずに、昔ながらのスタイルでコードしてみましょう。ほとんどの場合、たくさんの if ... elif ブロックを使用することになます。条件をよりシンプルにするために、新しい変数fieldsを分割前のフィールドに、nをフィールドの数に設定します。
code: python
command = input("What are you doing next? ")
fields = text.split()
n = len(fields)
print("Goodbye!")
quit_game()
current_room.describe()
elif n == 2 and fields0 == "get": character.get(obj, current_room)
elif n >= 1 and fields0 == "drop": for obj in objects:
character.drop(obj, current_room)
elif n == 2 and fields0 == "go": if direction in current_room.exits:
current_room = current_room.neighbor(direction)
else:
print("Sorry, you can't go that way")
else:
print(f"Sorry, I couldn't understand {command!r}")
少し短くなっただけでなく、構造的マッチングバージョンの方が読みやすく、変数のバインドがされるため fields[1] のように手動でインデックス操作をする必要がなくなっていることにも注目してください。
この例では、charaterとcurrent_root いうオブジェクトがありますが、current_room を character の属性として保持させるとアクションがディスパッチがずっとシンプルになります。特に'go' の時の方向('direction') このディスパッチ処理のところで行っていますが、これをchracter.go() を作っておき、その中で処理することにすれば、辞書のキーをマッチさせる方法でアクションだけを振り分けられるようになります。この結果、この部分のコードは次のように書くことができずっと簡潔にはなります。ただし、どのアクションを行っているのかはこのディスパッチしている部分だけでは追いにくくなってしまいます。
code: python
command = input("What are you doing next? ")
fields = text.split()
action_table = {
'quit': {'todo': quit_game },
'look': {'todo': charater.look },
'get': {'todo': character.get },
'drop': {'todo': character.drop },
'go': {'todo': character.go },
}
try:
except KeyError:
print(f"Sorry, I couldn't understand {command!r}")
except Game.Command_Error as e:
print(e)
このチュートリアルでは、ゲームのイベントループの一部と思われる、クラスベースのマッチングの例も紹介されています。
code: python
match event.get():
case Click((x, y), button=Button.LEFT): # This is a left click
handle_click_at(x, y)
case Click():
pass # ignore other clicks
case KeyPress(key_name="Q") | Quit():
game.quit()
case KeyPress(key_name="up arrow"):
game.go_north()
...
case KeyPress():
pass # Ignore other keystrokes
case other_event:
raise ValueError(f"Unrecognized event: {other_event}")
これを単純なif ... elifを使って書き換えてみましょう。
code: python
e = event.get()
if isinstance(e, Click):
x, y = e.position
if e.button == Button.LEFT:
handle_click_at(x, y)
# ignore other clicks
elif isinstance(e, KeyPress):
key = e.key_name
if key == "Q":
game.quit()
elif key == "up arrow":
game.go_north()
# ignore other keystrokes
elif isinstance(e, Quit):
game.quit()
else:
raise ValueError(f"Unrecognized event: {e}")
この例で言えば両者には大差はあまりないように思えます。パターンマッチではcaseがすべて揃うという利点があり、if ... elifにはイベントタイプがより強くグループ化され、キータイプの繰り返しを避けることができるという利点があります。
Guido van Rossumがこの機能を紹介するために書いた式のパーサーと評価器 があります。これは match ... case を多用しています (かなり小さなファイルに 11 回も使われています)。次のコードはその抜粋です。 code: python
def eval_expr(expr):
"""Evaluate an expression and return the result."""
match expr:
case BinaryOp('+', left, right):
return eval_expr(left) + eval_expr(right)
case BinaryOp('-', left, right):
return eval_expr(left) - eval_expr(right)
case BinaryOp('*', left, right):
return eval_expr(left) * eval_expr(right)
case BinaryOp('/', left, right):
return eval_expr(left) / eval_expr(right)
case UnaryOp('+', arg):
return eval_expr(arg)
case UnaryOp('-', arg):
return -eval_expr(arg)
case VarExpr(name):
raise ValueError(f"Unknown value of: {name}")
case float() | int():
return expr
case _:
raise ValueError(f"Invalid expression value: {repr(expr)}")
これを if ... elifで実装するとどうなるでしょう? BinaryOpのケースをすべてまとめた場合には、若干異なる構造になるでしょう。それと、ifブロックの入れ子でのインデントレベルに注意してください。
code: python
def eval_expr(expr):
"""Evaluate an expression and return the result."""
if isinstance(expr, BinaryOp):
op, left, right = expr.op, expr.left, expr.right
if op == '+':
return eval_expr(left) + eval_expr(right)
elif op == '-':
return eval_expr(left) - eval_expr(right)
elif op == '*':
return eval_expr(left) * eval_expr(right)
elif op == '/':
return eval_expr(left) / eval_expr(right)
elif isinstance(expr, UnaryOp):
op, arg = expr.op, expr.arg
if op == '+':
return eval_expr(arg)
elif op == '-':
return -eval_expr(arg)
elif isinstance(expr, VarExpr):
raise ValueError(f"Unknown value of: {name}")
elif isinstance(expr, (float, int)):
return expr
raise ValueError(f"Invalid expression value: {repr(expr)}")
BinaryOpとUnaryOpの属性を手動で展開しているため、2行多くなっています。この場合もどちらがよいという決定的な違いはないように見えます。ただし、この例の場合では評価するクラスが’2つですが、多くなるにつれてパターンマッチでの記述の方がやみやすくなるはずです。
パターンマッチが役に立つもう一つの場面は、HTTPリクエストからのJSONの構造を検証するときです。
code: python
try:
obj = json.loads(request.body)
except ValueError:
raise HTTPBadRequest(f'invalid JSON: {request.body!r}')
match obj:
case {
'action': 'sign-in',
'username': str(username),
'password': str(password),
'details': {'email': email, **other_details},
} if username and password:
sign_in(username, password, email=email, **other_details)
case {'action': 'sign-out'}:
sign_out()
case _:
raise HTTPBadRequest(f'invalid JSON structure: {obj}')
code: python
try:
obj = json.loads(request.body)
except ValueError:
raise HTTPBadRequest(f'invalid JSON: {request.body!r}')
other_details.pop('email')
sign_in(username, password, email=email, **other_details)
sign_out()
else:
raise HTTPBadRequest(f'invalid JSON structure: {obj}')
この場合はパターンマッチの方がシンプルで読みやすいはずです。
Pythonの構造的パターンマッチで重要なのは、マッチさせようとしている構造的なケースをカバーするマッチを書くことです。定数に対する単純なマッチも良いのですが、それだけであれば、単純な辞書検索の方がずっと優れています。構造的パターンマッチの真価は、特定のオブジェクトやその一部ではなく、オブジェクトのパターンに対してマッチさせることができることです。
参考