목차
13 장

패턴 매칭 깊이

기초의 match-case 다음 단계 — 클래스 패턴과 __match_args__, 시퀀스/매핑 패턴, 캡처와 가드, 그리고 안티패턴까지 정리합니다.

3장 제어 흐름에서 match-case를 살짝 봤습니다. 단순 분기, OR 패턴, 구조 분해, 클래스 매칭, 가드 까지였습니다. 본 챕터는 그 다음 단계입니다. 모든 패턴 종류를 한곳에 정리하고, __match_args__ 같은 사용자 정의 통합 지점, 그리고 자주 빠지는 함정들까지 정리합니다.

본 챕터의 클래스 패턴은 8장 dataclass와 짝을 이룹니다 — @dataclass__match_args__를 자동 생성하기 때문입니다. 9장 typing 본격의 discriminated union도 본 챕터의 매핑 패턴과 함께 사용됩니다.

다섯 가지 패턴 카테고리 #

종류모양의미
리터럴 패턴case 200:정확한 값 일치
캡처 패턴case x:무엇이든 매치하고 변수에 바인딩
시퀀스 패턴case [a, b, *rest]:list / tuple 모양
매핑 패턴case {"key": v}:dict 모양
클래스 패턴case Point(x=0):클래스 + 속성 매치

이 다섯이 조합되어 거의 모든 경우를 표현합니다. 하나씩 깊이 봅시다.

리터럴과 값 비교 #

리터럴 패턴은 ==로 비교됩니다.

리터럴
match status:
    case 200:
        print("OK")
    case "active":
        print("active")
    case True:
        print("yes")
    case None:
        print("none")

점 표기 — 상수와 비교 #

case ABC:처럼 그냥 적으면 캡처 변수가 됩니다. 상수 비교를 하려면 점 (.)이 들어가야 합니다.

🚫 캡처로 해석됨
SUCCESS = 200

match code:
    case SUCCESS:    # ⚠ SUCCESS 라는 변수에 code 를 바인딩!
        print("ok")
    case _:
        print("else")

SUCCESS가 모든 값에 매치되어버려서 두 번째 case는 절대 실행되지 않습니다. 도구가 경고를 줍니다.

✅ 점 표기로 상수 비교
class Status:
    SUCCESS = 200
    NOT_FOUND = 404

match code:
    case Status.SUCCESS:
        print("ok")
    case Status.NOT_FOUND:
        print("not found")

상수를 매칭에 쓸 때는 enum 또는 클래스의 속성으로 두는 패턴이 안전합니다.

Enum이 가장 깔끔
from enum import Enum

class Status(Enum):
    SUCCESS = 200
    NOT_FOUND = 404

match status:
    case Status.SUCCESS:
        ...
    case Status.NOT_FOUND:
        ...

캡처 변수 #

캡처
match command:
    case x:
        print(f"받은 명령: {x}")

소문자 (정확히는 점이 없는 식별자)는 캡처입니다. 모든 값에 매치되고 변수에 바인딩합니다. _ (언더스코어)만 예외 — 와일드카드라 바인딩하지 않습니다.

캡처 변수 두 번 쓸 수 없음 #

🚫 한 패턴에 같은 이름 두 번
match (a, b):
    case (x, x):    # ✗ SyntaxError
        ...

같은 변수 이름을 한 패턴 안에 두 번 쓰면 에러입니다. 같은 값인지를 검사하려면 가드를 쓰세요.

✅ 가드로
match (a, b):
    case (x, y) if x == y:
        ...

시퀀스 패턴 #

리스트와 튜플의 모양을 패턴으로 쓸 수 있습니다.

시퀀스 패턴
match items:
    case []:
        print("빈 리스트")
    case [x]:
        print(f"하나만: {x}")
    case [x, y]:
        print(f"둘: {x}, {y}")
    case [first, *rest]:
        print(f"머리 {first}, 나머지 {rest}")
    case [*init, last]:
        print(f"앞 {init}, 마지막 {last}")
    case [first, *middle, last]:
        print(f"{first}, {middle}, {last}")

