Skip to content
This page has been auto-translated and may contain errors.View in English

제어 흐름

지금까지 작성한 모든 프로그램은 항상 같은 방식으로 실행됩니다. 위에서 아래로, 한 줄씩 차례대로 말이죠. 간단한 스크립트에는 그것으로 충분하지만, 실제 프로그램은 결정을 내리고 작업을 반복해야 합니다. 퀴즈는 답이 맞는지 확인해야 하고, 게임은 플레이어가 이기거나 질 때까지 계속 실행되어야 합니다. 이 장에서는 프로그램을 분기시키고 반복시키는 방법을 다룹니다.

제어 흐름은 프로그램의 실행 경로를 형성합니다. 조건문(if/elif/else)은 분기 사이에서 선택하고, 반복문(while, for)은 블록을 반복합니다. 파이썬의 for 루프는 인덱스 기반 카운터가 아니라 이터레이터 프로토콜입니다. 문법과 내부 모델을 모두 이해하면 코드가 더 깔끔해집니다.

파이썬의 제어 흐름 기본 요소는 if/elif/else, while, for입니다. for는 이터러블에 대해 __iter__를 호출하고 StopIteration이 발생할 때까지 next()를 호출합니다. while__bool__ 또는 __len__을 통해 조건 객체를 평가합니다. breakcontinue는 가장 가까운 둘러싼 루프에 영향을 미치고, loop-elsebreak 없이 루프가 완료될 때만 실행됩니다.

비교

결정을 내리기 전에 무언가를 비교해야 합니다. 비교 연산자는 True 또는 False를 반환합니다. 초기에 가장 중요하게 익혀야 할 것: =는 값을 할당하고, ==는 두 값이 같은지 확인합니다. 이 둘을 혼동하는 것은 초보자가 가장 흔히 저지르는 실수 중 하나입니다.

비교 연산자는 해당하는 던더 메서드(__eq__, __lt__ 등)를 호출하고 bool을 반환합니다. 파이썬은 연쇄 비교를 지원합니다: 0 < x < 10은 대부분의 언어처럼 왼쪽에서 오른쪽으로 평가되는 것이 아니라 0 < x and x < 10으로 평가됩니다. 문자열 비교는 유니코드 코드 포인트를 기준으로 한 사전식 비교입니다.

비교 연산자는 풍부한 비교 메서드를 호출합니다: __eq__, __ne__, __lt__, __le__, __gt__, __ge__. 파이썬은 단락 평가되는 연쇄 비교를 지원합니다: a < b < ca < b and b < c가 되지만, b는 한 번만 평가됩니다. is는 객체 정체성(id() 동등성)을 확인하지 값의 동등성을 확인하지 않습니다. 값 비교에는 ==를 사용하고, isNone, True, False에만 사용하세요.

python
5 > 3     # True
5 < 3     # False
5 == 5    # True   (참고: 두 개의 등호; =는 할당, ==는 비교)
5 != 3    # True   ("같지 않음")
5 >= 5    # True   ("크거나 같음")
5 <= 4    # False  ("작거나 같음")

===의 구분은 초기에 거의 모든 사람을 헷갈리게 합니다. 할당(=)은 값을 저장하고, 비교(==)는 두 값이 같은지 확인합니다.

문자열도 비교할 수 있습니다. 파이썬은 알파벳순으로 비교합니다:

python
"apple" == "apple"   # True
"apple" < "banana"   # True  (a가 b보다 먼저)
"apple" == "Apple"   # False (대소문자 구분)

조건 결합하기

and, or, not은 비교를 결합합니다. and는 양쪽이 모두 참이어야 합니다. or는 적어도 한쪽이 참이어야 합니다. not은 결과를 뒤집습니다. 이를 통해 "점수가 합격선 이상 AND 사용자가 활성 상태"와 같은 실제 조건을 표현할 수 있습니다.

andor는 단락 평가됩니다: and는 첫 번째 거짓 피연산자에서 멈추고, or는 첫 번째 참 피연산자에서 멈춥니다. 이들은 단순히 TrueFalse가 아니라 실제 피연산자 값을 반환합니다. not은 피연산자에 대해 __bool__을 호출하고 반전된 결과를 반환합니다.

