6.2 반복에서 반복문을 이용해 컬렉션을 순회하는 방법을 설명했다. 특히 for 문을 활용하면 while 문에서 프로그래머가 직접 작성해야 하는 반복 패턴을 간단히 나타낼 수 있어 편리하다는 점도 알아보았다.

이 절에서는 여러 가지 시퀀스와 원소의 순서가 없는 사전과 매핑을 순회하는 방법을 알아 본다. 그리고 반복문을 중첩해 중첩된 컬렉션을 순회하는 방법도 알아 본다.

7.2.1 시퀀스 순회하기

컬렉션을 순회할 때는 for 문을 사용하는 것이 편리하다. 6장에서 배운 내용을 떠올려 보자.

코드 7-9 리스트 순회하기

# 리스트 순회하기
for element in ['a', 'b', 'c', 'd']:
    print(element)

for 문은 in 예약어의 오른쪽에 정의한 컬렉션의 원소를 하나씩 꺼내 in 예약어의 왼쪽에 정의한 변수(element)에 대입한 후, 본문의 코드를 원소 개수만큼 실행한다. 따라서 코드 7-9를 실행하면 화면에 a, b, c, d가 한 행씩 차례대로 출력된다. 에제에서는 본문에 print(element)만을 수행하도록 했으나 리스트를 반복하며 실행해야 할 명령이라면 얼마든지 원하는대로 넣을 수 있다.

for 문은 리스트 외에도 다양한 시퀀스를 대상으로 실행할 수 있다.

코드 7-10 다양한 시퀀스 순회하기

# 튜플 순회하기
for element in ('월', '화', '수', '목', '금'):
    print(element)

# 문자열 순회하기
for character in '가나다라':
    print(character)

# 레인지 순회하기
for n in range(10)
    print(n)

튜플을 순회하는 방법은 리스트를 순회하는 것과 거의 동일하다. 문자열은 각 문자를 원소로 하는 시퀀스이므로 순회가 가능하다. 레인지를 순회하는 것은 6장에서 이미 살펴본 바 있다.

7.2.2 집합과 사전 순회하기

집합과 사전은 순서가 없는 컬렉션이다. 그래서 언뜻 생각하기에는 이들을 순회할 수 없을 것 같다. 하지만 순서가 없다 뿐이지 for 문을 이용해 순회하는 것이 가능하다. 먼저 집합을 순회하여 출력해 보자.

코드 7-11 집합 순회하기

# 집합 순회하기
for element in {'김치', '참치', '꽁치', '갈치'}:
    print(element)

실행 결과:

참치
김치
꽁치
갈치

보다시피 집합도 순회가 가능하다. 단, 집합에는 원소의 순서가 없어 element 변수에 어떤 순서로 원소가 대입될지는 알 수 없다. 집합을 순회할 때는 순서를 보장할 수 없다는 점을 염두에 두어야 한다.

한편, 사전을 순회할 때는 사전의 키를 순회할 것인지, 값을 순회할 것인지, 키-값 쌍을 함께 순회할 것인지를 고려해야 한다. OECD 회원국의 행복도를 나타내는 사전을 순회한다고 해 보자. 사전은 기본적으로 키를 기준으로 순회한다.

코드 7-12 키를 기준으로 사전 순회하기

happiness = {
    '호주': 7.95,
    '노르웨이': 7.9,
    '미국': 7.85,
    '일본': 6.2,
    '한국': 5.75,
}

# 사전 순회하기 - 키를 기준으로
for k in happiness:
    print(k, happiness[k])

실행 결과:

호주 7.95
노르웨이 7.9
미국 7.85
일본 6.2
한국 5.75

파이썬 공식 인터프리터 기준으로, 사전은 원소의 순서를 유지한 채 순회된다. 그러나 언어 정의 상으로는 순서가 보장되지 않는다. 사전을 순회할 때는 집합과 마찬가지로 순서가 없는 컬렉션임을 염두에 둬야 한다.

위 코드에서는 키를 기준으로 사전을 순회하기 때문에 값을 읽기 위해 happiness[k] 표현을 사용했다. 다음과 같이 사전의 키-값 쌍의 시퀀스를 구하는items() 메서드(5장)를 활용하면 키와 값을 나란히 순회할 수 있다.

코드 7-13 사전의 키와 값을 나란히 순회하기

# 사전 순회하기 - 키(k)와 값(v)을 나란히
for k, v in happiness.items():
    print(k, v)

코드 7-13은 사전 happiness를 직접 순회하는 대신 happiness.items()를 순회한다. 화면에 출력되는 결과는 코드 7-12과 동일하다. happiness.items()는 키와 값의 쌍(('호주', 7.95)와 같이 두 원소로 이뤄진 튜플)을 원소로 갖는 시퀀스이며, for 문의 각 반복주기에서는 변수에 이 키와 값의 쌍이 대입된다. 키와 값의 쌍은 하나의 변수에 튜플로 대입할 수도 있지만, 예제에서는 5.5 패킹과 언패킹에서 알아본 언패킹 기능을 이용해 k, v 두 원소에 나누어 대입하였다. 그 결과 for 문의 본문에서 키는 k로, 값은 v로 각각의 변수로 표현할 수 있다.

