4.2 match-case (Python 3.10+) — 구조적 패턴 매칭
Python 3.10에서 도입된 match-case는 단순한 switch문이 아닙니다. 구조적 패턴 매칭(Structural Pattern Matching) 으로, 데이터의 구조 자체를 패턴으로 표현할 수 있습니다. PEP 634에서 정의된 이 기능은 함수형 프로그래밍의 패턴 매칭을 Python에 가져온 것입니다.
기본 문법
# 문법: match <표현식>: / case <패턴>: / 본문
command = "quit"
match command:
case "quit":
print("프로그램 종료")
case "help":
print("도움말 표시")
case "start":
print("시작")
case _: # 와일드카드 패턴: 모든 값에 매칭 (default)
print(f"알 수 없는 명령: {command}")
match는 값을 위에서부터 차례로 패턴과 비교하여 첫 번째로 매칭되는 case 블록을 실행합니다. if-elif와 달리, 매칭되면 자동으로 종료되며 break가 필요 없습니다.
리터럴 패턴
status_code = 404
match status_code:
case 200:
message = "OK"
case 201:
message = "Created"
case 400:
message = "Bad Request"
case 401:
message = "Unauthorized"
case 403:
message = "Forbidden"
case 404:
message = "Not Found"
case 500:
message = "Internal Server Error"
case _:
message = f"Unknown status: {status_code}"
print(message) # Not Found
# 문자열 리터럴 패턴
lang = "python"
match lang:
case "python":
print("Python — 범용 언어")
case "rust":
print("Rust — 시스템 프로그래밍")
case "javascript":
print("JavaScript — 웹 프론트엔드")
case _:
print(f"{lang} — 기타 언어")
변수 캡처 패턴
# case에서 변수를 선언하면 값을 캡처함
point = (3, 7)
match point:
case (0, 0):
print("원점")
case (x, 0):
print(f"X축 위의 점: x={x}")
case (0, y):
print(f"Y축 위의 점: y={y}")
case (x, y):
print(f"일반 점: ({x}, {y})") # 출력: 일반 점: (3, 7)
# 주의: 캡처 패턴과 리터럴 패턴의 구분
# 이미 정의된 변수를 패턴으로 쓰면 리터럴 비교가 아닌 캡처가 됨
HTTP_OK = 200
status = 404
match status:
# case HTTP_OK: # 이렇게 쓰면 리터럴 200이 아닌 변수 캡처!
case 200:
print("성공")
case 404:
print("찾을 수 없음") # 출력됨
시퀀스 패턴
# 리스트/튜플의 구조를 패턴으로 표현
def describe_list(items: list) -> str:
match items:
case []:
return "빈 리스트"
case [single]:
return f"단일 원소: {single}"
case [first, second]:
return f"두 원소: {first}, {second}"
case [first, *rest]:
return f"첫 원소: {first}, 나머지 {len(rest)}개"
print(describe_list([])) # 빈 리스트
print(describe_list([42])) # 단일 원소: 42
print(describe_list([1, 2])) # 두 원소: 1, 2
print(describe_list([1, 2, 3, 4])) # 첫 원소: 1, 나머지 3개
# *rest로 나머지 원소 수집
def process_command(args: list[str]) -> str:
match args:
case ["go", direction]:
return f"{direction} 방향으로 이동"
case ["go", direction, *extra]:
return f"{direction} 방향으로 이동 (추가 인자: {extra})"
case ["pick", "up", item]:
return f"{item} 획득"
case ["drop", item, *location]:
dest = " ".join(location) if location else "현재 위치"
return f"{item}를 {dest}에 내려놓음"
case _:
return f"알 수 없는 명령: {args}"
print(process_command(["go", "north"]))
print(process_command(["go", "east", "fast"]))
print(process_command(["pick", "up", "sword"]))
print(process_command(["drop", "shield", "safe", "room"]))
매핑 패턴
# 딕셔너리 구조에 매칭
def handle_event(event: dict) -> str:
match event:
case {"type": "click", "button": "left", "x": x, "y": y}:
return f"왼쪽 클릭: ({x}, {y})"
case {"type": "click", "button": "right", "x": x, "y": y}:
return f"오른쪽 클릭: ({x}, {y})"
case {"type": "keypress", "key": key}:
return f"키 입력: {key}"
case {"type": "scroll", "delta": delta}:
direction = "위" if delta > 0 else "아래"
return f"스크롤 {direction}: {abs(delta)}"
case {"type": event_type}:
return f"알 수 없는 이벤트 타입: {event_type}"
case _:
return "잘못된 이벤트 형식"
events = [
{"type": "click", "button": "left", "x": 100, "y": 200},
{"type": "keypress", "key": "Enter"},
{"type": "scroll", "delta": -3},
]
for ev in events:
print(handle_event(ev))
# 매핑 패턴은 추가 키가 있어도 매칭됨 (부분 매칭)
data = {"action": "login", "user": "alice", "timestamp": 1234567890}
match data:
case {"action": "login", "user": username}:
print(f"로그인: {username}") # timestamp가 있어도 매칭됨
클래스 패턴
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
top_left: Point
bottom_right: Point
def describe_shape(shape) -> str:
match shape:
case Point(x=0, y=0):
return "원점"
case Point(x=x, y=0):
return f"X축 위 점: x={x}"
case Point(x=x, y=y):
return f"점: ({x}, {y})"
case Circle(center=Point(x=cx, y=cy), radius=r):
return f"원: 중심=({cx}, {cy}), 반지름={r}"
case Rectangle(top_left=Point(x=x1, y=y1), bottom_right=Point(x=x2, y=y2)):
width = abs(x2 - x1)
height = abs(y2 - y1)
return f"사각형: 너비={width}, 높이={height}"
case _:
return f"알 수 없는 도형: {type(shape).__name__}"
shapes = [
Point(0, 0),
Point(5, 0),
Point(3, 4),
Circle(Point(1, 2), 5.0),
Rectangle(Point(0, 10), Point(8, 0)),
]
for shape in shapes:
print(describe_shape(shape))
OR 패턴
# | 연산자로 여러 패턴을 OR로 결합
def classify_response(code: int) -> str:
match code:
case 200 | 201 | 202 | 204:
return "성공 (2xx)"
case 301 | 302 | 307 | 308:
return "리다이렉트 (3xx)"
case 400 | 401 | 403 | 404 | 422:
return "클라이언트 오류 (4xx)"
case 500 | 502 | 503 | 504:
return "서버 오류 (5xx)"
case _:
return f"기타: {code}"
for code in [200, 302, 404, 500, 999]:
print(f" {code}: {classify_response(code)}")
# OR 패턴과 변수 캡처 조합
def handle_direction(direction: str) -> str:
match direction.lower():
case "n" | "north" | "up":
return "북쪽으로 이동"
case "s" | "south" | "down":
return "남쪽으로 이동"
case "e" | "east" | "right":
return "동쪽으로 이동"
case "w" | "west" | "left":
return "서쪽으로 이동"
case other:
return f"알 수 없는 방향: {other}"
print(handle_direction("N"))
print(handle_direction("south"))
print(handle_direction("right"))
가드(Guard): case x if 조건
# case 패턴 뒤에 if 조건(가드)을 추가할 수 있음
def classify_number(n: int | float) -> str:
match n:
case 0:
return "영"
case n if n < 0:
return f"음수: {n}"
case n if n % 2 == 0:
return f"양의 짝수: {n}"
case n:
return f"양의 홀수: {n}"
for num in [0, -5, 4, 7]:
print(f" {num}: {classify_number(num)}")
# 가드와 시퀀스 패턴 결합
def validate_coordinates(coords: tuple) -> str:
match coords:
case (lat, lon) if -90 <= lat <= 90 and -180 <= lon <= 180:
return f"유효한 좌표: ({lat}, {lon})"
case (lat, lon):
return f"범위 초과: lat={lat}, lon={lon}"
case _:
return "잘못된 형식"
print(validate_coordinates((37.5, 126.9))) # 서울
print(validate_coordinates((100, 0))) # 위도 초과
실전 예제 1: 명령어 파서
from dataclasses import dataclass
from typing import Any
@dataclass
class Command:
name: str
args: list[Any]
flags: dict[str, Any]
def parse_and_execute(cmd: Command) -> str:
match cmd:
case Command(name="help", args=[], flags={}):
return "사용 가능한 명령어: help, quit, create, delete, list"
case Command(name="quit" | "exit", args=_, flags=_):
return "프로그램을 종료합니다"
case Command(name="create", args=[name], flags={"type": item_type}):
return f"{item_type} 타입의 '{name}' 생성"
case Command(name="create", args=[name], flags={}):
return f"기본 타입의 '{name}' 생성"
case Command(name="delete", args=[name], flags={"force": True}):
return f"'{name}' 강제 삭제"
case Command(name="delete", args=[name], flags=_):
return f"'{name}' 삭제 (확인 필요)"
case Command(name="list", args=[], flags={"verbose": True}):
return "상세 목록 표시"
case Command(name="list", args=_, flags=_):
return "간략 목록 표시"
case Command(name=unknown_cmd):
return f"알 수 없는 명령: {unknown_cmd}"
commands = [
Command("help", [], {}),
Command("create", ["my_file"], {"type": "document"}),
Command("delete", ["old_data"], {"force": True}),
Command("list", [], {"verbose": True}),
]
for cmd in commands:
print(f" {cmd.name}: {parse_and_execute(cmd)}")
실전 예제 2: AST 노드 처리
# 간단한 수식 AST를 match-case로 평가
from dataclasses import dataclass
from typing import Union
@dataclass
class Number:
value: float
@dataclass
class BinaryOp:
op: str
left: "Expr"
right: "Expr"
@dataclass
class UnaryOp:
op: str
operand: "Expr"
Expr = Union[Number, BinaryOp, UnaryOp]
def evaluate(expr: Expr) -> float:
match expr:
case Number(value=v):
return v
case UnaryOp(op="-", operand=e):
return -evaluate(e)
case BinaryOp(op="+", left=l, right=r):
return evaluate(l) + evaluate(r)
case BinaryOp(op="-", left=l, right=r):
return evaluate(l) - evaluate(r)
case BinaryOp(op="*", left=l, right=r):
return evaluate(l) * evaluate(r)
case BinaryOp(op="/", left=l, right=r):
divisor = evaluate(r)
if divisor == 0:
raise ZeroDivisionError("0으로 나눌 수 없습니다")
return evaluate(l) / divisor
case BinaryOp(op=op):
raise ValueError(f"지원하지 않는 연산자: {op}")
case _:
raise TypeError(f"알 수 없는 노드 타입: {type(expr)}")
# (3 + 4) * 2
expr = BinaryOp("*",
BinaryOp("+", Number(3), Number(4)),
Number(2)
)
print(f"(3 + 4) * 2 = {evaluate(expr)}") # 14.0
# -(10 / 2)
expr2 = UnaryOp("-", BinaryOp("/", Number(10), Number(2)))
print(f"-(10 / 2) = {evaluate(expr2)}") # -5.0
고수 팁
1. __match_args__ 로 클래스 패턴 위치 인자 순서 정의
class Color:
__match_args__ = ("red", "green", "blue") # 위치 패턴 순서 정의
def __init__(self, red: int, green: int, blue: int):
self.red = red
self.green = green
self.blue = blue
color = Color(255, 0, 0)
match color:
case Color(255, 0, 0):
print("빨간색") # 위치 인자로 매칭 가능
case Color(0, 255, 0):
print("초록색")
case Color(r, g, b):
print(f"기타 색상: rgb({r}, {g}, {b})")
2. AS 패턴으로 매칭된 값에 이름 붙이기
def process(data):
match data:
case {"items": [first, *_] as items_list}:
print(f"첫 번째 항목: {first}, 전체: {items_list}")
case [int() | float() as num]:
print(f"숫자 하나: {num}")
process({"items": [1, 2, 3]})
process([42])
3. 성능 참고
match-case는 내부적으로 if-elif와 유사하게 동작합니다. 단, 클래스 패턴과 매핑 패턴은 타입 체크와 속성 접근이 추가되므로 매우 많은 케이스에서는 딕셔너리 디스패치가 더 빠를 수 있습니다.