and는 왼쪽에서 오른쪽으로 평가하며 첫 번째 거짓 피연산자를, 모두 참이면 마지막 피연산자를 반환합니다. or는 첫 번째 참 피연산자를, 모두 거짓이면 마지막 피연산자를 반환합니다. 이 단락 평가 동작은 보장됩니다: 왼쪽이 결과를 결정하면 오른쪽은 평가되지 않습니다. not xTrue if not bool(x) else False와 동등하지만, 파이썬이 이를 최적화합니다.

python
age   = 25
score = 88

age >= 18 and score >= 80    # True  (둘 다 참이어야 함)
age < 18 or score >= 80      # True  (적어도 하나는 참이어야 함)
not age >= 18                # False (결과를 뒤집음)

and는 양쪽 모두 필요합니다. or는 적어도 한쪽이 필요합니다. not은 반전시킵니다.

참스러움(Truthy)과 거짓스러움(Falsy)

파이썬의 모든 값은 TrueFalse가 아니어도 부울 해석을 가집니다. 빈 문자열, 0, 빈 리스트, None은 모두 조건에서 False처럼 동작합니다. 그 외 모든 것은 True처럼 동작합니다. 즉, if results:if len(results) > 0:을 쓰지 않고도 리스트가 비어있지 않은지 확인합니다.

파이썬의 참스러움 규칙: 거짓 값은 False, 0, 0.0, "", [], (), {}, set(), None입니다. 그 외 모든 것은 참입니다. 조건문은 객체의 __bool__을 호출하며, __bool__이 정의되지 않은 경우 __len__으로 폴백됩니다. 길이가 0인 __len__을 가진 객체는 거짓입니다.

진리 테스트는 __bool__을 호출합니다. __bool__이 정의되지 않은 경우, 파이썬은 __len__으로 폴백합니다: __len__ == 0인 객체는 거짓입니다. 커스텀 클래스는 둘 중 하나의 메서드를 구현해 참스러움을 제어합니다. 표준 거짓 값은 다음과 같습니다: False, 0, 0.0, 0j, "", b"", [], (), {}, set(), frozenset(), None, 그리고 __bool__False를 반환하거나 __len__0을 반환하는 객체.

python
# 이들은 모두 조건에서 False처럼 동작합니다:
False, 0, 0.0, "", [], {}, (), None

# 그 외 모든 것은 True처럼 동작합니다

이는 if results:가 "리스트가 비어있지 않다면"이라고 말하는 자연스러운 방법이며, if name:은 문자열에 어떤 내용이 있는지 확인합니다.

if / elif / else

if 문은 조건이 True일 때만 코드 블록을 실행합니다. elif는 첫 번째가 거짓인 경우 확인할 추가 조건을 추가합니다. else는 어떤 조건과도 일치하지 않은 모든 경우를 잡아냅니다. 파이썬은 중괄호가 아닌 들여쓰기로 각 블록에 속한 내용을 정의합니다.

if/elif/else는 조건을 위에서 아래로 평가하고 일치하는 첫 번째 블록을 실행합니다. 파이썬은 블록 범위를 정의하기 위해 들여쓰기(관례상 4개의 공백)를 사용합니다. 일관성 없는 들여쓰기는 SyntaxError입니다. 오직 하나의 분기만 실행됩니다: 조건이 일치하면 모든 후속 elifelse는 건너뜁니다.

파이썬은 들여쓰기를 블록 구분자로 사용하며, 이는 파서에 의해 강제됩니다. 인터프리터는 각 분기에 대해 SETUP_BLOCK 바이트코드를 생성하며, 정확히 하나의 분기가 실행됩니다. elif는 문법적 설탕입니다: 일련의 단순한 if 문이 만들어낼 중첩을 피합니다. 각 조건은 모든 선행 조건이 거짓일 때만 지연 평가됩니다.

python
score = 87

if score >= 90:
    print("A grade")
elif score >= 80:
    print("B grade")
elif score >= 70:
    print("C grade")
else:
    print("Below C")

규칙:

  • if는 필수이며 항상 먼저 나옵니다
  • elif("else if"의 약자)는 선택 사항이며 필요한 만큼 가질 수 있습니다
  • else는 선택 사항이며, 어떤 조건과도 일치하지 않은 모든 것을 처리하고, 마지막에 옵니다
  • 파이썬은 각 블록에 속한 내용을 표시하기 위해 들여쓰기(4개의 공백)를 사용합니다. 중괄호는 없습니다