연습문제

연습문제 7-4 메뉴판 출력하기

다음은 음료의 이름과 가격을 표현한 메뉴판이다. 이 메뉴판은 음료의 이름이 키로, 음료의 가격이 값으로 된 사전으로 정의되어 있다.

menu_dict = {
    '아메리카노': 2500,
    '카페 라떼': 3000,
    '딸기 주스': 3500,
}

이 사전을 순회하여, 다음과 같이 출력해 보아라.

아메리카노의 가격은 2500 원 입니다.
카페 라떼의 가격은 2500 원 입니다.
딸기 주스의 가격은 3500 원 입니다.

7.2.3 중첩 컬렉션 순회하기

7.1 중첩 컬렉션에서 리스트와 사전을 여러 개 중첩하여 나타낼 수 있음을 알아보았고, 6.2.7 반복 중첩하기에서는 for 문을 여러 번 중첩하여 실행할 수 있음을 알아보았다. 이를 이용해 중첩 컬렉션을 순회해 보자. 다음은 코드 7-3에서 작성해 보았던 체스판을 표현한 이차원 리스트다.

코드 7-14 체스판을 나타내는 이차원 리스트

board = [
    [['black', '룩'], None, None, None, None, None, None, None],
    [None, None, None, ['black', '킹'], None, None, None, None],
    [None, None, None, None, None, None, None, None],
    [None, None, None, None, None, None, None, None],
    [None, None, ['white', '비숍'], None, None, None, None, None],
    [None, None, None, None, None, None, None, None],
    [None, None, None, None, None, None, None, None],
    [None, None, None, None, ['white', '킹'], None, None, None],
]

이 리스트를 순회하며 체스판을 화면에 출력해 보자. 첫번째 단계로, 바깥쪽의 리스트를 순회하며 원소(체스판의 각 행)를 출력해 보자.

코드 7-15 체스판의 각 행 출력하기

for row in board:  # 체스판의 각 행을 순회
    print(row)

지금까지 학습해 온 여러분이라면 각 행을 출력하는 것은 간단하게 느껴질 것이다. 그저 for 문을 이용해 각 행을 변수(행을 뜻하는 row로 하였다)에 전달하도록 하고 본문에서 출력하면 된다. 출력 결과는 다음과 같다.

[['black', '룩'], None, None, None, None, None, None, None]
[None, None, None, ['black', '킹'], None, None, None, None]
[None, None, None, None, None, None, None, None]
[None, None, None, None, None, None, None, None]
[None, None, ['white', '비숍'], None, None, None, None, None]
[None, None, None, None, None, None, None, None]
[None, None, None, None, None, None, None, None]
[None, None, None, None, ['white', '킹'], None, None, None]

이제 안쪽 리스트도 출력하도록 해 보자. 이번에는 앞에서 작성한 for 문의 본문 블록에 각 행의 원소(체스말)를 출력할 for 문을 작성해야 한다.

코드 7-16 체스판 전체 순회하며 출력하기

for row in board:      # 체스판의 각 행을 순회
    for piece in row:  # 행의 각 체스말을 순회
        # 체스말이 없으면 . 출력
        if piece is None:
            print('.', end=' ')
        # 체스말이 있으면 I 출력
        else:
            print('I', end=' ')
    print()            # 줄바꿈

바깥쪽 for 문의 본문은 이제 두 개의 명령으로 늘어났다. 하나는 안쪽 for 문이고, 다른 하나는 체스판의 각 행마다 줄바꿈을 하는 print()다. 안쪽 for 문은 각각의 행(row)을 순회한다. 행의 원소(체스말)은 순서대로 piece 변수에 대입되고, 안쪽 for 문의 본문에서는 체스말이 (체스말이 없음을 나타내는) None인지 아니면 체스말의 정보를 나타내는 리스트인지를 확인한 후, 그에 맞는 텍스트를 출력한다. 이 코드의 실행 결과는 다음과 같다.

실행 결과:

I . . . . . . . 
. . . I . . . . 
. . . . . . . . 
. . . . . . . . 
. . I . . . . . 
. . . . . . . . 
. . . . . . . . 
. . . . I . . . 

비록 화려한 그래픽은 아니지만 체스판 데이터를 훌륭하게 시각적으로 묘사했다. 코드 길이를 짧게 하기 위해 체스말을 I로만 표시했으나 원한다면 체스말의 종류와 팀에 따라 출력되는 문자를 다르게 할 수도 있을 것이다. 그건 여러분의 몫으로 남겨둔다. 이처럼 여러 번 중첩된 컬렉션이라도 for 문을 중첩함으로써 전체 원소를 빠짐없이 순회할 수 있다.

연습문제

연습문제 7-5 육십갑자표

과거에는 연도를 나타내기 위해 육십갑자를 사용했다. 육십갑자는 10개의 천간(십간)과 16개의 지지(십이지)를 교차하여 나열한 것이다. 다음과 같이 천간 리스트와 지지 리스트를 정의한 후,

