모던 파이썬 기초 #3 제어 흐름 — if, while, for, match-case

6 분 소요

#2 변수/기본 타입과 타입 힌트에서 만든 변수들을 가지고, 이번에는 흐름을 만들어 봅니다. if, while, for, 그리고 3.10에서 들어온 match-case까지 다룹니다.

파이썬 흐름 제어의 큰 특징은 두 가지입니다.

  1. 블록은 들여쓰기로 구분합니다. 중괄호 {}가 없습니다.
  2. 콜론(:)으로 헤더를 끝냅니다. if x > 0:처럼.

그래서 들여쓰기가 곧 문법입니다. 일반적으로 공백 4칸을 씁니다 (PEP 8). 탭과 스페이스를 섞어 쓰면 에러가 납니다.

if / elif / else #

다른 언어와 가장 비슷합니다. 괄호가 없는 게 차이.

if 기본
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(grade)   # B

else if가 아니라 elif 입니다. 왜 합쳤는지는 역사적 이유고, 그냥 외우면 됩니다.

한 줄짜리 — 삼항 연산자 #

C 계열의 cond ? a : b와 다릅니다. 파이썬은 a if cond else b 형태입니다.

삼항
status = "성인" if age >= 19 else "미성년"

자연어 가까운 순서라 처음에는 어색하지만, 익숙해지면 읽기 좋습니다.

진리값과 조건 — falsy 활용 #

#2에서 본 falsy 값들을 그대로 활용합니다.

falsy 조건
items: list[int] = []

if not items:
    print("비어 있음")    # 이게 출력됨

# 아래도 같은 의미지만, 위가 더 파이써닉
if len(items) == 0:
    print("비어 있음")

리스트 비었음 검사로 len(x) == 0을 쓰는 건 보통 not x로 줄입니다.

is== #

이건 주의가 필요한 부분입니다.

is와 ==의 차이
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True   (값이 같음)
print(a is b)   # False  (다른 객체)

x = None
print(x is None)   # True  ← None 비교는 항상 is

==은 값 비교, is는 객체 동일성 비교 입니다. None, True, False는 항상 is로 비교하세요. 다른 값은 ==입니다.

while #

조건이 참인 동안 반복.

while 기본
count = 0
while count < 5:
    print(count)
    count += 1

++는 없습니다. count += 1입니다.

break, continue #

break / continue
n = 0
while True:
    n += 1
    if n % 2 == 0:
        continue       # 짝수는 건너뜀
    if n > 7:
        break          # 7 넘으면 끝
    print(n)
# 1, 3, 5, 7

while True + break는 “조건이 복잡해서 위쪽에 적기 어려운” 무한 루프 패턴입니다. 흔히 씁니다.

else 절 — 잘 안 알려진 기능 #

while, for에는 else가 붙을 수 있습니다. “루프가 break 없이 끝났을 때” 실행됩니다.

while-else
n = 0
while n < 10:
    if n == 99:
        break
    n += 1
else:
    print("break 없이 끝났음")    # 이게 출력

흔하지는 않지만 “찾는 값이 없어서 다 돌았을 때"를 처리하기에 깔끔한 경우가 있습니다. 너무 많이 쓰면 가독성이 오히려 떨어지니 적당히.

for — 모두 시퀀스 순회 #

다른 언어의 for (i = 0; i < n; i++) 같은 C 스타일은 없습니다. 파이썬의 for는 항상 이터러블을 순회 합니다.

for 기본
names = ["a", "b", "c"]
for name in names:
    print(name)

문자열도 이터러블입니다.

문자 하나씩
for ch in "hello":
    print(ch)
# h, e, l, l, o

range — 숫자 시퀀스가 필요할 때 #

range
for i in range(5):       # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 7):    # 2, 3, 4, 5, 6
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8 (step 2)
    print(i)

range(start, stop, step)입니다. stop포함되지 않습니다 (반-개방 구간). 이게 처음에 헷갈리지만, 다른 언어 슬라이스와 같은 관례라 곧 익숙해집니다.

enumerate — 인덱스가 필요할 때 #

인덱스가 필요할 때
items = ["사과", "배", "감"]
for i, item in enumerate(items):
    print(f"{i}: {item}")
# 0: 사과
# 1: 배
# 2: 감

for i in range(len(items))items[i]로 꺼내는 건 안티패턴입니다. enumerate 한 단어로 끝납니다.

zip — 여러 리스트 동시 순회 #

zip
names = ["커티스", "스미스", "존"]
ages = [30, 25, 40]

for name, age in zip(names, ages):
    print(f"{name}: {age}")

길이가 다르면 짧은 쪽에 맞춰 끊깁니다. 길이 안 맞을 때 에러를 내고 싶으면 zip(..., strict=True) (3.10+).

strict 모드
for n, a in zip(names, ages, strict=True):
    ...

패턴 매칭 — match-case (3.10+) #