들여쓰기는 선택 사항이거나 외관상의 문제가 아닙니다. 파이썬은 이를 구조 정의에 사용합니다. 일관성 없는 들여쓰기는 문법 오류입니다.

한 줄 조건문

간단한 예/아니오 할당의 경우, 파이썬에는 삼항식이라는 간결한 한 줄 형식이 있습니다: value_if_true if condition else value_if_false. 로직이 진정으로 간단하고 문장처럼 읽힐 때만 사용하세요.

조건 표현식(삼항 연산자)은 조건에 따라 두 값 중 하나로 평가됩니다. 이것은 문이 아니라 표현식이므로, 값이 예상되는 어디든 나타날 수 있습니다: f-string 내부, 함수 인수로, 할당에서. 간단한 예/아니오 경우에 사용하세요; elif가 관련된 모든 경우에는 전체 버전을 작성하세요.

조건 표현식 x if condition else y는 조건을 평가하고 다른 분기를 실행하지 않고 x 또는 y를 반환하는 단일 표현식입니다. 이는 POP_JUMP_IF_FALSE 바이트코드의 조합에 매핑됩니다. if/else 블록과 달리 여러 문에 걸칠 수 없고 elif를 포함할 수 없습니다; 복잡한 분기에는 전체 if/elif/else 블록이 더 명확합니다.

python
label = "pass" if score >= 50 else "fail"

이것은 삼항식이며 문장처럼 읽힙니다. 로직이 진정으로 간단할 때 사용하세요. elif가 관련된 경우에는 전체 버전을 작성하세요.

while 루프

while 루프는 조건이 True인 동안 블록을 반복합니다. 루프가 몇 번 실행되어야 하는지 미리 알 수 없을 때 사용하세요. 예를 들어 유효한 입력을 기다리거나 작업이 성공할 때까지 재시도하는 경우입니다.

while은 각 반복 전에 조건을 평가하고 조건이 참일 때만 블록을 실행합니다. 종료 조건이 루프 내부에서 변경되는 무언가에 의존하는 루프에 사용하세요. 반복 횟수를 알고 있거나 컬렉션을 순회하는 경우에는 일반적으로 for가 더 깔끔합니다.

while은 각 반복 전에 조건 표현식에 대해 __bool__(또는 __len__)을 호출합니다. 본문은 조건을 수정할 수 있습니다. 내부 break가 있는 while True는 종료 조건이 본문 중간이나 끝에서 평가되어야 할 때의 관용적인 "루프-까지" 패턴입니다. break가 빠진 무한 루프는 일반적인 멈춤의 원인입니다.

python
lives = 3

while lives > 0:
    print(f"Lives remaining: {lives}")
    lives -= 1

print("Game over")

while은 루프가 몇 번 실행될지 미리 알 수 없을 때 가장 좋습니다. 알고 있거나 컬렉션을 순회할 때는 for가 더 깔끔합니다.

break와 continue

break는 남은 반복 횟수에 관계없이 루프를 즉시 종료합니다. continue는 현재 반복의 나머지를 건너뛰고 조건 확인으로 다시 돌아갑니다. 둘 다 자신이 속한 가장 안쪽 루프에만 영향을 미칩니다.

break는 가장 가까운 둘러싼 루프를 종료하고, 그 다음 첫 번째 문으로 제어를 이동시킵니다. continue는 현재 루프 본문의 나머지를 건너뛰고 조건 확인(또는 for 루프의 다음 반복)에서 다시 시작합니다. 둘 다 가장 안쪽 둘러싼 루프에만 영향을 미칩니다.

breakBREAK_LOOP 바이트코드를 발행하여 루프의 코드 블록을 종료하고 연관된 else 절을 건너뜁니다. continue는 컨텍스트에 따라 CONTINUE_LOOP(또는 JUMP_ABSOLUTE)를 발행하여 루프 헤더에서 재개합니다. 둘 다 가장 가까운 둘러싼 루프로 범위가 한정됩니다; 파이썬에는 레이블된 break가 없습니다. 중첩 루프에서 빠져나오려면 부울 플래그를 사용하거나 return이 있는 함수로 재구성하세요.