천간 = ['갑', '을', '병', '정', '무', '기', '경', '신', '임', '계']
지지 = ['자', '축', '인', '묘', '진', '사', '오', '미', '신', '유', '술', '해']

이들을 교차 출력하여 육십갑자표를 출력해 보자.

출력 결과는 다음과 같아야 한다.

갑자 갑축 갑인 갑묘 갑진 갑사 갑오 갑미 갑신 갑유 갑술 갑해 
을자 을축 을인 을묘 을진 을사 을오 을미 을신 을유 을술 을해 
병자 병축 병인 병묘 병진 병사 병오 병미 병신 병유 병술 병해 
정자 정축 정인 정묘 정진 정사 정오 정미 정신 정유 정술 정해 
무자 무축 무인 무묘 무진 무사 무오 무미 무신 무유 무술 무해 
기자 기축 기인 기묘 기진 기사 기오 기미 기신 기유 기술 기해 
경자 경축 경인 경묘 경진 경사 경오 경미 경신 경유 경술 경해 
신자 신축 신인 신묘 신진 신사 신오 신미 신신 신유 신술 신해 
임자 임축 임인 임묘 임진 임사 임오 임미 임신 임유 임술 임해 
계자 계축 계인 계묘 계진 계사 계오 계미 계신 계유 계술 계해 

7.2.4 여러 개의 컬렉션을 나란히 순회하기

서로 독립적인 여러 개의 컬렉션을 나란히 순회해야 할 때도 있다. 예를 들어, 다음 코드의 두 리스트를 동시에 순회하면서 그 내용을 출력하는 프로그램을 작성해야 한다고 해 보자.

코드 7-17 서로 독립된 여러 개의 컬렉션

>>> seasons = ['봄', '여름', '가을', '겨울']
>>> mountains = ['금강산', '봉래산', '풍악산', '개골산']

중첩된 구조를 순회할 때처럼 for 문을 중첩하여 순회하도록 하면 어떨까?

코드 7-18 반복을 중첩하는 것으로는 두 리스트를 나란히 순회할 수 없다

>>> for season in seasons:
...     for mountain in mountains:
...         print(season + '에는 ' + mountain)
... 
봄에는 금강산
봄에는 봉래산
봄에는 풍악산
봄에는 개골산
여름에는 금강산
여름에는 봉래산
여름에는 풍악산
여름에는 개골산
가을에는 금강산
가을에는 봉래산
가을에는 풍악산
가을에는 개골산
겨울에는 금강산
겨울에는 봉래산
겨울에는 풍악산
겨울에는 개골산

이 결과에서 보듯, 두 리스트는 중첩된 관계가 아니기 때문에 for 문을 중첩하는 것으로는 원하는 결과를 얻을 수 없다. 우리가 원하는 동작은 seasons의 첫번째 원소와 mountains의 첫번째 원소를 동시에 출력하고, 그 다음으로 seasons의 두번째 원소와 mountains의 두번째 원소를 동시에 출력하는 식의 동작이다. 하지만 중첩 반복에서는 seasons의 각 원소마다 mountains의 모든 원소가 출력되어버린다.

zip() 함수로 엮기

이 문제는 zip() 함수를 사용하여 해결할 수 있다. zip() 함수는 여러 개의 컬렉션을 입력받아 각 원소를 순서대로 엮는다. 예를 들어 [1, 2, 3], ['a', 'b', 'c'], ['가', '나', '다'] 세 개의 시퀀스를 zip() 함수로 엮은 결과는 다음과 같다.

코드 7-19 zip() 함수의 동작

>>> list(zip([1, 2, 3], ['a', 'b', 'c'], ['가', '나', '다']))
[(1, 'a', '가'), (2, 'b', '나'), (3, 'c', '다')]

zip() 함수를 이용해 두 리스트를 나란히 순회해 보자.

코드 7-20 zip() 함수를 이용해 두 리스트를 나란히 순회하기

>>> for season, mountain in zip(seasons, mountains):
...     print(season + '에는 ' + mountain)
... 
봄에는 금강산
여름에는 봉래산
가을에는 풍악산
겨울에는 개골산

위 코드에서는 seasonsmountains를 직접 순회하지 않고, 두 리스트를 zip() 함수로 묶은 후 순회하였다. zip() 함수가 반환하는 시퀀스의 각 원소는 seasonsmountains의 각 원소를 순서대로 엮은 튜플이기 때문에 이를 언패킹하여 seasons 변수와 mountain 변수에 대입하도록 했다. 이런 요령으로 여러 개의 컬렉션을 나란히 순회할 수 있다.

연습문제

연습문제 7-6 두 시퀀스의 원소 합하기

두 시퀀스의 원소를 합하는 함수 plus_elements()를 정의하여라. 이 함수는 두 개의 시퀀스를 전달받은 후 각 원소를 순서대로 합한 리스트를 반환한다. 예를 들어, (1, 2, 3)[4, 5, 6]을 전달받은 경우 [5, 7, 9]를 반환한다.