4.2 match文

(1/1)
分かれ道に立つ人のイラスト(女性)
Python には、条件分岐として、他言語にある switch文が存在しない。if文で代用できるというのがその理由なのだが、代わりに Python 3.10match文が実装された。
今回は、switch文とはひと味違う match文の書き方について学ぶ。

目次

サンプル・プログラム

月の大小を判定する(if文)

グレゴリオ暦では、31日ある月を大の月、それ以外を小の月と呼ぶ。具体的には、1月、3月、5月、7月、8月、10月、12月が大の月で、2月、4月、6月、9月、11月が小の月である。
プログラム "isBigMonth1.py" は、前回学んだ if文を使って、入力した月が大の月が小の月かを判定する。
def isBigMonth1(month):
	"""月の大小を判定する(if文)
	
	Args:
		month(int): 月
	
	Returns:
		bool: 判定成否, bool: True 大の月 / False 小の月
	"""
	if (month == 1):
		return True, True
	elif (month == 2):
		return True, False
	elif (month == 3):
		return True, True
	elif (month == 4):
		return True, False
	elif (month == 5):
		return True, True
	elif (month == 6):
		return True, False
	elif (month == 7):
		return True, True
	elif (month == 8):
		return True, True
	elif (month == 9):
		return True, False
	elif (month == 10):
		return True, True
	elif (month == 11):
		return True, False
	elif (month == 12):
		return True, True
	else:
		return False, False
エラーメッセージを除くと、True(大の月)を戻すか False(小の月)を戻すかの2通りしかない。にもかかわらず、12の月すべての場合分けをするのは、力業に見える。しかし、月が偶数か奇数かによって大小を判定することもできない。
条件演算子(OR演算子)を使えばブロックを減らすことができるが、
(month == 1) or (month == 3) or (month == 5) or (month == 7) or (month == 8) or (month == 10) or (month == 12)
のように条件式が長くなってしまう。

月の大小を判定する(match文)

Python 3.10で実装された match文を使ったプログラムが "isBigMonth2.py" である。
def isBigMonth2(month):
	"""月の大小を判定する(match文)
	
	Args:
		month(int): 月
	
	Returns:
		bool: 判定成否, bool: True 大の月 / False 小の月
	"""
	match month:
		case 1:
			return True, True
		case 2:
			return True, False
		case 3:
			return True, True
		case 4:
			return True, False
		case 5:
			return True, True
		case 6:
			return True, False
		case 7:
			return True, True
		case 8:
			return True, True
		case 9:
			return True, False
		case 10:
			return True, True
		case 11:
			return True, False
		case 12:
			return True, True
		case _:
			return False, False
match文に並んで書いた式が、case句に続く式に合致するとき、そのブロックを実行する。
if文より式は簡単になったが、依然として、月の数だけ returnを書くことに変わりはない。
JavaScriptやPHPでは switch~case文の中で使える break句があるため、もう少し簡便に書くことができる。残念ながら、Python ではそれもできない。逆に言えば、switch文if文で代替できるので実装する必要はない、という Pythonの考え方は正しい。

では、match文を使うのは、どのような場合か――。

2次元座標の象限判定

4つの象限
2次元座標 (x, y) は左図のように、4つの象限に分けられる。これに加え、その座標が X軸、または Y軸上にあるかどうかを判定するプログラムが "dispQuadra.py" だ。
def getQuadra(x, y):
	"""エラー・メッセージを表示してプログラムを終了する.
	
	Args:
		x(float): X座標
		y(float): Y座標
	
	Returns:
		str: 位置情報
	"""
	match (x, y):
		case (0, 0):
			return "原点"
		case (_, 0):
			return "X軸上"
		case (0, _):
			return "Y軸上"
		case (x, y) if x > 0 and y > 0:
			return "第1象限"
		case (x, y) if x < 0 and y > 0:
			return "第2象限"
		case (x, y) if x < 0 and y < 0:
			return "第3象限"
		case (x, y) if x > 0 and y < 0:
			return "第4象限"
		case _:
			return "無効な座標"