break는 루프를 즉시 종료합니다:

python
target = 5
num    = 0

while True:
    num += 1
    if num == target:
        print(f"Found {target}")
        break   # 루프 중단

break가 있는 while True:는 종료 조건이 복잡하거나 루프 본문의 끝에서 발생해야 할 때 유효하고 일반적인 패턴입니다.

continue는 현재 반복의 나머지를 건너뛰고 조건 확인으로 돌아갑니다:

python
num = 0

while num < 10:
    num += 1
    if num % 2 == 0:
        continue    # 짝수 건너뛰기
    print(num)      # 홀수만 출력됨: 1, 3, 5, 7, 9

for 루프

for 루프는 시퀀스의 항목을 한 번에 하나씩 순회합니다: 리스트, 문자열, 숫자 범위 등. for 뒤에 이름 지정한 변수는 각 항목을 차례로 받습니다. 카운터를 관리하거나 직접 길이를 확인하지 않아도 됩니다.

for는 이터러블에 대해 iter()를 호출하여 이터레이터를 얻고, StopIteration이 발생할 때까지 그 위에서 next()를 호출합니다. 즉, for는 이터레이터 프로토콜을 구현하는 모든 것에서 작동합니다: 리스트, 문자열, 딕셔너리, 파일, 범위, 그리고 커스텀 객체. 인덱스된 시퀀스에 국한되지 않습니다.

for target in iterableiter(iterable)을 호출하여 이터레이터 객체를 얻은 다음, StopIteration이 발생할 때까지 반복적으로 next(iterator)를 호출하고 결과를 target에 바인딩합니다. for 루프는 내부적으로 StopIteration을 잡습니다. __iter__를 구현하는(또는 __iter____next__ 둘 다 구현하는) 모든 객체는 이터러블입니다. 여기에는 제너레이터 및 파일 객체와 같은 지연 객체가 포함됩니다.

python
players = ["민준", "서연", "지호"]

for player in players:
    print(f"Hello, {player}!")

for 루프는 문자열(문자별로 순회)이나 다른 시퀀스 타입에서도 작동합니다.

range()

range()는 순회할 숫자 시퀀스를 생성합니다. range(5)0, 1, 2, 3, 4를 제공합니다. 시작, 끝, 단계 크기를 제어할 수 있습니다. 루프가 특정 횟수만큼 실행되어야 할 때 사용하세요.

range(start, stop, step)start부터 stop 미만까지(포함하지 않음) step씩 증가시켜 정수를 생성합니다. 이는 지연 시퀀스입니다: 리스트를 생성하지 않고, 필요에 따라 숫자를 생성합니다. 이로 인해 range(10_000_000)은 메모리 효율적입니다. 세 가지 형식 모두 역방향 카운팅을 위한 음수 인수를 받아들입니다.

range는 함수가 아닌 타입입니다. range(n)은 시퀀스를 구체화하지 않고 O(1)에서 멤버십과 인덱싱을 계산하는 range 객체를 만듭니다. 이것은 len(), in, 슬라이싱, 역순 순회를 지원합니다. 내부적으로는 시작, 끝, 단계만 저장합니다. 순회만 필요한 경우 list(range(n))보다 이것을 선호하세요.

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

range()에는 세 가지 형식이 있습니다:

호출생성하는 것
range(5)0, 1, 2, 3, 4
range(2, 6)2, 3, 4, 5
range(0, 10, 2)0, 2, 4, 6, 8 (2씩 증가)
range(5, 0, -1)5, 4, 3, 2, 1 (카운트다운)

range()는 리스트를 생성하지 않습니다. 숫자를 한 번에 하나씩 생성하므로 매우 큰 범위에서도 효율적입니다.

enumerate()

enumerate()는 루프 진행 중에 인덱스와 값을 모두 제공하므로 카운터를 별도로 추적할 필요가 없습니다. i, player 부분은 각 반복에서 자동으로 한 쌍의 값을 받습니다.

enumerate(iterable, start=0)는 모든 이터레이터를 감싸 (index, value) 튜플을 산출합니다. start 매개변수는 카운터를 오프셋시키지만 기본 인덱스는 변경하지 않습니다. 카운터 변수를 관리하는 것보다 enumerate()를 선호하세요; 더 깔끔하고 오류가 발생하기 어렵습니다.

