모던 파이썬 중급 #6 패턴 매칭 깊이
기초 #3에서 match-case를 살짝 봤습니다. 단순 분기, OR 패턴, 구조 분해, 클래스 매칭, 가드까지 봤습니다. 이번 글은 그 다음 단계입니다. 모든 패턴 종류를 한곳에 정리하고, __match_args__ 같은 사용자 정의 통합 부분, 그리고 자주 빠지는 함정들까지 정리합니다.
다섯 가지 패턴 카테고리 #
| 종류 | 모양 | 의미 |
|---|---|---|
| 리터럴 패턴 | 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 또는 클래스의 속성으로 두는 패턴이 안전합니다.
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을 모두 받습니다. 단 str과 bytes는 시퀀스지만 매칭에서 제외됩니다 (의도치 않은 매치 방지).
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__를 알려줘야 합니다.
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가 자동 생성
#
#1의 @dataclass가 __match_args__를 자동으로 만들어줍니다. 그래서 dataclass는 패턴 매칭과 가장 자연스럽게 맞물립니다.
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를 씁니다.
match shape:
case Circle() as c:
register(c)
case Square(side=s) as sq if s > 10:
register(sq)원본을 그대로 한 번 더 참조해야 하는 경우에 유용합니다.
OR 패턴 — |
#
match status:
case 200 | 201 | 204:
print("성공")
case 400 | 404 | 422:
print("클라이언트 오류")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 응답을 다양한 모양에 따라 분기하는 예시입니다.
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 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 score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"판단 기준: 모양으로 분기하는가, 값 범위로 분기하는가. 모양이 키워드면 match, 단순 비교면 if/elif가 보통 낫습니다.
정리 #
이번 글에서 정리한 것:
- 다섯 패턴 — 리터럴, 캡처, 시퀀스, 매핑, 클래스
- 점 표기 (
Status.OK)로 상수 비교, 단순 이름은 캡처 - 시퀀스:
[a, *rest, b], str/bytes는 매치 제외 - 매핑: 부분 매치, 키는 리터럴,
**rest로 나머지 - 클래스 패턴:
__match_args__로 위치 매칭, dataclass가 자동 생성 - 빌트인 매칭:
int(n),str(s)형태 as로 전체 값 캡처|로 OR (모든 갈래가 같은 변수만 캡처)- 가드
if는 모든 패턴과 결합 가능 _는 캡처 없이, 여러 위치에 중복 가능- 모양 분기는 match, 값 범위 분기는 if/elif
다음 글(#7 비동기 입문)이 중급 시리즈의 마지막 — **asyncio**와 async/await를 다룹니다. 위에서 짧게 본 코루틴이 본격적으로 등장하는 단계입니다.