*name으로 나머지를 한 번에 받을 수 있고 — 일반 unpacking과 같습니다. 매칭은 list와 tuple을 모두 받습니다.strbytes는 시퀀스지만 매칭에서 제외됩니다 (의도치 않은 매치 방지).

문자열은 시퀀스 패턴에 매치 안 됨
match "hello":
    case [a, *rest]:
        print(a, rest)    # 매치 안 됨
    case "hello":
        print("hello!")   # 이쪽으로

길이 고정 vs 가변 #

[a, b, c] — 정확히 길이 3 [a, b, *rest]3 이상 [*items] — 모든 길이 (사실상 캡처와 같음)

매핑 패턴 #

dict의 부분 매칭입니다. 적은 키가 다 있으면 매치되고, 그 외 키는 무시됩니다.

매핑 패턴
event = {"type": "click", "x": 10, "y": 20, "extra": "ignored"}

match event:
    case {"type": "click", "x": x, "y": y}:
        print(f"({x}, {y})")
    case {"type": "key", "code": code}:
        print(f"key {code}")

매핑 패턴은 부분 매치입니다. {"type": "click"}는 “type 키가 click 인 dict 면 OK”. 다른 키가 있어도 매치됩니다.

**rest로 나머지 받기 #

나머지 받기
match config:
    case {"host": host, "port": port, **rest}:
        print(f"{host}:{port}, 그 외 옵션: {rest}")

키 자체는 리터럴이어야 #

🚫 키는 캡처 안 됨
match data:
    case {key: value}:    # ✗ key 는 캡처가 아님
        ...

매핑 패턴의 키 위치는 항상 리터럴 또는 점 표기입니다. 캡처할 수 없습니다.

클래스 패턴 #

3장 제어 흐름에서 짧게 봤던 형태:

클래스 매칭 — 기초
class Circle:
    def __init__(self, radius: float):
        self.radius = radius

match shape:
    case Circle(radius=r):
        print(f"원, 반지름 {r}")

키워드 인자 형태로 매치합니다. 속성 이름을 쓰는 것입니다.

__match_args__ — 위치 매칭 #

Point(0, 0)처럼 위치 인자로 매치하려면 클래스가 __match_args__를 알려줘야 합니다.

__match_args__
class Point:
    __match_args__ = ("x", "y")

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

match p:
    case Point(0, 0):
        print("원점")
    case Point(0, y):
        print(f"y 축 위, y={y}")
    case Point(x, 0):
        print(f"x 축 위, x={x}")
    case Point(x, y):
        print(f"({x}, {y})")

@dataclass가 자동 생성 #

8장@dataclass__match_args__자동으로 만들어줍니다. 그래서 dataclass와 패턴 매칭은 천생연분입니다.

dataclass + match
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

match p:
    case Point(0, 0):       # 자동으로 동작
        print("원점")

빌트인도 매칭 가능 — 단 한 인자만 #

int, str, bool 같은 빌트인은 위치 한 개로 값 자체를 매치합니다.

빌트인 매칭
match x:
    case int(n):
        print(f"정수 {n}")
    case str(s):
        print(f"문자열 {s}")
    case list() if not x:    # 빈 리스트
        print("빈 리스트")

int(n)은 “int 인 값을 n에 캡처"라는 의미입니다.

캡처 vs 비교 — as 키워드 #

매치된 전체 값을 변수에 받고 싶을 때 as를 씁니다.

as
match shape:
    case Circle() as c:
        register(c)
    case Square(side=s) as sq if s > 10:
        register(sq)

원본을 그대로 한 번 더 참조해야 하는 경우에 유용합니다.

OR 패턴 — | #

OR
match status:
    case 200 | 201 | 204:
        print("성공")
    case 400 | 404 | 422:
        print("클라이언트 오류")

OR 패턴 안에서 모든 변수가 같은 변수를 캡처 해야 합니다.

🚫 다른 캡처는 OR 못 씀
match val:
    case [x] | [x, y]:    # ✗ y 는 첫 패턴에 없음
        ...
✅ 같은 변수만
match val:
    case [x] | [x, _]:    # 모두 x 만 캡처
        ...

가드 — if #

3장에서 짧게 봤습니다.

가드
match (x, y):
    case (a, b) if a == b:
        ...
    case (a, b) if a > b:
        ...