enumerate는 모든 이터레이터를 (count, value) 쌍을 산출하는 enumerate 객체로 감쌉니다. start 인수는 초기 카운터 값을 설정합니다. for 헤더에서 언패킹(for i, v in enumerate(...))은 각 산출된 항목이 튜플이기 때문에 작동합니다. enumerate는 카운터 외에 추가 메모리 할당 없이 단계당 O(1)입니다.

python
players = ["민준", "서연", "지호"]

for i, player in enumerate(players):
    print(f"{i + 1}. {player}")
# 1. 민준
# 2. 서연
# 3. 지호

i, player 문법은 언패킹이라고 부릅니다. 파이썬이 (index, value) 쌍을 자동으로 두 이름으로 나눕니다.

기본적으로 enumerate()는 0에서 시작합니다. 변경하려면 시작 값을 전달하세요:

python
for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")    # 1에서 시작

중첩 루프

다른 루프 안에 루프를 넣을 수 있습니다. 내부 루프는 외부 루프의 한 번의 반복마다 완전히 실행됩니다. 격자, 조합, 또는 두 단계 구조를 가진 모든 데이터를 처리하는 방식입니다.

중첩 루프는 외부 길이 m과 내부 길이 n에 대해 O(m × n) 반복 횟수를 가집니다. 중첩 루프 내부의 breakcontinue는 가장 안쪽 루프에만 영향을 미칩니다. 여러 레벨에서 빠져나오려면 플래그 변수를 사용하거나 함수로 재구성하세요.

for 루프 호출은 새로운 이터레이터 객체를 생성합니다. 중첩 루프는 이터레이터를 독립적으로 구성합니다. break는 가장 안쪽 루프만 종료합니다; 파이썬에는 레이블된 break가 없습니다. 일반적인 해결책은 플래그 변수를 사용하거나 내부 루프를 함수로 캡슐화하고 return을 사용하는 것입니다. 데카르트 곱의 경우 itertools.product가 중첩 for 루프보다 더 가독성 있습니다.

python
rows = [1, 2, 3]
cols = ["A", "B"]

for row in rows:
    for col in cols:
        print(f"{col}{row}", end=" ")
    print()   # 각 행 다음에 줄바꿈
# A1 B1
# A2 B2
# A3 B3

중첩 루프 내부의 breakcontinue는 가장 안쪽 루프에만 영향을 미칩니다.

Loop-else

파이썬 루프는 break에 걸리지 않고 루프가 완료된 경우에만 실행되는 else 절을 가질 수 있습니다. 자주 사용되지는 않지만, "리스트를 검색하고 아무것도 찾지 못하면 이것을 수행"이라고 쓰는 가장 깔끔한 방법입니다.

for 또는 while 루프의 elsebreak에 걸리지 않고 루프가 정상적으로 완료될 때(이터러블을 소진하거나 조건이 거짓이 됨) 실행됩니다. 별도의 발견 플래그 변수 없이 "검색하고 찾지 못한 경우 보고"하는 관용적인 패턴입니다.

루프 elseBREAK_LOOP 바이트코드에 의해 제어됩니다: break가 발동하면 else 블록의 셋업 코드가 진입되지 않습니다. 루프가 소진되면 else 블록이 실행됩니다. 이것은 if의 일반적인 else와는 의미적으로 다릅니다. 주된 실용적 사용은 break를 포함한 검색 패턴입니다; 그 외에는 거의 보이지 않으며 익숙하지 않은 독자를 혼란스럽게 할 수 있습니다.

python
target = "수빈"
names  = ["민준", "서연", "지호"]

for name in names:
    if name == target:
        print(f"Found {target}")
        break
else:
    print(f"{target} not in list")   # break가 발동하지 않았기 때문에 실행됨

break가 실행되면 else는 건너뜁니다. 루프가 시퀀스를 소진하면 else가 실행됩니다. 틈새 패턴이지만 플래그 변수보다 깔끔합니다.

정렬