switch와 모양은 비슷한데, 할 수 있는 일이 훨씬 많습니다. 파이썬은 단순 분기가 아니라 패턴 매칭으로 설계됐습니다.

가장 단순한 형태 #

단순 분기
def http_status(code: int) -> str:
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:
            return "Unknown"

case _:가 와일드카드(default)입니다. JavaScript의 default:에 해당합니다.

여러 값 한 케이스로 — | #

OR 패턴
def category(code: int) -> str:
    match code:
        case 200 | 201 | 204:
            return "성공"
        case 400 | 401 | 403 | 404:
            return "클라이언트 오류"
        case 500 | 502 | 503:
            return "서버 오류"
        case _:
            return "기타"

구조 분해 — 여기서부터 진가 #

리스트, 튜플, 딕셔너리의 모양 자체를 패턴으로 쓸 수 있습니다.

구조 분해
def describe(point: tuple[int, ...]) -> str:
    match point:
        case ():
            return "빈 점"
        case (x,):
            return f"1D: {x}"
        case (x, y):
            return f"2D: ({x}, {y})"
        case (x, y, z):
            return f"3D: ({x}, {y}, {z})"
        case _:
            return "고차원"

print(describe((1, 2)))      # 2D: (1, 2)
print(describe((1, 2, 3)))   # 3D: (1, 2, 3)

각 패턴 안의 x, y해당 위치의 값을 변수에 바인딩 합니다. 매칭과 동시에 변수가 만들어지는 구조로, JavaScript 구조 분해와 switch를 한곳에 합친 것과 비슷합니다.

딕셔너리 패턴 #

dict 패턴
event = {"type": "click", "x": 100, "y": 200}

match event:
    case {"type": "click", "x": x, "y": y}:
        print(f"클릭: ({x}, {y})")
    case {"type": "key", "code": code}:
        print(f"키: {code}")
    case _:
        print("알 수 없는 이벤트")

특정 키가 존재할 때 그 값을 변수로 가져오는 패턴으로, API 응답 처리에 정말 잘 어울립니다.

가드 — if 조건 추가 #

가드
match (x, y):
    case (a, b) if a == b:
        print(f"동일: {a}")
    case (a, b) if a > b:
        print(f"a가 큼")
    case _:
        print("그 외")

클래스 패턴 #

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

class Square:
    def __init__(self, side: float):
        self.side = side

shape = Circle(5)

match shape:
    case Circle(radius=r):
        print(f"원, 반지름 {r}")
    case Square(side=s):
        print(f"정사각형, 한 변 {s}")

타입 검사 + 속성 추출이 한 줄에 들어갑니다.

switch와 결이 다른 이유 #

match-case는 단순한 다중 분기가 아니라 자료의 모양으로 분기하는 도구 입니다. 위 코드를 if-elif로 다시 쓰면 길고 어색해집니다.

match 없이 같은 일
if isinstance(shape, Circle):
    r = shape.radius
    print(f"원, 반지름 {r}")
elif isinstance(shape, Square):
    s = shape.side
    print(f"정사각형, 한 변 {s}")

같은 동작인데 타입 체크와 속성 꺼내기가 따로따로입니다. match는 이 두 단계를 합쳐 한 줄로 표현합니다. JSON/이벤트 객체 처리에서 효용이 큽니다.

흐름의 진실 — pass #

블록이 비어 있을 수가 없습니다. 들여쓰기 블록이 없으면 문법 오류입니다. 일단 비워두고 싶을 때 **pass**를 씁니다.

비워두기
def todo(): 
    pass    # 나중에 구현

if x > 0:
    pass    # 일단 무시
else:
    handle(x)

... (Ellipsis 객체)도 같은 용도로 종종 보이지만, **pass**가 더 관습적입니다.

for도 컴프리헨션이라는 패턴이 있다 #

리스트, 딕셔너리, 세트를 만들 때 for를 한 줄에 압축하는 문법이 있습니다.

간단한 미리보기
squares = [x ** 2 for x in range(5)]
# [0, 1, 4, 9, 16]

이건 다음 글(#4 컬렉션과 컴프리헨션)에서 본격적으로 다룹니다.

정리 #

이번 글에서 본 것:

  • 들여쓰기로 블록 — 4칸 공백, 콜론으로 헤더 종료
  • if / elif / else, 삼항 a if cond else b
  • is는 객체 동일성, ==는 값 비교, None 검사는 is None
  • whilebreak / continue / else
  • for in으로 이터러블 순회 — range, enumerate, zip
  • match-case — 단순 분기, OR 패턴, 구조 분해, dict 패턴, 가드, 클래스 매칭
  • 빈 블록에는 pass

다음 글(#4 컬렉션과 컴프리헨션)에서는 가장 많이 쓰이는 자료 구조 4개 — list, tuple, dict, set과, 그것들을 한 줄로 만드는 컴프리헨션을 다룹니다.

X