match文が2次元座標の数値(浮動小数)のペア (x, y) を式としていることに注目してほしい。
これに対応する case句は8つ――
  1. (0, 0):原点
  2. (_, 0):X軸上。アンダースコアはワイルドカードパターンといって、この場合は、すでに登場した0以外の数値にマッチする。つまり、Y座標が0の直線を意味しており、すなわちX軸上にある座標にマッチする。
  3. (0, _):Y軸上。すでに登場した0以外の数値にマッチするから、X座標が0の直線を意味しており、すなわちY軸上にある座標にマッチする。
  4. (x, y) if x > 0 and y > 0:(x, y) が if文の条件式でTrueのときにマッチするから、第1象限になる。
  5. (x, y) if x < 0 and y > 0:(x, y) が if文の条件式でTrueのときにマッチするから、第2象限になる。
  6. (x, y) if x < 0 and y < 0:(x, y) が if文の条件式でTrueのときにマッチするから、第2象限になる。
  7. (x, y) if x > 0 and y < 0:(x, y) が if文の条件式でTrueのときにマッチするから、第2象限になる。
  8. _:これまでマッチしなかった(ワイルドカードパターン)2次元座標。具体的には、xやyに数値以外が入っている場合。
勇者のイラスト(男性)
このように match文if文と組み合わせることができる点で、他言語の switch~case文とはひと味違った条件分岐を実装できる。switch は文字通り〈切り替える〉というイメージだが、match はマッチするケースを網羅的に書くことができる。

match文の性質を応用することで、たとえばアドベンチャーゲームで、プレイヤーの経験知やMPなどのパラメーターに応じてシナリオを分岐させることができる。さらに if文 を組み合わせることで、パラメーターの値の範囲に応じて分岐させることも可能になる。

練習問題

次回予告

複利計算や通日計算のように、繰り返し計算を行うときに for文が活躍する。
さらに、for文の中に if文などの制御文を組み込んだり、break文 を使って脱出することで応用範囲が広がる。
次回は、繰り返し制御ができる for文を学ぶ。

コラム:定義域・値域とシステム・スコープ

Python には calendaモジュールというカレンダー計算に関わる便利なモジュールがあり、これだけで、うるう年の判定から万年カレンダーの作成まで、いろいろなことができる。calendar._monthlen メソッドを使えば、指定した年月の日数を求めることができる。
# 月の日数を取得する
day = calendar._monthlen(year, month)
このように calendar._monthlen メソッドには西暦年と月を渡すのだが、ここで注意が必要なのは、入力値バリデーションだ。メソッドを数学の関数と考え、引数として渡せる値の範囲を定義域とみなそう(戻り値は値域とみなせる)。
月の値は、1から12の自然数であることは自明だ。よって、入力値バリデーションとして使ってきたユーザー関数 validateNumber をそのまま使えばいい。問題は西暦年の方だ――。

2000年問題を経験した古参プログラマはお気づきの通り、プログラミング言語が用意している西暦年の範囲には必ず制約がある。それは、OSの制約だったり、ハードウェア(主にCPU)の制約だったりすることが多い。そして、言語仕様の隅の隅まで読まないと、その制約を拾うことができない。
Python の公式 https://docs.python.org/ja/3/library/calendar.html を読むと、明記はされていないようだが、どうやら西暦年の値域は datetimeモジュールに依存しているようだ。
そこで、https://docs.python.org/ja/3/library/datetime.html#module-datetime を読んでみると、西暦年の定義域は datetime.MINYEAR 以上、datetime.MAXYEAR 以下であることがわかる。手元のWindowsの実行環境では、1~9999が定義域だ。Python はintの桁数制約がないにもかかわらず、西暦年の制約が意外に厳しい(OSやハードの制約ではないだろう)。

さて、システム開発では、そのシステムを適用する範囲(スコープ)を要件定義に明記する。そのスコープを満たす開発環境を用意する。
Pythoncalendarモジュールについては言えば、事務系の業務システムであれば、西暦1年から9999年まで扱うことができれば十分だろう。だが、世界史のカレンダーを作ったり、地質学や天文学のシステムとなると、これでは不十分だ。ガンダム世界や超人ロック世界のカレンダー計算はできるが、アシモフの銀河帝国シリーズは定義域外になってしまう。
それでも Python で開発するという方針であれば、自力でカレンダー計算クラスを作ることになる。Python のintには値域の制約がないので、実は、それほど難しい話ではない。極端な話、1億年離れた日付の差分計算を日数単位で行おうとすると(計算結果なので値域)、365億日を超える。もしintが32ビットだと、オーバーフローを起こしてしまう。Python にはその心配がないからだ。
(この項おわり)
header