가드는 모든 패턴 종류와 결합 가능 합니다. 캡처된 변수를 보고 추가 조건을 검사하는 용도입니다.

와일드카드 — _ #

_캡처 없이 매치 합니다. case _:가 default 역할을 합니다.

_
match status:
    case 200:
        print("OK")
    case _:
        print("그 외")    # default

또한 여러 위치에 와도 충돌하지 않습니다.

여러 _
match items:
    case [_, _, third]:
        print(f"세 번째: {third}")

x, x는 안 되지만 _, _는 됩니다 — _는 캡처를 안 하기 때문입니다.

결합 예 — JSON 처리 #

API 응답을 다양한 모양에 따라 분기하는 예시입니다. 24장 Pydantic v2 깊이의 discriminated union과 같은 문제를 다른 방식으로 다룹니다.

실전 — API 응답 분기
def handle(response: dict) -> str:
    match response:
        # 성공 케이스 — 데이터가 있고 type 이 정해진 모양
        case {"status": "ok", "data": {"items": list(items), "total": int(total)}}:
            return f"받음: {len(items)} 개 / 전체 {total}"

        # 페이지네이션이 있는 경우
        case {"status": "ok", "data": {"items": items, "next_cursor": str(cursor)}}:
            return f"다음 페이지 cursor: {cursor}"

        # 빈 결과
        case {"status": "ok", "data": {"items": []}}:
            return "결과 없음"

        # 에러 — 코드 + 메시지
        case {"status": "error", "code": int(code), "message": str(msg)}:
            return f"에러 {code}: {msg}"

        # 그 외
        case _:
            return "알 수 없는 응답"

같은 일을 if/elif로 풀어 쓰면 타입 검사 + 키 존재 검사 + 값 캡처가 따로따로 흩어집니다. match가 그 셋을 한 줄로 합쳐줍니다.

match-case가 어울리지 않는 경우 #

만능은 아닙니다. 단순 if가 더 읽기 쉬운 경우도 많습니다.

🚫 굳이 match
match score:
    case s if s >= 90:
        grade = "A"
    case s if s >= 80:
        grade = "B"
    case s if s >= 70:
        grade = "C"
    case _:
        grade = "F"
✅ if/elif가 더 자연
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

판단 기준: 모양으로 분기하는가, 값 범위로 분기하는가. 모양이 키워드면 match, 단순 비교면 if/elif가 보통 낫습니다.

연습문제 #

  1. @dataclassShape의 자식 Circle(radius: float), Rectangle(width: float, height: float), Triangle(base: float, height: float) 셋을 만드세요. def area(s: Shape) -> float:match 한 줄로 작성해 각 도형의 넓이를 반환합니다 (__match_args__ 자동 생성을 활용).
  2. API 응답 dict {"status": "ok", "data": [...]} / {"status": "error", "code": int, "message": str} 두 모양을 처리하는 함수를 작성하세요. data 안의 리스트가 비어 있는 경우 / 100개 이상인 경우를 추가 분기로 분리합니다 (매핑 패턴 + 시퀀스 패턴 + 가드 결합).
  3. 옛 스타일 코드 (if isinstance(x, A): ...를 5단 이상 적은 코드)를 본 챕터의 match로 다시 작성해 보세요. 라인 수가 얼마나 줄고 의도가 얼마나 분명해졌는지 비교합니다.

한 줄 요약: 패턴 5개 — 리터럴 / 캡처 / 시퀀스 / 매핑 / 클래스. 상수 비교는 점 표기(Status.OK) 필수. 시퀀스는 list/tuple만, str/bytes는 제외. 매핑은 부분 매치, **rest로 나머지. 클래스는 __match_args__ 위치 매칭 (@dataclass 자동). as로 전체 캡처, |로 OR (같은 변수만), 가드는 모든 패턴 결합. 모양 분기는 match, 값 범위 분기는 if/elif.

다음 챕터 #

다음 14장 비동기 입문 (asyncio)이 2부의 마지막 — **asyncio**와 async/await를 다룹니다. 11장에서 짧게 본 코루틴이 본격적으로 등장하는 단계입니다. 3부의 18장 비동기 깊이의 토대가 됩니다.

X