F-3: 패턴 매칭과 최신 문법 — 값의 모양으로 가지치기
목차 29
안녕하세요, 홍순구 튜터입니다. 지난 시간엔 값을 하나씩 흘려보내는 제너레이터, 함수를 감싸는 데코레이터, with의 정체인 컨텍스트 매니저까지 모던 파이썬의 세 기둥을 세웠어요.
지난 시간을 닫으면서 제가 슬쩍 예고를 했죠. "갈래를 나누는 일 자체를 더 우아하게 푸는 도구가 있다"고요. 우리가 A-3에서 처음 만난 if/elif/else 기억나시죠? 조건이 두세 개일 땐 깔끔하지만, "이 값이 이것이면 A, 저것이면 B, 리스트 모양이면 C, 딕셔너리 모양이면 D…"처럼 한 값의 여러 모양을 따질 땐 elif가 길게 늘어져요.
오늘은 그 길어진 사슬을 값의 모양(pattern)으로 깔끔하게 가지치는 match/case를 배워요. 신호등 앞에서 "빨강이면 멈춤, 초록이면 출발, 노랑이면 주의"를 한눈에 정리하듯, 값을 죽 나열한 모양들과 맞춰보는 도구예요. 더불어 조건을 검사하면서 그 값을 바로 변수에 담는 깜찍한 기호 walrus(:=)와, 파이썬 3.14가 오타까지 짚어주는 친절한 에러 메시지까지 만나요.
오늘의 여정
match / case → if / elif 사슬을 값의 모양으로 가지치기
패턴 4종 → 리터럴 · 시퀀스 · 매핑 · 가드(조건)
walrus := → 검사하면서 그 값을 바로 변수에 담기
향상된 에러 → 3.14가 오타까지 짚어주는 친절한 에러 메시지
💡 오늘 수업의 핵심 — "값의 모양에 따라 가지를 친다"
match/case는 한 값을 여러 모양과 맞춰보며 가지를 쳐요. 리터럴(정확한 값)·시퀀스(리스트 구조)·매핑(딕셔너리 구조)·가드(조건)까지, if/elif로 길어지던 분기를 한눈에 읽히게 정리해요.
🎯 학습 목표
match/case로 값의 모양에 따라 가지를 치고,case _와일드카드와 소프트 키워드를 이해해요.- 리터럴·OR·캡처·가드·시퀀스·매핑 패턴으로 명령 파서와 이벤트 분기를 만들어요.
- walrus(
:=)로 검사와 값 담기를 한 번에 처리해요. - 파이썬 3.14의 향상된 에러 메시지를 읽고, 최신 문법(t-string·괄호 없는
except)을 살펴봐요.
Step 1: "if-elif 사슬을 우아하게 — match/case 첫걸음"
A-3에서 우리는 if/elif/else로 갈래를 나눴어요. 계정 상태(status) 하나를 받아 설명을 돌려주는 함수를 예전 방식으로 써볼게요.
# src/modern/f3_match_intro.py
def describe_status_if(status):
if status == "active":
return "활동 중인 계정이에요."
elif status == "paused":
return "잠시 멈춘 계정이에요."
elif status == "banned":
return "정지된 계정이에요."
else:
return "알 수 없는 상태예요."
status == "..."를 네 번 반복했어요. 같은 변수(status)를 매번 다시 적으면서 값만 바꿔 비교하고 있죠. 이걸 match/case로 바꾸면 이렇게 돼요.
def describe_status(status):
match status:
case "active":
return "활동 중인 계정이에요."
case "paused":
return "잠시 멈춘 계정이에요."
case "banned":
return "정지된 계정이에요."
case _:
return "알 수 없는 상태예요."
읽어볼까요? "status를 가지고(match status:), 그게 "active" 모양이면(case "active":)…" 하고 한 변수의 여러 모양을 죽 나열해요. status ==를 반복하지 않아도 돼서 한눈에 들어와요.
맨 아래 case _:가 눈에 띄죠. 밑줄 하나(_)는 와일드카드(wildcard)예요. "위에서 아무것도 안 맞으면 여기로"라는 뜻이라, if/elif의 else와 같은 역할이에요. 위에서부터 차례로 맞춰보다가 처음 맞는 case 하나만 실행하고 끝나요.
for s in ["active", "paused", "banned", "ghost"]:
print(s, "→", describe_status(s))
# active → 활동 중인 계정이에요.
# paused → 잠시 멈춘 계정이에요.
# banned → 정지된 계정이에요.
# ghost → 알 수 없는 상태예요.
"ghost"는 어떤 case에도 안 맞아서 case _로 떨어졌어요.
한 가지 더 알아둘 게 있어요. match와 case는 사실 완전한 예약어가 아니라 소프트 키워드(soft keyword)예요. 즉 변수 이름으로도 쓸 수 있어요.
match = "match는 변수 이름으로도 OK"
case = "case도 마찬가지"
print(match)
print(case)
# match는 변수 이름으로도 OK
# case도 마찬가지
for나 if 같은 진짜 예약어는 변수 이름으로 절대 못 쓰는데, match는 됩니다. 파이썬이 "match 다음에 값이 오고 :로 끝나며 아래에 case가 있으면" 패턴 매칭 문법으로, 그냥 match = ...이면 변수로 알아서 구분해요. 덕분에 옛날부터 match라는 변수를 쓰던 코드도 안 깨지고 잘 돌아가요.
💡 한 줄 정리
match 값: 아래에 case 모양:을 죽 나열하면, 값을 위에서부터 모양과 맞춰보다 처음 맞는 가지 하나만 실행해요. case _는 else처럼 "그 외 전부"고, match/case는 변수 이름으로도 쓸 수 있는 소프트 키워드예요.
🙋 학생 질문 — "튜터님, 그냥 if/elif 쓰면 안 되나요? 굳이 match를 왜 배워요?"
if/elif도 전혀 문제없어요. 조건이 두세 개거나, 서로 다른 변수를 검사할 땐(if a > 0 and b < 10:) 오히려 if가 자연스러워요.
match가 빛나는 자리는 따로 있어요. 한 값을 두고 그게 어떤 모양인지를 여러 갈래로 따질 때예요. 오늘 뒤에서 보겠지만, match는 단순히 값이 같은지만 보는 게 아니라 "리스트인데 원소가 두 개인 모양", "딕셔너리인데 type 키가 있는 모양"처럼 구조까지 꿰뚫어 봐요. 이건 if로는 한 줄에 못 적어요. 그러니 "값이 같은지만 보면 if, 값의 모양·구조를 보면 match" 정도로 기억해두면 좋아요.
Step 2: "값으로 가지치기 — 리터럴 패턴과 |"
가장 단순한 패턴부터 정리해요. Step 1에서 본 case "active":처럼, 정확한 값 하나를 적는 걸 리터럴 패턴(literal pattern)이라고 해요. 문자열뿐 아니라 숫자도 돼요.
이번엔 인스타 앱에서 들어오는 명령을 처리하는 함수를 만들어볼게요. 오늘의 큰 그림인 명령 파서의 첫걸음이에요.
# src/modern/f3_literal.py
def handle_command(command):
match command:
case "like":
return "좋아요를 눌렀어요."
case "follow" | "unfollow":
return "팔로우 상태를 바꿨어요."
case "share" | "save" | "report":
return "메뉴 동작을 처리했어요."
case _:
return "모르는 명령이에요."
새로 등장한 기호는 세로 막대 |예요. case "follow" | "unfollow":는 "follow 또는 unfollow면"이라는 뜻이라, 여러 값을 한 case로 묶어요. 이걸 OR 패턴이라고 불러요. A-2에서 배운 or와 결이 비슷하지만, 여기선 패턴들 사이를 잇는 기호예요.
for c in ["like", "follow", "unfollow", "save", "logout"]:
print(c, "→", handle_command(c))
# like → 좋아요를 눌렀어요.
# follow → 팔로우 상태를 바꿨어요.
# unfollow → 팔로우 상태를 바꿨어요.
# save → 메뉴 동작을 처리했어요.
# logout → 모르는 명령이에요.
follow와 unfollow가 같은 가지로, save는 세 개를 묶은 가지로 떨어졌어요. 숫자에도 똑같이 써요. HTTP 응답 코드를 분류하는 함수예요.
def http_status(code):
match code:
case 200:
return "성공"
case 404:
return "없는 페이지"
case 500 | 502 | 503:
return "서버 오류"
case _:
return "기타"
500 | 502 | 503처럼 여러 숫자를 한 줄로 묶으면, "서버 오류 계열"을 깔끔하게 한 가지로 모을 수 있어요.
💡 한 줄 정리
case "like":처럼 정확한 값을 적는 게 리터럴 패턴이에요. case "a" | "b":의 |(OR 패턴)는 여러 값을 한 가지로 묶어요. 문자열·숫자 모두 돼요.
🙋 학생 질문 — "튜터님, case _ 를 빼먹으면 어떻게 되나요?"
좋은 질문이에요. case _가 없는데 어떤 case에도 안 맞으면, match는 그냥 아무것도 안 하고 지나가요. 에러가 나는 게 아니라, 함수라면 return을 못 만나서 None이 돌아와요(C-1에서 본 그 None이에요).
그래서 "혹시 모를 나머지"를 꼭 처리하고 싶으면 case _를 두는 습관이 좋아요. 반대로 "정해진 값만 들어온다"고 확신하는 자리라면 일부러 case _를 빼서 "예상 밖 값은 조용히 통과"하게 두기도 해요. 상황에 따라 골라 쓰면 돼요.
Step 3: "값을 붙잡고 조건 걸기 — 캡처 패턴과 가드"
지금까지는 값이 정확히 무엇인지만 봤어요. 그런데 "좋아요가 100개 이상이면 인기글"처럼 범위로 나누고 싶을 땐 어떻게 할까요? 두 가지 새 도구가 필요해요. 값을 변수에 붙잡는 캡처 패턴과, 거기에 조건을 더하는 가드예요.
# src/modern/f3_guard.py
def grade_likes(likes):
match likes:
case 0:
return "아직 좋아요가 없어요."
case n if n >= 10000:
return f"인플루언서급! ({n}개)"
case n if n >= 1000:
return f"인기 게시물 ({n}개)"
case n if n >= 100:
return f"좋아요 {n}개"
case n:
return f"좋아요 {n}개 (조금 더 힘내요)"
case n:을 보세요. n은 미리 정해둔 값이 아니라 변수예요. 이렇게 적으면 "어떤 값이든 받아서 그걸 n이라는 이름으로 붙잡아"라는 뜻이에요. 이게 캡처 패턴(capture pattern)이에요. Step 1의 case _(와일드카드)와 닮았지만, _는 값을 버리고 n은 값을 담아둬서 case 몸통에서 {n}으로 쓸 수 있어요.
여기에 if를 붙인 case n if n >= 10000:이 가드(guard)예요. "이 모양이면서(case n), 게다가 이 조건까지 맞으면(if n >= 10000)" 통과해요. 모양만으로 부족할 때 조건을 한 겹 더 거는 거죠.
for likes in [0, 50, 100, 1500, 23000]:
print(likes, "→", grade_likes(likes))
# 0 → 아직 좋아요가 없어요.
# 50 → 좋아요 50개 (조금 더 힘내요)
# 100 → 좋아요 100개
# 1500 → 인기 게시물 (1500개)
# 23000 → 인플루언서급! (23000개)
여기서 순서가 아주 중요해요. match는 위에서부터 내려오며 처음 맞는 가지에서 멈춰요. 그래서 가장 좁은 조건(>= 10000)을 위에, 넓은 조건(>= 100)을 아래에 둬요. 만약 >= 100을 맨 위에 두면 23000도 거기서 먼저 걸려 "좋아요 23000개"가 돼버려요.
likes = 23000
case 0: → 0 아님, 통과
case n if n >= 10000: → 23000 >= 10000 참! 여기서 멈춤
case n if n >= 1000: (실행 안 됨)
...
맨 아래 case n:은 가드가 없는 캡처라 "나머지 전부"를 받아요. 0보다 크고 100보다 작은 값들이 여기로 떨어져요.
💡 한 줄 정리
case n:은 어떤 값이든 변수 n으로 붙잡는 캡처 패턴이에요(값을 버리는 _와 달리 몸통에서 쓸 수 있어요). case n if 조건:의 if는 가드라 모양에 조건을 한 겹 더 걸어요. 좁은 조건을 위에, 넓은 조건을 아래에 둬요.
🙋 학생 질문 — "튜터님, case n 처럼 변수를 쓰면 위에 있는 "active" 같은 값도 다 잡아버리지 않나요?"
정확한 걱정이에요. case n:은 무엇이든 잡기 때문에, 그 아래에 적은 case들은 영영 실행될 기회가 없어요. 그래서 캡처 패턴(case n:)이나 와일드카드(case _:)는 거의 항상 맨 아래에 둬요.
실제로 case n:을 위에 두고 그 아래 case 0:을 적으면, 파이썬이 아예 SyntaxError: name capture 'n' makes remaining patterns unreachable(이름 캡처 n 때문에 남은 패턴에 도달할 수 없다) 에러를 내며 실행을 막아줘요. "여긴 못 와요"라고 미리 막아주는 셈이죠. 그러니 구체적인 모양은 위에, 포괄적인 캡처는 아래에 — 이 순서만 지키면 돼요.
Step 4: "구조를 분해하다 — 시퀀스 패턴"
이제 패턴 매칭의 진짜 매력으로 들어가요. match는 값이 같은지만 보는 게 아니라 구조를 꿰뚫어 봐요. 리스트나 튜플의 모양을 보고 그 자리에서 분해까지 해주는 게 시퀀스 패턴(sequence pattern)이에요.
좌표를 설명하는 함수를 볼게요. 좌표는 [x, y] 형태의 리스트예요.
# src/modern/f3_sequence.py
def parse_point(point):
match point:
case [0, 0]:
return "원점"
case [x, 0]:
return f"x축 위, x={x}"
case [0, y]:
return f"y축 위, y={y}"
case [x, y]:
return f"좌표 ({x}, {y})"
case _:
return "좌표가 아니에요"
case [0, 0]:은 원소가 딱 두 개이고 둘 다 0인 리스트일 때만 맞아요. case [x, 0]:은 "두 개인데 둘째가 0, 첫째는 무엇이든 — 그걸 x로 붙잡아"라는 뜻이고요. 리스트의 모양을 따지면서 동시에 원소를 변수로 분해하는 거예요. B-1에서 배운 언패킹(a, b = [1, 2])이 case 안으로 들어왔다고 보면 돼요.
for p in [[0, 0], [5, 0], [0, 3], [2, 4], [1, 2, 3]]:
print(p, "→", parse_point(p))
# [0, 0] → 원점
# [5, 0] → x축 위, x=5
# [0, 3] → y축 위, y=3
# [2, 4] → 좌표 (2, 4)
# [1, 2, 3] → 좌표가 아니에요
[1, 2, 3]은 원소가 세 개라 어떤 두 칸 모양에도 안 맞아 case _로 떨어졌어요. 개수까지 정확히 따진다는 게 핵심이에요.
그럼 개수가 들쭉날쭉한 명령은요? B-1의 *(별표)가 여기서도 활약해요. 명령을 단어 리스트로 받아 처리하는 함수예요.
def run_command(parts):
match parts:
case []:
return "빈 명령이에요"
case [cmd]:
return f"인자 없는 명령: {cmd}"
case [cmd, arg]:
return f"명령 {cmd}, 인자 {arg}"
case [cmd, *args]:
return f"명령 {cmd}, 인자 {len(args)}개: {args}"
case [cmd, *args]:의 *args는 "첫 칸은 cmd로 잡고, 나머지 전부는 args라는 리스트로 묶어"라는 뜻이에요.
parts = ["tag", "a", "b", "c"]
case [cmd, *args] → 매칭!
cmd = "tag" (첫 칸 하나)
args = ["a", "b", "c"] (나머지 전부 리스트로)
for c in [[], ["help"], ["post", "photo.jpg"], ["tag", "a", "b", "c"]]:
print(c, "→", run_command(c))
# [] → 빈 명령이에요
# ['help'] → 인자 없는 명령: help
# ['post', 'photo.jpg'] → 명령 post, 인자 photo.jpg
# ['tag', 'a', 'b', 'c'] → 명령 tag, 인자 3개: ['a', 'b', 'c']
빈 리스트, 한 칸, 두 칸, 그리고 나머지를 묶는 모양까지 한 함수가 깔끔하게 갈라냈어요. if len(parts) == 0: ... elif len(parts) == 1: ...로 짰다면 훨씬 길고 읽기 힘들었을 거예요.
💡 한 줄 정리
시퀀스 패턴은 리스트·튜플의 개수와 구조를 따지면서 동시에 원소를 변수로 분해해요. case [x, y]:는 두 칸, case [cmd, *args]:는 "첫 칸 + 나머지 전부"예요. B-1 언패킹이 case 안으로 들어온 셈이에요.
🙋 학생 질문 — "튜터님, 시퀀스 패턴은 리스트만 되나요? 문자열도 글자 시퀀스인데 분해되나요?"
아주 날카로운 질문이에요. 결론부터 말하면 문자열은 시퀀스 패턴으로 분해되지 않아요. 리스트와 튜플은 case [a, b]로 분해되지만, "hi" 같은 문자열은 case [a, b]에 맞지 않아요.
왜냐하면 문자열을 한 글자씩 쪼개는 일은 거의 의도치 않은 실수일 때가 많거든요. "like"라는 명령을 받았는데 실수로 ['l', 'i', 'k', 'e']로 분해돼버리면 곤란하겠죠? 그래서 파이썬은 문자열(과 바이트열)은 시퀀스 패턴에서 하나의 통짜 값으로만 다뤄요. "리스트·튜플은 분해, 문자열은 통째로" 이렇게 기억하면 돼요.
Step 5: "딕셔너리 모양으로 가지치기 — 매핑 패턴"
리스트를 분해했으니 이번엔 딕셔너리예요. B-2에서 만든 {"type": "like", "user": "..."} 같은 데이터, 실무에선 이런 모양으로 이벤트가 쏟아져요. 누가 좋아요를 눌렀고, 누가 댓글을 달았고… 이걸 모양으로 갈라내는 게 매핑 패턴(mapping pattern)이에요.
# src/modern/f3_mapping.py
def handle_event(event):
match event:
case {"type": "like", "user": user}:
return f"{user}님이 좋아요를 눌렀어요."
case {"type": "comment", "user": user, "text": text}:
return f"{user}님의 댓글: {text}"
case {"type": "follow", "user": user}:
return f"{user}님이 팔로우했어요."
case {"type": event_type}:
return f"처리하지 않는 이벤트: {event_type}"
case _:
return "이벤트 형식이 아니에요."
case {"type": "like", "user": user}:을 읽어볼게요. "type 키의 값이 정확히 "like"이고, user 키가 있으면 그 값을 user로 붙잡아"라는 뜻이에요. 키는 정확한 값("like")으로 따지고, 값은 변수(user)로 분해하는 게 한 줄에 섞여 있어요.
events = [
{"type": "like", "user": "jaehoon"},
{"type": "comment", "user": "minji", "text": "멋져요!"},
{"type": "follow", "user": "seungwoo"},
{"type": "block", "user": "spammer"},
"not an event",
]
for e in events:
print(handle_event(e))
# jaehoon님이 좋아요를 눌렀어요.
# minji님의 댓글: 멋져요!
# seungwoo님이 팔로우했어요.
# 처리하지 않는 이벤트: block
# 이벤트 형식이 아니에요.
{"type": "block", ...}은 위의 세 가지에 안 맞고 case {"type": event_type}:에 걸려 "block"을 event_type으로 붙잡았어요. 마지막 "not an event"는 딕셔너리가 아니라 case _로 떨어졌고요.
매핑 패턴엔 꼭 알아둘 성질이 하나 있어요. 패턴에 적은 키만 맞으면, 딕셔너리에 다른 키가 더 있어도 통과해요. case {"type": "like", "user": user}:은 {"type": "like", "user": "kim", "extra": 1}처럼 extra 키가 더 붙어 있어도 매칭돼요. "최소한 이 키들은 있어야 한다"는 뜻이지, "딱 이 키들만"이 아니에요. 실무 데이터엔 항상 부가 정보가 덕지덕지 붙으니, 이 너그러움이 아주 편해요.
💡 한 줄 정리
매핑 패턴(case {"type": "like", "user": user}:)은 딕셔너리의 키-값 모양으로 가지를 쳐요. 키는 정확한 값으로 따지고 값은 변수로 분해해요. 패턴에 적은 키만 맞으면 다른 키가 더 있어도 통과해요.
🙋 학생 질문 — "튜터님, 딕셔너리 말고 우리가 만든 클래스(Member 같은 객체)도 모양으로 가지칠 수 있나요?"
네, 됩니다! 그걸 클래스 패턴(class pattern)이라고 해요. D 카테고리에서 만든 클래스나 @dataclass를 두고 case Member(name=n):처럼 적으면, "이게 Member 객체이고 name 속성을 n으로 붙잡아"라는 뜻이 돼요. 특히 @dataclass는 이 패턴과 아주 잘 어울려요.
다만 클래스 패턴은 객체의 속성 구조를 깊이 다루는 주제라, 오늘은 "이런 것도 있다" 정도로만 살짝 맛만 봐둘게요. 우리가 더 큰 프로젝트를 다루는 다음 과목들에서 본격적으로 써먹게 될 거예요. 오늘은 리터럴·시퀀스·매핑·가드, 이 네 가지를 익히는 데 집중해요.
Step 6: "검사하면서 담는다 — 바다코끼리 :="
match에서 잠깐 빠져나와, 이름이 귀여운 최신 문법 하나를 배워요. :=는 생긴 모양이 바다코끼리의 눈과 엄니를 닮았다고 해서 바다코끼리 연산자(walrus operator)라고 불러요. 하는 일은 한 문장이에요. 검사하면서, 그 값을 동시에 변수에 담아요.
긴 캡션만 골라내는 코드를 예전 방식으로 써볼게요.
# src/modern/f3_walrus.py
def long_captions_before(items):
result = []
for caption in items:
if len(caption) > 10:
result.append(f"긴 캡션({len(caption)}자): {caption}")
return result
len(caption)을 두 번 불렀어요. 한 번은 if에서 검사하려고, 또 한 번은 출력에 길이를 적으려고요. 같은 계산을 두 번 하는 게 살짝 아깝죠. walrus를 쓰면 검사하면서 그 값을 바로 담아둘 수 있어요.
def long_captions_after(items):
result = []
for caption in items:
if (length := len(caption)) > 10:
result.append(f"긴 캡션({length}자): {caption}")
return result
(length := len(caption)) > 10을 뜯어보면, len(caption)을 계산해서 그 값을 length에 담고(length := len(caption)), 동시에 그 값이 10보다 큰지 검사해요. 한 번 계산한 길이를 length로 재사용하니 len을 두 번 부르지 않아요.
captions = ["좋아요", "오늘 노을이 정말 예뻤어요", "맛집 다녀왔습니다 강추"]
print(long_captions_after(captions))
# ['긴 캡션(14자): 오늘 노을이 정말 예뻤어요', '긴 캡션(12자): 맛집 다녀왔습니다 강추']
walrus는 while과도 잘 어울려요. "값을 하나 꺼내서, 그게 끝 신호가 아니면 계속"이라는 흐름을 한 줄에 담거든요.
def read_until_blank(lines):
result = []
it = iter(lines)
while (line := next(it, "")) != "":
result.append(line.strip())
return result
(line := next(it, "")) != ""는 "다음 줄을 꺼내 line에 담고, 그게 빈 문자열이 아니면 계속"이에요. 빈 줄을 만나면 멈춰요.
print(read_until_blank([" a ", "b ", " c", "", "안 보임"]))
# ['a', 'b', 'c']
빈 문자열 뒤의 "안 보임"은 이미 멈춘 뒤라 처리되지 않았어요. 컴프리헨션(B-3) 안에서도 비싼 계산을 한 번만 하고 재사용할 때 요긴해요.
likes = [50, 1200, 80, 3000]
boosted = [score for n in likes if (score := n * 2) > 1000]
print(boosted)
# [2400, 6000]
n * 2를 계산해 score에 담고, 그게 1000을 넘으면 그 score를 결과에 담아요. 계산을 두 번 하지 않죠.
💡 한 줄 정리
walrus(:=)는 검사하면서 그 값을 변수에 동시에 담아요. if (n := len(x)) > 10:처럼 쓰면 같은 계산을 두 번 하지 않아요. if·while·컴프리헨션의 조건 자리에서 빛나요.
🙋 학생 질문 — "튜터님, := 랑 그냥 = 는 뭐가 다른가요? 헷갈려요."
핵심은 "값을 돌려주느냐"예요. 그냥 =(대입문)는 값을 담기만 하고 그 자체가 값이 되지는 않아요. 그래서 if x = len(s):처럼 조건 자리에 =를 쓰면 아예 문법 에러가 나요(사실 이건 실수를 막아주는 안전장치예요).
반면 :=(walrus)는 값을 담으면서 그 값을 그대로 돌려줘요. 그래서 if (x := len(s)) > 10:처럼 조건 한가운데에 끼워 넣을 수 있어요. "담기"만 필요하면 평범하게 x = len(s)를 쓰고, "담으면서 곧바로 검사·사용"까지 한 줄에 하고 싶을 때만 :=를 꺼내요. 남발하면 오히려 읽기 어려우니, 같은 계산을 두 번 하게 될 때 같은 분명한 자리에서만 쓰는 게 좋아요.
Step 7: "에러는 더 친절해졌다 — 향상된 에러 메시지"
A-1에서 "에러는 친구"라고 했던 것 기억나시죠? 파이썬은 버전이 오를 때마다 에러 메시지를 점점 더 친절하게 다듬어 왔어요. 우리가 쓰는 3.14는 그 친절함이 거의 정점이에요. 일부러 오타를 내서 직접 확인해볼게요.
# src/modern/f3_errors_demo.py
username = "jaehoon"
# 'username'을 'usernme'로 한 글자 빠뜨림
print(usernme)
username을 usernme로 한 글자 빠뜨렸어요. 이 파일을 실행하면 이런 메시지가 떠요.
Traceback (most recent call last):
File "src/modern/f3_errors_demo.py", line 10, in <module>
print(usernme)
^^^^^^^
NameError: name 'usernme' is not defined. Did you mean: 'username'?
두 가지를 눈여겨보세요. 첫째, 문제가 된 usernme 아래에 ^^^^^^^로 정확히 어디가 잘못됐는지 짚어줘요. 둘째, Did you mean: 'username'?이라며 우리가 의도했을 법한 이름을 추측해서 제안해줘요. 비전공자에게 정말 든든한 안내죠. 오타 한 글자를 찾으려고 코드를 노려보지 않아도 돼요.
3.14에선 이런 에러 메시지가 터미널에서 색깔로 표시되기도 해요(빨강·노랑 등). 어디가 문제인지 한눈에 들어오게요. 에러를 만났을 때 당황하지 말고, 맨 아랫줄(에러 종류와 설명)과 ^^^ 표시부터 천천히 읽는 습관을 들이면, 에러가 정말 친구처럼 느껴질 거예요.
🌟 이런 것도 있어요 — 파이썬 3.14의 더 새로운 문법
오늘 깊게 다루진 않지만, 3.14엔 갓 나온 문법이 더 있어요. 이름만 가볍게 만나두세요.
- t-string(
t"..."): f-string(f"...")의 동생뻘이에요. f-string은 곧장 완성된 문자열을 만들지만, t-string은 "글자 부분과 끼워 넣는 값 부분"을 따로 보관한 특별한 꾸러미를 돌려줘요. 값을 합치기 전에 한 번 손볼 수 있어서, 안전하게 문자열을 다뤄야 하는 자리에 쓰여요.- 괄호 없는
except: 여러 에러를 한 번에 잡을 때 예전엔except (A, B):처럼 괄호가 필요했는데, 3.14부터는except A, B:로 괄호 없이도 돼요.둘 다 "지금 당장 쓰자"는 게 아니라, "파이썬은 지금도 계속 자라고 있다"는 걸 느껴보라고 살짝 소개하는 거예요.
💡 한 줄 정리
파이썬 3.14는 ^^^로 문제 위치를 짚고 Did you mean으로 가까운 이름을 제안하는 친절한 에러 메시지를 줘요. 에러를 만나면 맨 아랫줄과 ^^^부터 읽으면 돼요. t-string·괄호 없는 except 같은 최신 문법도 계속 늘어나고 있어요.
🙋 학생 질문 — "튜터님, Did you mean 이 항상 정답을 맞히나요?"
항상은 아니에요. 파이썬은 우리가 쓴 틀린 이름과 철자가 비슷한 이름을 주변에서 찾아 제안해요. 그래서 usernme → username처럼 한두 글자 차이일 땐 거의 정확히 맞혀요. 하지만 완전히 다른 이름을 적었거나 비슷한 후보가 없으면 제안이 안 뜨기도 해요.
그러니 Did you mean은 "정답"이 아니라 "친절한 힌트"로 받아들이면 돼요. 맞으면 고맙게 따르고, 아니면 내가 의도한 이름이 정말 정의돼 있는지 직접 확인하면 됩니다.
Step 8: "명령 파서 완성 — 종합"
오늘 배운 패턴들을 한자리에 모아 진짜 명령 파서를 만들어요. 사용자가 한 줄로 친 명령("like 42", "follow minji" 같은)을 받아 처리하는 함수예요.
# src/modern/f3_command_parser.py
def parse(line):
parts = line.split()
match parts:
case []:
return "빈 줄이에요."
case ["like", post_id]:
return f"{post_id}번 게시물에 좋아요."
case ["follow" | "unfollow" as action, user]:
return f"{user}님을 {action} 했어요."
case ["post", *words] if words:
return f"게시물 작성: {' '.join(words)}"
case ["delete", post_id] if post_id.isdigit():
return f"{post_id}번 게시물 삭제."
case ["delete", _]:
return "삭제하려면 숫자 ID가 필요해요."
case [cmd, *_]:
return f"모르는 명령: {cmd}"
먼저 line.split()으로 한 줄을 단어 리스트로 쪼개요(A-2에서 배운 split이에요). 그 리스트를 match로 가지치는데, 오늘 배운 게 거의 다 들어 있어요.
case ["like", post_id]:— 시퀀스 패턴 + 캡처."like"다음 단어를post_id로 붙잡아요.case ["follow" | "unfollow" as action, user]:— OR 패턴에as가 새로 붙었어요. "follow또는unfollow인데, 맞은 그 값을action으로 붙잡아"라는 뜻이에요. 그래서action엔 실제로 들어온"follow"나"unfollow"가 담겨요.case ["post", *words] if words:— 나머지 단어(*words)를 모으고, 가드if words로 "단어가 한 개라도 있을 때만"을 걸었어요(빈 글은 거르려고요).case ["delete", post_id] if post_id.isdigit():— 가드에서post_id.isdigit()으로 "숫자로만 이뤄졌는지" 확인해요. 숫자가 아니면 바로 아래case ["delete", _]:로 떨어져 친절한 안내를 돌려줘요.
lines = [
"like 42",
"follow minji",
"unfollow spammer",
"post 오늘 날씨가 좋아요",
"delete 7",
"delete abc",
"dance now",
"",
]
for line in lines:
print(f"> {line}")
print(" ", parse(line))
실행하면 이렇게 나와요.
> like 42
42번 게시물에 좋아요.
> follow minji
minji님을 follow 했어요.
> unfollow spammer
spammer님을 unfollow 했어요.
> post 오늘 날씨가 좋아요
게시물 작성: 오늘 날씨가 좋아요
> delete 7
7번 게시물 삭제.
> delete abc
삭제하려면 숫자 ID가 필요해요.
> dance now
모르는 명령: dance
>
빈 줄이에요.
"delete 7"은 숫자라 삭제로, "delete abc"는 숫자가 아니라 안내로 갈렸어요. 같은 delete라도 인자의 모양에 따라 다르게 처리되죠. 이 모든 분기를 if/elif와 len()·인덱싱으로 짰다면 훨씬 길고 실수하기 쉬웠을 거예요. 패턴 매칭이 "값의 모양으로 가지치기"라는 한 문장을 이렇게 또렷하게 보여줘요.
💡 한 줄 정리
명령 한 줄을 split()으로 쪼개 match로 가지치면, 리터럴·OR·캡처·시퀀스·가드가 한 함수 안에서 어우러져 깔끔한 명령 파서가 돼요. 패턴 as 이름은 "맞은 그 값을 이름으로 붙잡기"예요.
🙋 학생 질문 — "튜터님, case ["follow" | "unfollow" as action, ...] 에서 as 는 정확히 뭘 잡는 거예요?"
as는 "방금 맞은 패턴의 값을, 이 이름에 담아줘"라는 뜻이에요. "follow" | "unfollow"는 둘 중 하나에 맞는지만 보지, 어느 쪽에 맞았는지는 따로 알려주지 않아요. 그래서 as action을 붙이면 실제로 들어온 값("follow" 또는 "unfollow")이 action에 담겨, 몸통에서 {action}으로 쓸 수 있어요.
Step 3의 캡처(case n:)가 "통째로 하나 잡기"였다면, as는 "OR처럼 조합된 패턴이 맞았을 때 그 값을 잡기"예요. 둘 다 "값을 붙잡아 쓴다"는 결은 같아요.
마무리
오늘은 if/elif로 길어지던 분기를 값의 모양으로 가지치는 match/case를 익혔어요. 정확한 값(리터럴), 리스트 구조(시퀀스), 딕셔너리 구조(매핑), 거기에 조건(가드)까지 — 명령 파서와 이벤트 분기를 또렷한 코드로 풀어냈죠. 검사하면서 담는 walrus(:=)와 3.14의 친절한 에러 메시지도 함께 익혔고요.
오늘 배운 핵심 세 가지
- 💡 하나 —
match/case는 한 값을 여러 모양과 맞춰 처음 맞는 가지 하나를 실행해요. 리터럴(case "like")·OR(case "a" | "b")·캡처(case n)·시퀀스(case [x, *rest])·매핑(case {"key": v})·가드(case n if 조건)를 골라 쓰면, 길어지던if/elif가 한눈에 읽혀요. 구체적인 모양은 위, 포괄적인 캡처는 아래라는 순서가 핵심이에요. - 💡 둘 — walrus(
:=)는 검사하면서 그 값을 동시에 변수에 담아요.if (n := len(x)) > 10:처럼 쓰면 같은 계산을 두 번 하지 않고,while·컴프리헨션의 조건 자리에서도 빛나요. 평범한=와 달리 값을 돌려주기에 조건 한가운데 끼울 수 있어요. - 💡 셋 — 파이썬 3.14의 향상된 에러 메시지는
^^^로 위치를,Did you mean으로 가까운 이름을 짚어줘요. t-string·괄호 없는except같은 최신 문법도 계속 자라고 있고요. 에러를 만나면 맨 아랫줄과^^^부터 읽으면 돼요.
다음 시간 예고
오늘로 패턴 매칭이라는 봉우리를 넘었어요. 다음 시간(F-4)엔 모던 파이썬의 마지막 봉우리, 비동기(async)를 만나요. 지난 시간에 만든 피드 제너레이터 기억나시죠? 게시물을 하나씩 흘려보냈잖아요. 그런데 게시물 열 개를 불러오는데 하나당 1초씩 걸린다면, 차례로 기다리면 10초가 걸려요. 비동기는 "기다리는 동안 다른 일을 하게" 만들어, 열 개를 거의 동시에 불러오는 마법이에요. async/await라는 새 키워드와 함께, 우리 피드가 어떻게 동시에 여러 게시물을 불러오는지 그 원리부터 차근차근 들여다볼게요.
과제
오늘 배운 match/case를 직접 손으로 굴려볼 차례예요. 막히면 해당 Step을 다시 펼쳐 보세요. 패턴은 한 번 익숙해지면 if/elif보다 훨씬 편해져요.
[기초] 메뉴 선택을 match로 분기하기
콘솔 메뉴에서 사용자가 누른 번호(문자열)를 받아 동작 이름을 돌려주는 menu_action을 만들어 보세요.
def menu_action(choice):
# 여기를 채워 보세요
...
"1"이면"새 게시물 작성","2"면"피드 보기"를 돌려주세요."3"또는"4"면"프로필 / 설정"을 돌려주세요(OR 패턴|을 써보세요).- 그 외 값이면
"잘못된 선택이에요"를 돌려주세요(case _).
힌트: Step 1·2의 리터럴 패턴과 OR 패턴(|)을 떠올리세요. case "3" | "4": 한 줄로 두 선택을 묶을 수 있어요.
[응용] 시퀀스 패턴으로 요청 라우팅하기
["GET", "/feed"]처럼 "메서드 + 경로"로 쪼개진 요청을 받아 처리하는 route를 만들어 보세요.
["GET", path]면f"{path} 조회"를 돌려주세요.["POST", path, *body]면f"{path}에 작성: {' '.join(body)}"를 돌려주세요(나머지 단어를body로 묶기).["DELETE", path]인데path가 숫자로만 이뤄졌으면f"{path}번 삭제"를, 숫자가 아니면"삭제하려면 숫자 ID가 필요해요"를 돌려주세요(가드if path.isdigit()).- 그 외엔
"처리할 수 없는 요청"을 돌려주세요.
힌트: Step 4의 시퀀스 패턴(*body)과 Step 3의 가드(if)를 합치는 문제예요. case ["DELETE", path] if path.isdigit():을 먼저, case ["DELETE", _]:을 그 아래에 두는 순서가 핵심이에요.
[심화] 매핑 패턴 + walrus로 이벤트 요약하기
이벤트 딕셔너리 목록을 받아 한 줄 요약 목록을 돌려주는 summarize를 만들어 보세요.
{"type": "like", "post": post}면f"좋아요: {post}".{"type": "comment", "text": text}인데 글자 수가 20을 넘으면f"긴 댓글({n}자)"(walrus로 길이를n에 담아 쓰기), 20 이하면"댓글".- 그 외엔
"기타 이벤트".
힌트: Step 5의 매핑 패턴과 Step 6의 walrus를 가드에서 합쳐요. case {"type": "comment", "text": text} if (n := len(text)) > 20:처럼요. 긴 댓글 case를 짧은 댓글 case보다 위에 둬야 길이 검사가 먼저 걸려요.
생각해볼 주제
1. match/case와 if/elif, 언제 무엇을 고를까
오늘 배운 match/case는 강력하지만, if/elif를 완전히 대체하는 건 아니에요. "좋아요가 100 이상이고 댓글도 있으면"처럼 서로 다른 변수 여러 개를 조합해 검사할 땐 if가 더 자연스럽고, "이 한 값이 어떤 모양인가"를 따질 땐 match가 빛나죠. 두 도구의 경계가 어디인지, 어떤 코드를 보면 "아, 이건 match로 바꾸면 훨씬 읽기 쉽겠다" 싶은지 생각해 보세요.
2. 패턴 매칭은 왜 단순한 switch보다 강할까
다른 언어의 switch는 보통 "값이 같은지"만 봐요. 그런데 파이썬의 match는 case [cmd, *args]나 case {"type": "like", "user": user}처럼 구조를 분해하면서 동시에 값을 변수로 붙잡았죠. 만약 패턴 매칭 없이 이걸 if로 짠다면, 리스트 길이를 재고 인덱스로 꺼내고 키가 있는지 확인하는 코드가 줄줄이 필요했을 거예요. "값 비교"를 넘어 "구조 분해"까지 한다는 게 어떤 차이를 만드는지, 오늘 만든 명령 파서를 if로 다시 짜본다고 상상하며 따져 보세요.
3. walrus(:=)는 어디까지 써야 적당할까
walrus는 같은 계산을 두 번 하지 않게 해주는 편리한 도구지만, 한 줄에 너무 많은 걸 욱여넣으면 오히려 읽기 어려워져요. if (n := len(x)) > 10:은 분명하지만, 조건이 복잡해질수록 "여기서 무엇이 담기고 무엇이 검사되는지" 헷갈릴 수 있죠. "코드는 짧을수록 좋다"와 "코드는 읽기 쉬울수록 좋다"가 맞부딪치는 대목이에요. walrus를 쓰면 정말 좋은 상황과, 차라리 평범하게 두 줄로 풀어 쓰는 게 나은 상황을 나눠 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 먼저 스스로 손으로 짠 뒤에 펼쳐 보세요. 코드를 직접 굴려 출력을 확인해보면 훨씬 오래 남아요.
🎯 [과제 1 예시답안] 메뉴 선택을 match로 분기하기
채점 포인트
| 항목 | 배점 | 확인 |
|---|---|---|
match choice:로 한 값을 여러 모양과 맞추는가 |
★★★ | |
"3" | "4"처럼 OR 패턴으로 두 선택을 묶는가 |
★★★ | |
case _로 나머지(잘못된 선택)를 받는가 |
★★☆ |
풀이 예시
def menu_action(choice):
match choice:
case "1":
return "새 게시물 작성"
case "2":
return "피드 보기"
case "3" | "4":
return "프로필 / 설정"
case _:
return "잘못된 선택이에요"
for choice in ["1", "2", "3", "4", "9"]:
print(choice, "→", menu_action(choice))
# 1 → 새 게시물 작성
# 2 → 피드 보기
# 3 → 프로필 / 설정
# 4 → 프로필 / 설정
# 9 → 잘못된 선택이에요
"3"과 "4"를 case "3" | "4": 한 줄로 묶어 같은 결과를 돌려준 게 핵심이에요. if/elif로 짰다면 if choice == "1": ... elif choice == "3" or choice == "4": ...처럼 choice ==를 반복해야 했을 텐데, match는 한 변수를 죽 나열한 모양과 맞춰보니 한결 깔끔해요. "9"는 어떤 case에도 안 맞아 case _로 떨어졌어요.
✅ case _를 맨 아래에 두었는가 — 위에서 안 맞은 값을 받는 안전망이에요.
❌ case _를 빠뜨리면 — "9" 같은 값에서 아무것도 return하지 않아 None이 돌아와요.
💡 튜터의 한마디: 콘솔 메뉴, 게임의 키 입력, 상태 코드 분류처럼 "정해진 값 몇 개로 갈래를 나누는" 자리에 리터럴 패턴이 딱이에요. OR 패턴(|)으로 비슷한 선택을 묶으면 가지 수도 줄고 의도도 또렷해져요.
🎯 [과제 2 예시답안] 시퀀스 패턴으로 요청 라우팅하기
채점 포인트
| 항목 | 배점 | 확인 |
|---|---|---|
case ["GET", path]:처럼 시퀀스를 분해하는가 |
★★★ | |
*body로 나머지 단어를 묶는가 |
★★★ | |
가드 if path.isdigit()로 숫자 여부를 거르는가 |
★★★ | |
숫자 가드 case를 일반 case보다 위에 두었는가 |
★★☆ |
풀이 예시
def route(parts):
match parts:
case ["GET", path]:
return f"{path} 조회"
case ["POST", path, *body]:
return f"{path}에 작성: {' '.join(body)}"
case ["DELETE", path] if path.isdigit():
return f"{path}번 삭제"
case ["DELETE", _]:
return "삭제하려면 숫자 ID가 필요해요"
case _:
return "처리할 수 없는 요청"
requests = [
["GET", "/feed"],
["POST", "/posts", "오늘", "점심"],
["DELETE", "42"],
["DELETE", "abc"],
["PATCH", "/posts", "1"],
]
for req in requests:
print(req, "→", route(req))
# ['GET', '/feed'] → /feed 조회
# ['POST', '/posts', '오늘', '점심'] → /posts에 작성: 오늘 점심
# ['DELETE', '42'] → 42번 삭제
# ['DELETE', 'abc'] → 삭제하려면 숫자 ID가 필요해요
# ['PATCH', '/posts', '1'] → 처리할 수 없는 요청
["POST", path, *body]의 *body가 "POST와 경로 다음 단어 전부"를 리스트로 묶어줘요. 그래서 ["POST", "/posts", "오늘", "점심"]에서 body는 ["오늘", "점심"]이 되고, ' '.join(body)로 "오늘 점심"이 돼요.
가장 중요한 건 DELETE 두 가지 case의 순서예요. 숫자를 거르는 가드 case ["DELETE", path] if path.isdigit():을 위에 두어, "42"는 여기서 걸리고 "abc"는 통과하지 못해 바로 아래 case ["DELETE", _]:로 떨어져요. 순서를 뒤집으면 "42"도 일반 case에 먼저 걸려 안내 메시지가 나와버려요.
✅ 가드가 붙은 좁은 case를 위에, 가드 없는 넓은 case를 아래에 두었는가.
❌ *body를 body로만 쓰면 — ["POST", path, body]는 단어가 정확히 세 개일 때만 맞아, 본문이 여러 단어면 매칭에 실패해요.
💡 튜터의 한마디: 시퀀스 패턴은 "명령 + 인자들"처럼 길이가 들쭉날쭉한 입력을 다룰 때 진가를 발휘해요. *로 나머지를 묶고 가드로 조건을 거는 조합은, 실무에서 명령줄 도구나 간단한 파서를 짤 때 그대로 써먹는 패턴이에요.
🎯 [과제 3 예시답안] 매핑 패턴 + walrus로 이벤트 요약하기
채점 포인트
| 항목 | 배점 | 확인 |
|---|---|---|
case {"type": "like", "post": post}:로 딕셔너리를 분해하는가 |
★★★ | |
가드에서 walrus (n := len(text)) > 20로 길이를 담아 쓰는가 |
★★★ | |
긴 댓글 case를 짧은 댓글 case보다 위에 두었는가 |
★★★ | |
그 외 이벤트를 case _로 받는가 |
★★☆ |
풀이 예시
def summarize(events):
result = []
for event in events:
match event:
case {"type": "like", "post": post}:
result.append(f"좋아요: {post}")
case {"type": "comment", "text": text} if (n := len(text)) > 20:
result.append(f"긴 댓글({n}자)")
case {"type": "comment"}:
result.append("댓글")
case _:
result.append("기타 이벤트")
return result
events = [
{"type": "like", "post": "photo_1"},
{"type": "comment", "text": "짧은 댓글"},
{"type": "comment", "text": "이건 스무 글자를 훌쩍 넘기는 제법 긴 댓글이에요"},
{"type": "share", "post": "photo_2"},
]
for line in summarize(events):
print(line)
# 좋아요: photo_1
# 댓글
# 긴 댓글(27자)
# 기타 이벤트
매핑 패턴과 walrus가 한 case에서 만났어요. case {"type": "comment", "text": text} if (n := len(text)) > 20:을 뜯어보면, 먼저 딕셔너리가 "comment 모양"인지 보고 text를 붙잡은 뒤, 가드에서 len(text)를 계산해 n에 담으면서 동시에 20보다 큰지 검사해요. n에 담아뒀으니 f"긴 댓글({n}자)"에서 다시 len을 부르지 않아도 되죠.
순서도 중요해요. 긴 댓글 case(가드 있음)를 짧은 댓글 case(가드 없음)보다 위에 둬야, 27자짜리 댓글이 길이 검사에 먼저 걸려요. 순서를 뒤집으면 모든 댓글이 가드 없는 case {"type": "comment"}:에 먼저 걸려 "댓글"로만 나와요.
✅ 긴 댓글 case를 위에 두었는가 — 가드 있는 쪽이 먼저 검사돼야 해요.
❌ case {"type": "comment"}만 쓰고 길이 가드를 빼면 — 모든 댓글이 똑같이 "댓글"로만 분류돼요.
💡 튜터의 한마디: 실무의 이벤트·메시지는 거의 다 딕셔너리(또는 JSON) 모양으로 흘러요. 매핑 패턴으로 종류를 가르고, 가드에 walrus를 곁들여 "검사하면서 값까지 담는" 흐름은, 들어오는 데이터를 분류하는 코드를 짧고 또렷하게 만들어줘요.
🤔 [생각해볼 주제 1] match/case와 if/elif, 언제 무엇을 고를까
문제 상황 요약
match/case는 강력하지만 if/elif를 완전히 대체하진 않아요. 어떤 코드엔 match가, 어떤 코드엔 if가 더 자연스러운지 그 경계를 가늠해보는 문제예요.
튜터의 가이드 및 해설
판단의 기준은 "무엇을 검사하느냐"예요. 한 값을 두고 "이게 어떤 값·어떤 구조인가"를 따질 땐 match가 빛나요. 상태 문자열, 명령 리스트, 이벤트 딕셔너리처럼 한 대상의 모양을 여러 갈래로 가를 때죠. match는 값이 같은지뿐 아니라 리스트·딕셔너리의 구조까지 보고 분해해주니, if로는 길어질 코드를 한눈에 정리해요.
반대로 서로 다른 변수 여러 개를 조합해 검사할 땐 if가 자연스러워요. "좋아요가 100 이상이고(likes >= 100) 댓글도 있으면(comments)"처럼 두 변수의 조건을 and로 엮는 건 match의 모양 맞추기로 표현하기 어색해요. 이런 자리엔 평범한 if가 더 읽기 쉬워요.
또 가지가 두세 개뿐이라면 굳이 match를 꺼내지 않아도 돼요. if status == "active": ... else: ... 정도는 if가 더 간결하죠. match는 가지가 많아지고, 그 가지들이 "한 값의 여러 모양"일 때 비로소 값을 해요.
실무 감각으론, "이 한 값이 무엇/어떤 구조인가"로 갈리면 match, "여러 조건을 조합"하면 if예요. 그리고 elif가 다섯 개를 넘어가며 같은 변수를 반복 비교하고 있다면, match로 바꿔볼 좋은 신호예요.
💡 핵심을 한마디로
"한 값의 모양·구조로 가지를 치면
match, 서로 다른 변수의 조건을 조합하면if예요.elif가 길어지며 같은 변수를 반복 비교하고 있다면, 그게match로 바꿀 때라는 신호예요."
🤔 [생각해볼 주제 2] 패턴 매칭은 왜 단순한 switch보다 강할까
문제 상황 요약
다른 언어의 switch는 보통 "값이 같은지"만 봐요. 그런데 파이썬의 match는 구조를 분해하면서 값을 변수로 붙잡았죠. 이 차이가 코드에 무엇을 만드는지 따져보는 문제예요.
튜터의 가이드 및 해설
switch는 "이 값이 1이면, 2면, 3이면"처럼 값 하나하나와의 일치만 봐요. 우리 match도 리터럴 패턴(case "like":)으로 그 일을 하죠. 하지만 match는 거기서 멈추지 않아요.
핵심은 구조 분해(destructuring)예요. case [cmd, *args]:는 "리스트인데 첫 칸은 cmd, 나머지는 args"라고 모양을 따지면서 동시에 값을 꺼내 변수에 담아요. case {"type": "like", "user": user}:도 "딕셔너리인데 type이 like고 user 키가 있으면, 그 값을 user로"라고 구조와 추출을 한 줄에 해내죠. 단순한 switch로는 이게 안 돼요. 값이 같은지만 보니까요.
이걸 패턴 매칭 없이 if로 짠다고 상상해보세요. 명령 파서라면 if len(parts) >= 1 and parts[0] == "like": post_id = parts[1] ...처럼 길이를 재고, 인덱스로 꺼내고, 키가 있는지 확인하는 코드가 줄줄이 필요해요. 인덱스를 잘못 짚으면 에러도 나고요. match는 "이 모양이면 이렇게 분해해서 받아"를 선언적으로 적어, 그 번거로운 과정을 파이썬에게 맡겨요.
그래서 패턴 매칭은 "값 비교 + 구조 검사 + 값 추출"을 한 문법에 담은 도구예요. 데이터의 모양이 다양할수록(중첩 리스트, 키가 제각각인 딕셔너리) 그 위력이 커져요. 단순 분기를 넘어 "데이터를 모양대로 해체하는" 일이라, switch보다 한 차원 강한 거예요.
💡 핵심을 한마디로
"
switch는 값이 같은지만 보지만,match는 리스트·딕셔너리의 구조를 따지면서 동시에 값을 변수로 꺼내요. '값 비교 + 구조 분해 + 추출'을 한 줄에 담으니, 인덱스로 일일이 파헤치던 코드가 선언적으로 또렷해져요."
🤔 [생각해볼 주제 3] walrus(:=)는 어디까지 써야 적당할까
문제 상황 요약
walrus(:=)는 같은 계산을 두 번 하지 않게 해주지만, 한 줄에 너무 많은 걸 담으면 오히려 읽기 어려워져요. "짧은 코드"와 "읽기 쉬운 코드"가 부딪치는 자리에서 균형점을 찾아보는 문제예요.
튜터의 가이드 및 해설
walrus가 정말 좋은 자리는 분명해요. 같은 계산을 검사와 사용 양쪽에서 써야 할 때예요. if (n := len(caption)) > 10:처럼 길이를 검사에도 쓰고 출력에도 쓴다면, walrus로 한 번만 계산해 재사용하는 게 깔끔하고 효율적이에요. while (line := next(it, "")) != "":처럼 "값을 꺼내 검사하고 계속"이라는 흐름도 walrus가 자연스럽게 한 줄에 담아요.
반대로 walrus를 피하는 게 나은 자리도 있어요. 조건이 이미 복잡한데 거기에 :=까지 끼우면, 한 줄에 "담기 + 검사 + 다른 조건"이 뒤엉켜 무엇이 무엇인지 헷갈려요. 이럴 땐 차라리 위에 n = len(caption) 한 줄을 따로 두고, 다음 줄에서 if n > 10:으로 검사하는 게 읽기 쉬워요. 한 줄이 늘어나도, 읽는 사람의 머릿속은 더 편해지죠.
기준을 세워본다면, walrus는 "한 번 계산한 값을 바로 그 줄에서 한 번 더 쓸 때"만 꺼내는 게 좋아요. "그냥 변수에 담기만" 하면 평범한 =가 더 솔직하고, "담은 값을 여러 줄에 걸쳐 복잡하게" 쓸 거면 미리 변수로 빼는 게 나아요. 도구가 화려하다고 늘 쓰는 게 아니라, 코드를 읽을 다음 사람(미래의 나 포함)을 떠올리며 고르는 거예요.
💡 핵심을 한마디로
"walrus는 한 번 계산한 값을 같은 줄에서 곧바로 검사·사용할 때만 꺼내요. 그냥 담기만 하면 평범한
=가 더 솔직하고, 조건이 복잡하면 미리 변수로 빼는 게 읽기 쉬워요. '짧게'보다 '읽기 쉽게'가 늘 우선이에요."