sorted()는 새로운 정렬된 리스트를 반환하고 원본은 그대로 둡니다. .sort()는 리스트를 제자리에서 정렬하고 None을 반환합니다. key= 인수를 사용하면 원시 값이 아닌 다른 것으로 정렬할 수 있습니다. 예를 들어 이름을 대소문자 구분 없이 정렬하거나 플레이어 튜플을 점수로 정렬할 수 있습니다.

sorted()는 안전한 기본값입니다: 원본을 절대 수정하지 않습니다. .sort()는 제자리에서 수정하고 None을 반환합니다. 둘 다 내림차순을 위해 reverse=True를 받아들입니다. key= 인수는 비교 전에 각 요소에 적용되는 함수를 받습니다. 이는 정렬 기준을 데이터로부터 분리합니다.

둘 다 Timsort를 사용합니다: 최악의 경우 O(n log n), 거의 정렬된 데이터에서는 O(n), 안정적. .sort()는 의도적으로 None을 반환합니다(명령-쿼리 분리). key= 함수는 비교당 한 번이 아니라 요소당 한 번 호출되므로 비싼 키 계산이 반복되지 않습니다. key=str.lower는 언바운드 메서드 참조이고; key=lambda p: p[1]은 특정 필드에 접근하기 위한 인라인 함수입니다.

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)           # [42, 55, 71, 87, 96] (새 리스트)
scores.sort()                     # 원본 리스트를 정렬, None 반환
scores.sort(reverse=True)         # [96, 87, 71, 55, 42]

둘 다 key= 인수를 받아들입니다: 비교 전에 각 항목에 적용되는 함수입니다:

python
names = ["지호", "민준", "서연"]
sorted(names, key=str.lower)       # 대소문자 구분 없는 정렬

players = [("민준", 87), ("서연", 96), ("지호", 55)]
sorted(players, key=lambda p: p[1])   # 점수로 정렬

람다란?

lambda p: p[1]은 한 줄 함수입니다. 플레이어 튜플을 받아 점수를 반환합니다. 람다 함수는 람다, 컴프리헨션, zip 장에서 다룹니다.

간단한 경우에는 sorted()를 사용하세요. 제자리에서 수정하려는 리스트의 경우 .sort()를 사용하세요.

실전에서

점수를 순회하면서 총합을 누적하고, 합격 등급을 세고, 요약을 출력합니다:

python
raw_scores = [87, 42, 96, 55, 71, 63]

total   = 0
passing = 0

for score in raw_scores:
    total += score
    if score >= 60:
        passing += 1

average = total / len(raw_scores)
print(f"Average: {average:.1f}")
print(f"Passing: {passing}/{len(raw_scores)}")
print(f"Top score: {sorted(raw_scores, reverse=True)[0]}")

파일 리스트를 정렬된 순서로 처리하고, 너무 큰 것은 건너뛰며, 몇 개가 건너뛰어졌는지 보고합니다:

python
files = [
    {"name": "report_jan.csv", "size_mb": 12},
    {"name": "report_feb.csv", "size_mb": 850},
    {"name": "report_mar.csv", "size_mb": 7},
]

MAX_SIZE = 100
skipped  = 0

for f in sorted(files, key=lambda x: x["name"]):
    if f["size_mb"] > MAX_SIZE:
        print(f"Skipping {f['name']} ({f['size_mb']} MB, too large)")
        skipped += 1
    else:
        print(f"Processing {f['name']}...")

print(f"\nDone. {skipped} file(s) skipped.")

요청 로그에서 오류를 스캔한 다음, 성공 또는 시도 제한에 도달하면 종료되는 재시도 루프를 사용합니다:

python
requests = [
    {"method": "GET",  "path": "/users",  "status": 200},
    {"method": "POST", "path": "/users",  "status": 201},
    {"method": "GET",  "path": "/broken", "status": 500},
]

errors = []

for req in requests:
    if req["status"] >= 400:
        errors.append(req)

if errors:
    print(f"{len(errors)} error(s) in request log:")
    for err in errors:
        print(f"  {err['method']} {err['path']} -> {err['status']}")
else:
    print("All requests succeeded")

attempts    = 0
max_retries = 3
success     = False

while attempts < max_retries and not success:
    attempts += 1
    print(f"Attempt {attempts}...")
    success = attempts >= 2   # 두 번째 시도에서 성공 시뮬레이션

print("Connected" if success else "Failed after all retries")