# 12. 마법의 사물함을 이용해서 데이터 다루기

1 학습 내용

1.1 목표

본 단원에서는 데이터를 묶어서 한 개의 덩어리로 쉽게 다룰 수 있도록 하거나, 많은 데이터를 담을 수 있도록 해주는 파이썬의 리스트를 학습한다.

1.2 목차

1 학습 내용

2 많은 양의 데이터를 관리할 때의 문제점

3 마법의 사물함, 리스트

4 리스트와 반복문의 만남

5 리스트 생성 및 기타 연산

6 미로게임 수정 (함정이 1개이며 리스트를 활용하는 경우)

7 미로게임 수정 (함정이 여러 개이고 리스트를 활용하는 경우)

연습문제

2 많은 양의 데이터를 관리할 때의 문제점

데이터가 2 개인 경우

사과가 10개 있다고 하자. 변수 x를 사용해서 표현하면:

>>> x = 10
>>> print(x)
10

사과가 10개 있다가 15개로 늘었다고 하자. 변수 x에 15를 저장한다. 그러면 이전 값 10은 삭제된다 변수는 1개 값만 저장할 수 있다. 그 변수에 또 다른 값을 저장하려면 이미 저장된 값을 삭제해야 한다.

>>> x = 15
>>> print(x)
15

변수를 사용해서 2개 값을 저장하려면 x=10, y=15와 같이 변수를 2개 사용해야 한다. 저장할 값의 개수가 증가할수록 변수의 개수로 당연히 늘어난다.

미로 게임에 함정이 여러 개라면?

또 한 개의 예를 들어보자. 11장에서 사용자가 키보드를 이용해서 거북이 로봇이 함정에 빠지면 빨강색으로 표시해서 잘못 움직였음을 알려주는 프로그램을 작성하였다. 11장에서는 함정 영역을 사각형 형태로 표현하면서 두 개 점의 좌표를 x1, y1, x2, y2라고 이름 붙여 사용하였다. 이때는 함정이 한 개 밖에 없었으니 변수 4개로 함정 영역을 표시하는데 문제가 없었다. 만약 함정을 여러 개 만든다면 어떻게 해야 할까?

  • 예를 들어 함정 한 개일 때에는 x1, y1, x2, y2가 필요하다.
  • 함정이 두 개라면 변수 4개 (x3, y3, x4, y4)를 추가해 8개가 필요하다.
  • 함정이 10개 만들어진다면 x5, y5, x6, y6, ..., x19, y19, x20, y20 까지 총 40개 변수가 필요하다.
  • 함정이 100개가 된다면 더 심각해진다. 모두 400개의 변수가 필요하고, 함정의 개수가 많아질 수록 변수의 개수가 많아지게 된다.

함정의 개수가 늘어나면서 if문도 증가하게 된다. 함정에 빠졌는지 확인하는 함수 isInTrap() 를 생각해보자. 아래 코드는 11장에서 사용했던 오른쪽 화살표 키에 대한 이벤트 처리 함수이다.

def keyeast():
    position = t1.pos()
    t1.goto(position[0] + 10, position[1])
    if isInTrap(position[0] + 10, position[1], x1, y1, x2, y2):
        showTrapInRed(x1, y1, x2, y2)
        writeGameOver()

만약 이 코드를 수정해서 함정 2개를 처리하도록 만들어야 한다면 아래 코드와 같이 수정된다.

def keyeast():
    position = t1.pos()
    t1.goto(position[0] + 10, position[1])
    if isInTrap(position[0] + 10, position[1], x1, y1, x2, y2):
        showTrapInRed(x1, y1, x2, y2)
    if isInTrap(position[0] + 10, position[1], x3, y3, x4, y4):
        showTrapInRed(x3, y3, x4, y4)
    writeGameOver()

변경 부분을 살펴보면, isInTrap() 함수를 2번 호출한다. 함정좌표가 x1, y1, x2, y2만 있던 것에 x3, y3, x4, y4까지 추가되었다. 만약 함정이 10개가 된다면 아래 코드와 같이 구현해야 한다. 즉 if문으로 되어 있는 코드가 모두 10번 사용될 것이다.

def keyeast():
    position = t1.pos()
    t1.goto(position[0] + 10, position[1])
    if isInTrap(position[0] + 10, position[1], x1, y1, x2, y2):
        showTrapInRed(x1, y1, x2, y2)
    if isInTrap(position[0] + 10, position[1], x3, y3, x4, y4):
        showTrapInRed(x3, y3, x4, y4)
    ...    # 중간 생략
    if isInTrap(position[0] + 10, position[1], x17, y17, x18, y18):
        showTrapInRed(x17, y17, x18, y18)
    if isInTrap(position[0] + 10, position[1], x19, y19, x20, y20):
        showTrapInRed(x19, y19, x20, y20)    
    writeGameOver()

만약 100개의 함정이 만들어진다면 중복 코드가 100회 발생한다. 비슷한 코드가 여기만 있는 것이 아니라 왼쪽/오른쪽/아래쪽/위쪽 화살표 키에 대한 이벤트 처리 함수에도 필요하다. 즉 함정의 개수가 한 개씩 늘어날 때마다 결국 상당한 중복이 발생한다.

데이터가 많아진다면?

지금까지는 함정 개수만을 가지고 얘기를 했는데 일상 생활에서 발생할 수 있는 또 다른 예를 생각해보자. 학생 이름을 변수로 사용해서, 등록 학생 명단에서 휴학, 복학, 재학을 따로 분류해내는 작업을 한다고 하자. 전체 학생 숫자에 해당되는 만큼의 변수가 만들어져야 하고, 등록 여부를 확인하는 코드 역시 각 변수마다 존재해야 할 것이다. 많은 데이터를 처리하는 경우에도 변수만을 사용하면 매우 어렵다.

학생들 성적을 관리하는 프로그램을 작성해야 한다면 어떻게 될까? 만약 수백 명 혹은 수천 명의 학생들의 점수를 관리해야 한다면? 학생 한 명이 5~6과목 정도를 듣는다고 하면, 수천 개 혹은 수만 개의 데이터가 나올 것이다. 이것들을 모두 따로 변수로 만들어야 한다면 어떻게 될까? 생각만 해봐도 아찔하지 않은가? 그 많은 변수를 코드로 넣어야 하고 또 그것들을 관리해야 한다면 말이다.

그 밖에도 예는 많다. 대규모 학원에서 수백 명 수강생의 평균 성적을 계산하는 경우도 있을 것이다. 또 대규모 단지 아파트 주민의 관리비를 냈는지 안냈는지 확인해야 하는 경우도 유사하다. 전 국민이 투표할 수 있는 대선 투표에서 투표 용지 관리는 어떨까?

정리하면, 함정을 비롯해서 데이터의 개수를 늘리니까 발생하는 문제는 크게 두 가지이다. 첫 번째는 변수의 개수가 늘어나는 것이고 두 번째는 그 변수를 사용하는 코드들이 비슷하지만 완전히 같지는 않아 함수로 처리하기 어려울 정도로 조금씩 다르기 때문에 결국 같은 패턴의 코드 반복이 늘어난다는 점이다.

3 마법의 사물함, 리스트

이러한 문제들을 해결할 수 있는 방법은 없는 것일까? 결론부터 말하자면 답은 "있다"이다. 컴퓨터는 대용량의 데이터를 처리하기에 적합하다고 했었다. 여기서 적합하다고 하는 것은 단순히 컴퓨터가 아무리 많은 일을 시키더라도 불평을 하지 않는다는 의미만은 아니라, 프로그램을 작성할 때 많은 양의 비슷한 데이터들이 있으면 관련 데이터로 묶어 처리하는 기법을 활용하는 것이다.

보통 프로그래밍 언어에는 많은 데이터를 저장하고, 반복문을 이용해서 코드를 길게 작성하지 않아도 해결할 수 있도록 제공하는 기능들이 있다. 일부 프로그래밍 언어들에서는 이러한 기능들을 배열(Array)이라고 부르기도 하며, 파이썬에서는 리스트(List)라고 부른다.

간단한 리스트 만들기

리스트 같은 자료구조를 사용하면 변수의 개수를 늘리지 않아도 된다. 앞에서 x는 변수였다. 지금은 x를 리스트로 사용해보자. 잠깐! 앞서 x에 값을 여러 개 넣으면 덮어썼다. 변수 x에 여러 개의 값을 넣으려면 어떻게 해야 할까? 이런 경우 x를 리스트라고 '특이하게' 정의(define)를 해야 한다. 거북이 로봇을 생성할 때 turtle.Turtle() 함수를 사용했던 것과 유사하게 list() 함수는 리스트 객체를 생성해서 반환하다. 따라서 아래 코드에서 첫 번째 줄에 있는 list() 코드는 비어 있는 리스트(요소가 한 개도 없는 리스트) 객체를 만들어서 돌려주게 되고, 여기에 x라는 이름을 붙인다. 데이터를 넣는 방식도 주의를 기울여야 한다. 리스트를 사물함을 하나씩 연결하는 비유를 떠올리면서 이해하면 append()라고 하는 이름의 함수를 기억하기 쉽다. 나중에 다시 자세히 설명하겠지만, 리스트의 append() 함수는 주어진 인자 값을 리스트에 추가한다. print() 함수를 사용해서 리스트를 출력하면, 대괄호 안에 값이 들어간 상태로 출력된다.

>>> x = list()
>>> x.append(1)
>>> x.append(2)
>>> x.append(3)
>>> print(x)
[1, 2, 3]

@@박스처리 시작

괄호 파이썬 입출력 시 괄호에 주의해야 한다. 대괄호는 우리가 지금 배우는 리스트를 의미한다. 이 책에서는 범위를 넘어서지만 중괄호는 딕션너리, 세트를 소괄호는 튜플에 쓰인다. 즉, 괄호의 생김새는 자료구조의 타잎에 따라 결정된다. 참고로 소괄호는 함수를 호출할 때 사용할 수 있다.

구분 설명 사용 예
대괄호 [ ] 대괄호는 리스트에 쓰인다. x=[1,2,3], x[0]
중괄호 { } 중괄호는 딕셔너리(dictionary), 세트(set)와 같은 자료구조에 쓰인다. x={'name'='홍길동'}
소괄호 ( ) 소괄호는 튜플(tuple)에 쓰인다. 또한 함수를 나타낼 때 쓰인다. x=(1,2,3), x[0], print()

@@박스처리 끝

간단한 리스트 코드 살펴보기

변수는 1개의 값을 저장하지만, 리스트는 여러 값을 저장할 수 있다고 했다. 사물함에 비유하면 리스트는 사물함 집합이다. 값을 꺼내 쓰는 방법도 변수와 비교하면 다르다. 보통 사물함에 이름을 붙이거나 번호를 붙여서 사용하는 것처럼, 파이썬의 리스트도 요소들에 번호를 붙여서 사용한다. 우선 리스트에서 데이터를 읽는 예제 코드를 보자. 아래 코드는 숫자 5개를 담은 리스트의 이름을 변수 a라고 붙이고, 한 개씩 출력하는 것을 보이고 있다. 파이썬 쉘에서 실행시키면 아래처럼 결과가 나올 것이다.

>>> a = [2, 7, 5, 4, 3]
>>> print(a[0])
  2
>>> print(a[1])
  7
>>> print(a[2])
  5
>>> print(a[3])
  4
>>> print(a[4])
  3

리스트를 만드는 방 법은 두 가지가 있다. 첫 번째로는 처음에 보인 코드처럼 list()라는 함수를 사용하는 것이다. 이 함수를 이용해서 리스트 객체를 만들고, 이에 데이터를 추가할 수 있다. 두 번째 방법은 바로 앞에 있는 코드에서 보인 것처럼 대괄호 []기호를 이용해서 만든다. 즉 변수를 만들면서 값을 줄 때 [] 사이에 리스트를 구성하는 요소(element)들을 ','로 분리해서 넣어주기만 하면 된다. 첫 번째 방법은 빈 리스트를 만들어서 데이터를 새롭게 추가할 수 있는 것이고, 두 번째 방법은 이미 어느 정도 데이터를 가지고 있는 리스트를 만들 때 사용할 수 있다.

리스트는 여러 개의 변수들을 묶어 놓은 것이라고 보면 된다. 그리고 그 리스트의 개별 요소의 값(즉 리스트 안에 있는 한 개 변수의 값)을 추출하는 것은 마찬가지로 []를 이용한다. 즉 리스트는 대괄호로 표현한다. []는 인덱스 연산자(Index Operator)라고 부르며 대괄호 사이에는 순서를 나타내는 숫자를 쓴다. 이를 인덱스(Index)라고 한다. 리스트의 요소의 위치를 인덱스 값으로 전달하면 해당 부분에 대한 요소를 뽑아준다. 리스트의 인덱스는 반드시 0부터 시작하며 순서대로 0, 1, 2, ... 라는 인덱스로 값을 추출하거나 값을 넣는데 사용할 수 있다. 참고로 리스트의 길이는 len()함수를 이용해서 확인할 수 있다. 아래 코드를 살펴보면 리스트를 두 개 만들고 각각의 길이를 화면에 출력한다.

>>> a = [1, 3, 5, 7, 9] 
>>> b = [1, 3, 5]
>>> print(len(a))
  5
>>> print(len(b))
  3
>>> print(a)
  [1, 3, 5, 7, 9]
>>> print(b)
  [1, 3, 5]

위에 있는 코드에서 보면, a라는 리스트를 만들면서 1, 3, 5, 7, 9라는 요소를 담았다. b는 1, 3, 5라는 숫자 값들을 가지고 있다. a 리스트의 길이를 출력하면 5개의 요소가 들어있으므로 5라고 출력되고, b의 길이가 출력되면 3이라고 나온다.

@@박스처리 시작

컴퓨터의 숫자 세기 숫자를 셀 때 사람은 하나, 둘 이렇게 센다. 그러나 컴퓨터는 0, 1이렇게 센다. 1개를 0개, 2개를 1개라고 0부터 시작해서 센다.

>>> print(x[0])
10
>>> print(x[1])
15

@@박스처리 끝

리스트

파이썬의 리스트는 마법의 사물함과 유사하다. 보통 일부 프로그래밍 언어들에서 배열이라고 부르는 것들은 같은 종류의 데이터만 담을 수 있도록 되어 있는 반면에 파이썬의 리스트는 종류와 크기를 가리지 않고 데이터를 담을 수 있다. 심지어 리스트에 다른 리스트를 요소로 넣는 것도 가능하다. 그래서 파이썬의 리스트는 마법의 사물함과 비슷하다고 생각하면 될 것 같다. 위에 있는 그림은 인터넷 무료 이미지 사이트에서 받은 사물함 사진이다. 아래 표는 학교에서 학생들이 주로 사용하는 사물함이나 지하철 역에 있는 사물함의 특징을 생각해보고 그에 대응되는 파이썬의 리스트의 유사점과 차이점을 설명할 것이다. 위에 있는 사진을 보면서 한 번 특징들을 살펴보도록 하자.

사물함 파이썬의 리스트
사물함은 물품 보관용 공간이다. 변수는 값을 저장하고, 리스트도 변수처럼 값을 넣을 수 있는 메모리 공간이다.
사물함은 한 개의 공간이 독립적으로 있는 것이 아니라, 여러 개의 공간들이 함께 붙어 있다. 리스트도 이렇게 데이터를 넣을 수 있는 공간들이 여러 개 묶여 있다고 생각하면 된다.
사물함은 고유한 명칭으로 특정할 수 있다. 예를 들어 "서울시 지하철 1호선 종각역 사물함" 또는 "서울 OO고등학교 3학년 사물함" 등과 같이 이름이나 위치 등을 이용해서 전체 사물함을 나타낼 수 있다. 리스트도 고유한 명칭이 있다. 변수와 같이, 리스트에 이름을 붙이고 값을 저장하는 공간이다.
사물함의 개별 공간함에는 이름이나 번호가 붙어 있어 특정 사물함 공간을 지칭하는 것이 가능하다. 예를 들어, "서울시 지하철 1호선 종각역 3번 사물함에 물건을 넣어두었다"라고 얘기하면 어떤 공간을 의미하는 지가 뚜렷하게 나타난다. 리스트도 각 요소 별로 숫자 번호가 붙어있다. 단 리스트 요소를 이름으로 구별할 수는 없고, 숫자 번호로만 구별한다. 이 숫자를 인덱스(index)라고 하고, 항상 0번부터 시작한다. 리스트의 0번은 리스트의 가장 앞에 있는 공간이나 값을 의미하며, 1번은 그 다음 요소를 의미한다.
일반적인 사물함은 크기가 고정되어 있다. 따라서 정해진 크기보다 더 큰 물품을 손상없이 넣는 것은 불가능하다. 또한 사물함에 다른 사물함을 넣는 것 자체도 불가능하다. 사물함과는 달리, 리스트는 크기의 제약이 없이 데이터에 맞춰 공간을 자유자재로 늘려서 저장할 수 있다. 숫자를 넣는 것도 가능하고, 객체나 심지어 리스트를 넣는 것도 가능하다. 그래서 파이썬의 리스트를 마법의 사물함이라고 표현했다.

4 리스트와 반복문의 만남

앞서 리스트 a를 만들고 print()함수를 이용해서 요소들을 화면에 출력하였다. 리스트 a는 5개 요소를 가지고 있었으므로 print()함수를 다섯 번 사용하여 출력하였다. 하지만 이런 식으로 출력한다면 요소가 100개인 리스트를 출력해야 한다면 아래 코드와 같이 출력문을 100번 사용해야 한다.

print(a[0])
print(a[1])
print(a[2])
...
print(a[98])
print(a[99])

리스트가 이런 중복을 해결할 수 있다고 했었는데, 계속해서 출력문이 중복된다면 리스트를 배워야 하는 의미가 없는 것 아닐까? 이럴 때 반복문을 사용하면 된다. 리스트는 반복문과는 아주 찰떡궁합으로 잘 어울린다. 특히 리스트는 for반복문과 함께 사용하면 이런 중복 코드를 상당히 줄일 수 있다.

반복문과 인덱스를 사용해서 리스트 사용하기

우선 리스트 a를 화면출력하는 프로그램을 반복문을 이용해서 다시 작성해보자. 위 코드를 잘 살펴보면 print(a[index])코드에서 index부분만 0-4까지 또 0-99까지 바뀌는 패턴을 확인할 수 있다. 따라서 예전에 배웠던 for반복문과 range()함수를 사용하면 아래처럼 바꿀 수 있다.

>>> a = [2, 7, 5, 4, 3]
>>> for i in range(5):
...     print(a[i]),
... 
2 7 5 4 3

마찬가지로 100개 요소를 가진 리스트를 출력하는 코드는 아래와 같이 변경될 수 있다. 아래 코드에서와 같이 리스트 이름 또는 반복문에서 사용되는 변수이름은 임의로 정할 수 있다는 예로 bn을 사용하였다.

b = [2, 7, 5, 4, 3, ..., 15] # 100개의 요소를 가진 리스트 생성
for n in range(100):
    print(b[n])

위 코드에서는 요소 개수를 range()함수에 직접 전달하고 있다. 만약 개수를 모르는 경우에는 어떻게 해야 할까? 혹은 리스트에 데이터가 추가, 삭제되어서 개수가 변할 수 있다면 문제가 된다. 앞서 리스트 요소들의 개수를 알려주는 함수가 len()이라고 했었다. 이 함수를 이용하면 좀 더 일반화된 코드를 작성하는 것이 가능하다. 아래 예제를 살펴보자.

a = [2, 7, 5, 4, 3]
for i in range(len(a)):
    print(a[i])

b = [2, 7, 5, 4, 3, ..., 15] # 100개의 요소를 가진 리스트 생성
for n in range(len(b)):
    print(b[n])

@@박스처리 시작

일반화 일반화는 일부에서 전체로 적용범위를 넓히는 의미이다. 위의 예에서 최댓값에 따라 for문을 변경해야 하는 것보다 len()을 사용하면 최댓값이 달라져도 사용될 수 있게 되었으므로 일반화되었다고 말한다. @@박스처리 끝

이렇게 range()함수를 이용해서 번호를 생성하고, 이를 리스트 인덱스로 사용하였다. 이렇게 range()함수를 사용하면, 리스트의 각 요소 별로 작용할 수 있다. 짝수, 홀수 또는 5 배수에 해당되는 요소들만 출력하는 등 다양한 형태의 작업이 가능해진다. 아래 코드는 그 중 한 가지 예로 짝수 요소들을 먼저 출력하고 그 다음에는 홀수 요소들을 화면에 출력하는 프로그램을 보인다.

a = [2, 7, 5, 4, 3]

# 짝수 번째 요소 출력
for i in range(0, 5, 2):
    print(a[i])

# 홀수 번째 요소 출력
for i in range(1, 5, 2):
    print(a[i])

@@박스처리 시작

반복자(Iterator) 파이썬에는 리스트 말고도 비슷하게 여러 개의 데이터를 담을 수 있는 자료구조가 더 있다. 이 책에서는 리스트만 다루지만, 다른 파이썬 책들을 살펴보면 좀 더 정보를 얻을 수 있을 것이다. 그런데 이런 형태의 자료구조들을 다루는데 여러 개의 자료를 한 개씩 처음부터 끝까지 처리하려면 자료구조들마다의 특성 때문에 사용법이 달라질 수 밖에 없다. 그러다 보면 자료구조를 한 개씩 배울 때마다 사용법을 익혀야 한다는 문제가 있다. 예를 들어, 리스트는 순서가 있기 때문에 인덱스를 이용해서 데이터를 추출하는 것이 가능했다. 하지만, 만약 순서가 없는 자료구조라면 어떻게 해야 할까? 0, 1, 2 같은 인덱스로는 더 이상 어떤 자료를 의미하는 지 알 수 없다. 그럼 다른 형태로 값을 가져오는 것을 배워야 한다. 자료구조가 다양해지면 다양해질수록 배워야 할 양은 많아진다.

파이썬 같은 현대 언어에서는 이렇게 다양한 자료구조들을 제공하는 경우가 많은데, 이럴 때마다 모든 사용법들을 다 익히는 것이 어려우니까, 데이터를 추출해서 무엇인가 작업을 해야 하는 경우에 대해 좀 더 쉽게 각개별 데이터를 사용할 수 있도록 통일된 방법을 제공한다. 이를 반복자라고 하는데, 반복자를 사용하면 다양한 자료구조를 사용하면서 종류에 관계없이 동일한 방법으로 각각의 데이터를 추출할 수 있다. 반복자를 사용하는 방법은 for 문과 함께 아래에서 보인 것처럼 사용할 수 있다.

for 요소 in 자료구조:
  ...  # 각 요소 값을 가지고 있는 변수를 이용해서 작업을 함

@@박스처리 끝

반복문과 반복자를 사용해서 리스트 사용하기

만약 리스트에 있는 각 요소들에 대해서 반복적인 작업을 하고 싶을 때에는 range()함수를 사용하지 않고 처리하는 방법도 있다. 아래 코드를 파이썬 쉘에서 입력해보자.

>>> for i in [2, 7, 5, 4, 3]:
      print(i)

2
7
5
4
3

위 코드는 아래와 동일한 의미이다. in 뒤에 리스트를 나타내는 변수의 이름을 쓴 것뿐이다. 아래 내용을 파이썬 쉘에서 입력해보자.

>>> a = [2, 7, 5, 4, 3]
>>> for i in a:
      print(i),

2
7
5
4
3

파이썬 쉘이 아니라 Thonny의 텍스트 에디터에 넣어서 실행시켜도 마찬가지 결과가 나온다. 아래는 코드와 그 코드를 실행시켰을 때의 출력 화면의 내용이다.

# 12-01.py
for i in [2, 7, 5, 4, 3]:
  print(i)

# 12-02.py
a = [ 2, 7, 5, 4, 3 ]
for i in a:
  print(i)

코드를 실행시키면 위에서 보인 것처럼 range(5) 함수를 사용했을 때와 똑같은 결과가 나온다. 위에서 보인 for반복문은 i라는 변수가 리스트와 함께 쓰이면서 리스트에 있는 모든 요소에 대해서 각각의 요소들이 i라는 변수에 대입되고 for반복문에 있는 코드를 실행시킨다. 따라서 print()함수에서는 i라는 변수를 그대로 출력하면 되는 것이다. 또 이렇게 리스트를 바로 반복문에서 사용할 때의 장점은 리스트에 있는 요소의 개수를 생각할 필요가 없다는 점이다. 앞에서는 리스트의 개수를 len()함수를 이용해서 파악한 후에 range()함수를 사용하였지만, 여기서는 그럴 필요가 없다.

앞서 range()함수를 사용하면 짝수 번째나 홀수 번째 또는 특정 숫자의 배수에 해당되는 인덱스에 해당되는 요소만 출력하는 것도 가능하다고 했었다. 그렇다면 위에서 배운 것처럼 리스트를 직접 쓰게 되면 모든 요소에 대해서 처리한다고 했으니, range()함수 처럼 다양하게 다양한 요소에 대해 작업하는 것은 불가능한가? 아래 코드를 살펴보면 range()함수 없이도 짝수 번째와 홀수 번째 요소에 대해서 따로 처리하는 것을 보인다.

# 12-03.py
a = [2, 7, 5, 4, 3]

# 짝수 번째 요소 출력
count = 0
for i in a:
  if (count % 2) == 0:
    print(i)
  count += 1

# 12-04.py
a = [ 2, 7, 5, 4, 3 ]

# 홀수 번째 요소 출력
count = 0
for i in a:
  if (count % 2) == 1:
    print(i)
  count += 1

코드에서 보인 것처럼 count 변수를 이용해서 짝수와 홀수를 구별하고 그에 맞춰서 화면에 요소를 출력한다. 짝수 번째 요소들을 출력할 때에는 count를 0부터 시작해서 한 개 요소에 대해서 처리할 때마다 1씩 증가시킨다. 매 요소에 대해서 count 값을 2로 나눈 나머지 값이 0일 때 (즉 짝수 번째일 때) 리스트의 요소를 출력한다. 홀 수 번째 요소들에 대해서도 count를 다시 0으로 초기화 시킨 후에 마찬가지로 매 요소마다 count를 2로 나눈 나머지 값이 1일 때 리스트의 요소를 출력한다. 역시 매 요소에 대한 처리가 끝날 때마다 (출력을 하던 안 하던 처리는 된 것임) count 변수를 1씩 증가시킨다. 이렇게 range()함수를 사용하지 않고 리스트를 반복문에 바로 쓰면서도 짝수 번째, 홀수 번째 또는 특정 숫자의 배 수 등에 해당되는 요소에 대해서 처리하는 것이 가능하다. 다만 이러한 경우에는 range()함수를 사용하는 것이 더 효율적인 코드 작성에 유리하다.

@@박스처리 시작

효율성과 줄수로 본 좋은 프로그램이란?

프로그래밍은 정답이 없다. 프로그래밍을 실행하고 산출하는 결과는 동일하더라도, 푸는 과정이 다양할 수 있다는 의미다. 정상은 한 곳이지만 등산로는 정해져 있지 않다고 비유할 수 있다. 프로그래밍은 다양한 방법을 창의적으로 생각하면서 문제를 해결하는 과정이 이 중요하다. 그럼에도 불구하고 좋은 프로그램은 있다. 좋은 프로그램이란 평가기준에 비추어 결정되는 것이다. 빠른 등산로, 경치가 아름다운 등산로, 힘들지 않은 등산로와 같이 기준에 따라 평가가 다를 수 있다.

  • 프로그램은 효율성이 중요한 평가기준이다. 실행시간이 빠르거나, 메모리를 적게 사용하면 효율적이다. 불필요한 반복문이나 입출력을 사용하지 않아야 하는 이유이다.
  • 혹은 프로그램 줄수 (LOC, Lines Of Code)에 따라 좋은 프로그램인지 평가할 수 있다. 소스코드 중복이 반복되든지, 단축해도 될 명령문을 늘려 사용하거나 복잡하게 사용하지 않아야 한다.

따라서 프로그래밍을 할 때에는 다양한 각도에서 문제를 바라보고 해결하려고 노력하는 것이 중요하다. 또한 평가 요소가 무엇인지 확인하고 가능하면 효율적인 방법을 찾는 것이 필요하다. @@박스처리 끝

리스트 안에 리스트 넣기

이번에는 리스트 안에 리스트를 넣은 예제를 살펴보도록 하자. 11장에서 함정 영역을 사각형으로 표기하면서 꼭지점 한 개와 너비, 높이를 이용해서 사각형을 표현하거나 혹은 꼭지점 두 개를 이용해서 사각형을 표시하는 방법에 대해서 얘기했었다. 사각형을 리스트로 표현하면 [x1, y1, width, height] 혹은 [x1, y1, x2, y2] 형태로 표현할 수 있을 것이다. 그런데 사각형이 여러 개 있다면 어떻게 표현할까? 이럴 때 리스트 안에 리스트를 넣어서 사용하면 좋다. 아래 코드를 살펴보자.

# 사각형 2개를 리스트로 묶어 놓음

>>> rectangles = [[0, 0, 100, 100],  [100, 100, 200, 200]] 
>>> for rect in rectangles:
      print(rect)

  [0, 0, 100, 100]
  [100, 100, 200, 200]

여기서는 x1, y1, x2, y2로 구성된 사각형 리스트를 두 개 포함하는 리스트를 만들었다. 리스트의 요소를 출력해보면 또 다른 리스트임을 확인할 수 있다.

만약 사각형을 리스트 형태로 표현할 때, 좌표를 따로 리스트로 표현할 수도 있다. 아래 코드를 살펴보자.

>>> rectangle = [[0, 0], 100, 100]# 꼭지점 한 개와 너비 높이로 구성된 사각형 
>>> rectangle2 = [[0, 0], [100, 100]] # 꼭지점 두 개로 구성된 사각형
>>> for i in rectangle:
      print(i)      
  [0, 0]
  100
  100

>>> for i in rectangle2:
      print(i)      
  [0, 0]
  [100, 100]

여기서는 여러 가지 리스트를 포함하는 리스트를 구성해보았다. 요소가 리스트로만 구성되어 있는 rectangles나 rectangle2는 화면에 출력했을 때 리스트 형태로 보이고 있다. 리스트와 숫자가 섞여 있는 rectangle을 출력했을 때에는 리스트와 숫자가 있는 그대로 한 개씩 화면에 출력됨을 확인할 수 있다.

그럼 이번에는 요소들이 리스트로만 구성된 또 다른 예제를 한 번 살펴보자.

>>> a = [[3, 5], [7, 9], [3, 5], [4, 7]]
>>> for i in a:
      print(i)

  [3, 5]
  [7, 9]
  [3, 5]
  [4, 7]

이 프로그램을 실행시키면 예상할 수 있겠지만, 위에 있는 그림에서 보인 것처럼 각각의 리스트 요소들이 화면에 출력된다. 그런데 만약 a에 있는 리스트에 있는 요소들의 숫자 한 개씩 따로 떼어서 출력하겠다면 어떻게 해야 할까? 이미 각각의 요소에 해당되는 리스트를 화면에 출력하는 것은 보았으니, 각 요소에 대해서 다시 반복문을 사용해서 출력하면 된다. 아래 예제를 보자.

>>> a = [[3, 5], [7, 9], [3, 5], [4, 7]]
>>> for i in a:
      # i 는 요소 2개씩 묶여 있는 리스트임
      for n in i:
        print(n)
  3
  5
  7
  9
  3
  5
  4
  7

코드를 보면 반복문 안에 또 반복문을 사용하였다. i라는 변수는 a 리스트에 있는 요소인 또 다른 리스트를 값으로 가지고 있는 변수이므로 하단에 있는 반복문을 이용해서 다시 i리스트의 요소들 각각을 추출하고 이를 화면에 출력한다. 프로그램을 실행하면 예상할 수 있는 것처럼 숫자 한 개씩만 출력하는 아래 보인 그림과 같은 결과를 얻을 수 있다.

그렇다면 이렇게 반복문을 중첩해서 써야만 리스트 안에 있는 리스트의 요소를 사용할 수 있는 것일까? 꼭 그렇지만은 않다. 앞서 우리는 리스트에 있는 요소들에 접근하기 위해서 []를 사용할 수 있다고 했었다. 따라서 위에 있는 코드에서 보면 a[0][3, 5]를 나타내고, a[1][7, 9]라는 리스트를 값으로 가지고 있다. a[0]이나 a[1]이 리스트이므로 만약 우리가 이러한 값들을 다른 이름의 변수에 넣는다면 어떻게 될까? 만약 위에 있는 코드에 추가해서 아래와 같은 내용을 추가한다면 어떻게 될까?

>>> b = a[0]
>>> c = a[1]
>>> print(b)
  [3, 5]
>>> print(c)
  [7, 9]

b라는 변수는 a[0]에 해당되는 값 즉 리스트 [3, 5]를 값으로 가진다. 또한 c라는 변수는 a[1]에 해당되는 리스트 즉 [7, 9]를 값으로 가지게 된다. 따라서 우리가 b 변수의 리스트에서 첫 번째 요소를 사용하고 싶다면 b[0]이라고 쓰면 되고 두 번째 요소에 접근하고 싶다면 b[1]이라고 쓰면 된다. 마찬가지로 c변수에서 7이라는 값을 추출하고 싶으면 c[0]을 9라는 값은 c[1]이라고 쓰면 된다. 그럼 이제 다시 bc의 요소들을 한 개씩 화면에 출력해보자.

>>> a = [ [3, 5], [7, 9], [3, 5], [4, 7] ]
>>> b = a[0]
>>> c = a[1]
>>> print(b[0])
  3
>>> print(b[1])
  5
>>> print(c[0])
  7
>>> print(c[1])
  9

위에 있는 코드를 쉘에서 실행시키면 위에 있는 그림에서 보인 것처럼 3, 5, 7, 9 숫자들을 한 개씩 출력하는 것을 확인할 수 있다. 여기서 보면 b라는 변수는 a[0]을 대신하기 위해서 쓰고 있고, c라는 변수는 a[1]을 대신하고 있음을 확인할 수 있다. 그렇다면 b대신 a[0]c대신 a[1]을 직접 사용하는 것은 안될까?

>>> print(a[0][0])
  3
>>> print(a[0][1])
  5
>>> print(a[1][0])
  7
>>> print(a[1][1])
  9

위에 있는 코드를 실행해보면 bc 변수를 사용할 때와 동일한 결과를 출력함을 볼 수 있다. 이렇게 만약 리스트 안에 리스트가 있고, 리스트 안에 있는 리스트의 요소들의 값을 사용하고 싶거나 요소들에 접근해야 하는 경우가 있다면 다른 변수에 값을 넣은 후에 사용하는 방법도 있고 직접 [][]처럼 []를 두 번 이상 붙여서 사용하는 것도 가능하다. 좀 더 복잡한 경우를 고려해서 리스트의 리스트의 리스트가 있는 경우에는 [][][]을 이용해서 접근이 가능할 것이다. 아래 예제를 통해 확인해보자.

# 12-09.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> print(a[0][0][0])    # 2를 출력
  2
>>> print(a[0][0][1])    # 4를 출력
  4
>>> print(a[0][1][0])    # 3을 출력
  3
>>> print(a[0][1][1])    # 5를 출력
  5
>>> print(a[1][0])         # 9를 출력
  9
>>> print(a[1][1])        # 10을 출력
  10
>>> print(a[2])            # 7을 출력
  7

만약 위에서 작성한 코드가 너무 복잡해 보인다면 다른 변수들을 써서 좀 더 이해하기 쉽게 만드는 것도 가능하다. 아래 코드처럼 바꿔보자. 출력화면의 결과는 12-09.py 프로그램과 같다.

# 12-10.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> b = a[0]            # b = [[2,4], [3,5]] 리스트
>>> c = a[1]            # c = [9, 10]
>>> d = a[2]            # d = 7
>>> print(b[0][0])    # 2를 출력
  2
>>> print(b[0][1])    # 4를 출력
  4
>>> print(b[1][0])    # 3을 출력
  3
>>> print(b[1][1])    # 5를 출력
  5
>>> print(c[0])     # 9를 출력
  9
>>> print(c[1])        # 10을 출력
  10
>>> print(d)        # 7을 출력
  7

[]가 여러 개 붙어서 복잡하게 보일 수 있는 코드를 다른 변수들을 사용해서 미리 배정해둠으로써 코드를 보는 사람이 좀 더 이해하기 쉬운 코드를 작성할 수 있다.

5 리스트 생성 및 기타 연산

5절에서는 리스트를 새롭게 만들어내는 세 가지 방법과 더불어 다양한 리스트 데이터에 대한 연산을 포함하고 있다. 내용도 많고 지금 모두 다 이해할 필요는 없다. 처음에 리스트를 만들어내는 부분과 데이터를 추가하는 부분만 보고 나머지는 필요할 때 찾아본다고 생각하면 된다.

리스트 만들기

파이썬 프로그래밍 언어에서는 다양한 방법으로 리스트를 생성할 수 있다. 아래 표는 세 가지 방법을 설명하고 있다.

리스트 생성 방법 설명 예제 코드
변수이름 = [ 값1, ..., 값n ] 여태까지 우리가 사용한 방법으로 리스트에 속하는 요소들을 값들을 나열함 x = [2, 3, 5]
변수이름 = [] 요소가 없는 빈 리스트를 생성함 x = []
변수이름 = list() 요소가 없는 빈 리스트를 생성함. []와 같음 x = list()

첫 번째로 본 방법은 '['와 ']' 사이에 직접 값을 넣어서 생성하는 것이다. 일반적으로는 리스트에 넣을 값들이 정해져 있을 때 사용 가능한 방법이고 우리가 여태까지 리스트를 만들었던 방법이기도 하다. 이렇게 리스트를 만들 때 값에는 숫자나 문자열 등이 들어갈 수도 있고, 객체 또는 다른 리스트, 심지어는 다른 값을 가지고 있는 변수를 넣는 것도 가능하다. 예를 들어 아래 코드를 살펴보자.

# 12-11.py
>>> a = [2, "abc", "a", 3 ]
>>> b = [ [2, 3], "abc", 3, 5 ]
>>> c = [a, b, 7, [9, 10] ]
>>> print(c)
  [[2, 'abc', 'a', 3], [[2, 3], 'abc', 3, 5], 7, [9, 10]]

위에 있는 코드에서는 다양한 종류의 값을 가지는 리스트를 만들었다. 예를 들어, 숫자와 문자열을 함께 포함하는 리스트인 a를 생성하였다. 그리고 또 다른 리스트를 포함하는 리스트인 b도 만들었다. 마지막으로 리스트 c는 변수 a와 b를 포함하고 숫자 7, 또 다른 리스트인 [9, 10]을 요소로 가지고 있다. 즉 리스트 c는 모두 4개의 요소로 구성된 리스트이며, 첫 번째 요소로는 a에 해당되는 리스트, 두 번째 요소로는 변수 b가 나타내는 리스트, 세 번째 요소는 숫자 7, 네 번째 요소는 [9, 10]에 해당되는 리스트이다. 따라서 리스트 c를 화면에 출력하면 위에서 보인 것처럼 [ [2, "abc", "a", 3], [ [2, 3], "abc", 3, 5 ], 7, [9, 10] ] 이 나타날 것이다. 이렇게 리스트를 생성할 때 변수가 리스트의 요소로 들어가면 그 변수의 값이 실제적으로 리스트에 포함됨을 기억해두자.

두 번째 방법과 세 번째 방법은 요소를 포함하지 않은 빈 리스트를 만드는 방법들이다. '['와 ']' 사이에 아무런 값을 넣지 않음으로써 빈 리스트를 만드는 것이 가능하다. 또 파이썬에서 제공하는 리스트 생성함수 (list())를 사용해서 빈 리스트를 만드는 것도 가능하다.

리스트에 데이터 추가

그런데 아무런 값 즉 요소를 가지고 있지 않는 리스트는 만들어서 무엇을 할까? 텅 빈 리스트는 만들 필요조차도 없는 것 아닐까? 파이썬의 리스트에는 요소를 추가하거나 삭제할 수 있는 기능이 제공된다. 아래 코드를 보자.

>>> # 12-12.py
>>> x = list()    # 빈 리스트를 생성하고 x라고 이름 붙임
>>> x.append(1)    # x의 요소로 숫자 1을 추가
>>> x.append(3)
>>> print(x)    # 화면에 [1, 3]이 출력됨
  [1, 3]
  • x라는 이름의 리스트를 생성했다.
  • 리스트의 append()함수는 매개 변수로 전달된 값을 이미 생성된 리스트의 가장 마지막 요소로 추가한다. 따라서 여기서는 1이라는 숫자 값을 비어있던 리스트에 추가한다.
  • x 리스트의 마지막 요소로 3이라는 숫자 값을 추가한다.
  • x라는 이름의 리스트를 화면에 출력한다.

리스트의 append함수는 숫자, 문자열, 객체, 혹은 리스트 등 어떤 값이라도 리스트의 마지막 요소로 추가한다. 그리고 리스트에는 다른 종류의 값들이 섞여 있을 수 있다. 따라서 위에 있는 코드에 이어서 아래 코드를 추가로 실행시키면 또 다른 형태의 리스트를 화면에 출력할 것이다.

>>> x.append("문자열")
>>> x.append([7, "python"])
>>> print(x)
  [1, 3, '문자열', [7, 'python']]
  • x 리스트의 마지막 요소로 "문자열"을 추가한다.
  • x 리스트의 마지막 요소로 리스트 [ 7, "python" ]을 추가한다.
  • 화면에 x를 출력하면 위에서 보인 것처럼 [1, 3, "문자열", [7, "python"]]이 출력된다.

새롭게 생성된 리스트에 새로운 데이터 즉 숫자, 문자열, 객체, 리스트 등을 추가하는 것을 보았다. 그런데 새롭게 생성된 리스트가 아니라 처음부터 값이 주어진 리스트에 새로운 데이터를 추가할 수 있을까? 아래 코드를 살펴보자.

>>> # 12-13.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> a.append(5)
>>> a.append(7)
>>> print(a)
  [[[2, 4], [3, 5]], [9, 10], 7, 5, 7]
  • 여기서는 a라는 이름의 리스트를 생성한다. 값은 [[2, 4], [3, 5]] 리스트, [9, 10] 리스트, 7이다.
  • 5와 7이라는 숫자 값들을 새로운 값으로 추가한다.
  • a 리스트를 출력한다. 화면은 위에서 보인 것처럼 원래 있던 값 세 개와 5라는 숫자 값이 출력된다.

이렇게 보면, 처음에 생성될 때 값이 주어진 리스트도 append() 함수를 이용해서 값을 추가할 수 있음을 보인다. 만약 기존에 있던 리스트에 새로운 리스트 값을 추가한다면 어떻게 될까? 아래 코드를 살펴보자.

>>> # 12-14.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> a.append([8])
>>> print(a)
  [[[2, 4], [3, 5]], [9, 10], 7, [8]]
  • a라는 변수는 위에서 정의한 것과 같은 리스트이다.
  • 기존에 있던 리스트에 [8]을 추가한다. 숫자 8이 아니라 숫자 값 8을 포함하는 리스트임에 주의하자.
  • 화면에 출력하면 [[2, 4], [3, 5]], [9, 10], 7, [8]에 해당되는 리스트가 화면에 출력된다.

리스트에 데이터 삽입

리스트에 새로운 데이터를 추가하는 방법을 살펴보았는데 append()함수의 문제는 항상 리스트의 끝에 데이터를 추가한다는 점이다. 만약 중간에 데이터를 넣고 싶다면 어떻게 해야 할까? 역시 아래 코드를 먼저 살펴보자.

>>> # 12-15.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> a.insert(2, [8])
>>> print(a)
  [[[2, 4], [3, 5]], [9, 10], [8], 7]

위에 있는 코드에 대한 설명은 다음과 같다.

  • a라는 리스트를 생성한다.
  • 인덱스 번호로 2 즉 처음부터 세 번째 자리에 리스트 [8]을 넣는다. 기존의 세 번째 자리에 있던 요소들은 한 칸씩 뒤로 밀린다.
  • 화면에 리스트 a의 내용을 출력한다.

파이썬의 리스트에서 제공하는 insert()함수는 두 개의 인자를 받는다. 첫 번째는 리스트의 몇 번째 위치에 데이터를 넣을 것인지를 나타내고, 두 번째 인자는 실제 삽입될 데이터를 나타낸다. 리스트의 위치 정보는 0부터 시작하므로, insert(n, data)로 사용할 때 매개변수로 전달되는 n은 실제로는 n + 1번째 위치임을 주의해야 한다. 데이터를 새로 삽입하게 되면 원래 그 자리에 있던 데이터는 한 칸씩 뒤로 밀려서 자리를 차지하게 된다.

append()insert()함수는 리스트를 확장시키는 용도로 쓰이기는 하지만, 데이터를 리스트의 마지막에 추가할 것이냐 아니면 어느 곳이라도 데이터를 넣을 수 있도록 할 것이냐의 차이가 있다고 보면 된다.

기존 리스트 요소의 값 변경

그렇다면 데이터를 새로 삽입하는 것이 아니라, 기존에 있던 리스트의 데이터를 바꾸고 싶다면 어떻게 해야 할까? 아래 코드를 살펴보자.

>>> # 12-16.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> a[2] = [8]
>>> print(a)
  [[[2, 4], [3, 5]], [9, 10], [8]]
  • a는 모두 3 개의 요소를 갖추고 있는 리스트이다.
  • a 리스트의 (2 + 1) 번째 즉 세 번째 자리에 있는 요소를 대입 연산자의 오른쪽에 있는 값으로 대체하라는 의미이다. 즉 세 번째 요소인 숫자 7을 제거하고 그 자리에 리스트 [8]을 넣으라는 의미가 된다.
  • a리스트를 화면에 출력한다.

따라서 기존에 있는 데이터를 대체하고 싶다면 바로 [] 연산자를 활용해서 데이터를 넣으면 된다.

리스트에서 데이터 제거

만약 리스트에 있는 데이터를 삭제하고 싶다면 어떻게 해야 할까? 리스트에 있는 데이터를 삭제하는 방법은 두 가지가 있다. 첫 번째는 리스트에 있는 자체 기능을 사용하는 것이다. 아래 코드를 살펴보자.

>>> a = [ 2, 7, 3, 5, 4, 3, 1 ]
>>> a.remove(3)
>>> print(a)
  [2, 7, 5, 4, 3, 1]

코드에 대한 설명은 아래와 같다.

  • a라는 리스트 생성. 리스트에는 같은 값이 중복될 수도 있어서, a에는 3이라는 숫자 값이 두 번 들어있다.
  • 리스트에 포함되어 있는 remove()함수는 매개 변수로 주어진 데이터를 리스트에서 삭제한다. 단 중복되는 데이터가 두 개 이상 있을 때에는 첫 번째 요소만 삭제한다.
  • 중복되는 3이라는 숫자 값을 삭제하라고 하였기 때문에 세 번짜 위치에 있는 3은 삭제된다. print() 명령을 이용해서 a를 출력할 때 나타나는 리스트의 값들은 위에서 보인 것처럼 실행결과처럼 [ 2, 7, 5, 4, 3, 1 ]이 된다.

만약 위에 있는 코드와 더불어 아래 코드가 추가로 실행된다면 어떻게 될까?

>>> a.remove(3)
>>> print(a)
  [2, 7, 5, 4, 1]

이렇게 되면 첫 번째 3이 사라진 뒤에 남아 있는 리스트의 요소 중에서 3을 찾아서 제거한다. 따라서 남은 3을 마저 없애고 화면에 출력되는 내용은 [ 2, 7, 5, 4, 1 ] 이 된다.

하지만 이렇게 사용되는 remove()함수는 지우고자 하는 특정 데이터의 값을 알아야만 사용 가능하다는 단점이 있다. 만약 어떤 값인지는 모르겠는데 특정 위치에 있는 데이터를 삭제하고 싶다면 어떻게 해야 할까? 이 때에는 리스트에 있는 데이터를 제거하는 또 다른 방법인 파이썬에 내장된 함수인 del()을 사용하는 것이다. 파이썬의 del() 함수는 리스트에 속해 있는 기능이 아니기 때문에 직접적으로 어떤 배열의 몇 번째 데이터를 삭제할 것인지를 알려줘야 한다. 아래 코드를 살펴보자.

>>> # 12-18.py
>>> a = [2, 7, 3, 5, 4, 3, 1]
>>> del(a[3]) 
>>> print(a)
  [2, 7, 3, 4, 3, 1]

위에 있는 코드에서의 del() 함수는 a리스트의 네 번째 위치에 있는 데이터를 삭제하라는 의미이다. 따라서 5가 제거되고 화면에는 [2, 7, 3, 4, 3, 1]이 출력된다.

그렇다면 위에서 보인 것처럼 단순한 숫자 값이 아닌 데이터들은 어떻게 삭제할까? 특별히 다른 방법은 없다. 아래 코드를 보면서 확인해보자.

# 12-19.py
>>> a = [[[2, 4], [3, 5]], [9, 10], 7]
>>> del(a[0])
>>> print(a)        # (1)
  [[9, 10], 7]
>>> a.remove([9, 10])
>>> print(a)        # (2)
  [7]

위에 있는 코드에서 보면 del() 함수에서는 a리스트의 첫 번째 요소를 삭제한다. 그리고 화면에 출력하면 위에서 보인 것처럼 [[9, 10], 7]만 남게 된다. 그 다음 호출되는 remove()함수에서는 리스트 [9, 10]을 찾아서 삭제하라고 하기 때문에 결국 최종적으로 화면에 출력되는 값은 [7]이 된다. 이렇게 del()함수와 리스트의 remove()함수는 섞어서 사용하는 것도 가능하다.

6 미로게임 수정 (함정이 1개이며 리스트를 활용하는 경우)

11장에서 다룬 미로 게임은 함정을 한 곳에만 가지고 있었다. 만약 여기에 여러 개의 함정을 두고 싶다면 어떻게 만들까? 우선 함정을 여러 개 관리해야 하므로 우리가 여기서 배웠던 리스트를 떠올릴 수 있을 것이다. "그래! 리스트는 어떤 데이터도 담을 수 있는 것처럼 말했는데, 그럼 함정 자체를 리스트의 요소로 넣으면 되겠네!" 이렇게 생각할 수 있지 않을까? 그리고 이미 앞에서 우리는 사각형을 리스트로 표현하는 방법에 대해서 얘기했었다. 한 가지 방법은 위에 있는 그림처럼 리스트에 처음 4개의 요소는 첫 번째 함정, 그 다음 4개의 숫자는 두 번째 함정 형태로 표기하는 것이다. 그런데 이렇게 묶기에는 아무래도 각각의 사각형을 구별하기가 쉽지 않다. 그래서 여기서는 두 개의 꼭지점을 나타내는 x1, y1, x2, y2를 한 개의 리스트로 묶는다. 그리고 여러 개의 함정을 만들어야 한다면, 함정 리스트를 여러 개 포함하는 또 다른 리스트를 만들기로 한다.

아래는 11장에서 사용했던 미로 게임의 코드에서 함정은 4개의 변수로 표현하지 않고 리스트 형태로 표현한 것을 보인다.

# mazeWithATrap.py
# 함정의 위치
trap = [ 109, -239, 188, -298 ]
# 혹은 아래처럼 작성하는 것도 가능함
# x1 = 109
# y1 = -239
# x2 = 188
# y2 = -298
# trap = [ x1, y1, x2, y2 ] 

# v1과 v2 중에서 작은 값을 반환
def getMinValue(v1, v2):
    if v1 < v2:
        return v1
    return v2

# v1과 v2 중에서 큰 값을 반환
def getMaxValue(v1, v2):
    if v1 > v2:
        return v1
    return v2

def moveTo(x, y):
    t1.penup()
    t1.goto(x, y)
    t1.pendown()

# 거북이의 위치가 x, y이고 함정의 위치가 x1, y1, x2, y2일때 min, max를 구해서 함정에 들어갔는 지 확인
def isInTrap(x, y, trap):
    minX = getMinValue(trap[0], trap[2])    # x1, x2
    maxX = getMaxValue(trap[0], trap[2])    # x1, x2
    minY = getMinValue(trap[1], trap[3])    # y1, y2
    maxY = getMaxValue(trap[1], trap[3])    # y1, y2
    if x >= minX and x <= maxX and y >= minY and y <= maxY:
        return True
    else:
        return False

def showTrapInRed(trap):
    t1.fillcolor("red")
    t1.begin_fill()
    moveTo(trap[0], trap[1])    # t1.goto(x1, y1)
    t1.goto(trap[2], trap[1]) # t1.goto(x2, y1)
    t1.goto(trap[2], trap[3])    # t1.goto(x2, y2)
    t1.goto(trap[0], trap[3])    # t1.goto(x1, y2)
    t1.goto(trap[0], trap[1])    # t1.goto(x1, y1)
    t1.end_fill()
    moveTo(trap[2] + 10, trap[3] + 10) # 함정 밖으로 거북이를 이동시킴

def keyeast():
    position = t1.pos()
    t1.goto(position[0] + 10, position[1])
    if isInTrap(position[0] + 10, position[1], trap):
        showTrapInRed(trap)

def keywest():
    position = t1.pos()
    t1.goto(position[0] - 10, position[1])
    if isInTrap(position[0] - 10, position[1], trap):
        showTrapInRed(trap)

def keysouth():
    position = t1.pos()
    t1.goto(position[0], position[1] - 10)
    if isInTrap(position[0], position[1] - 10, trap):
        showTrapInRed(trap)

def keynorth():
    position = t1.pos()
    t1.goto(position[0], position[1] + 10)
    if isInTrap(position[0], position[1] + 10, trap):
        showTrapInRed(trap)

import turtle

win = turtle.Screen()
t1 = turtle.Turtle()

win.bgpic("12.maze.gif")

# 이벤트 처리 함수 등록
win.onkey(keyeast, 'Right')   # 동
win.onkey(keywest, 'Left')    # 서
win.onkey(keysouth, 'Down')   # 남
win.onkey(keynorth, 'Up')     # 북

win.listen()
turtle.mainloop()

  • 코드를 살펴보면 대표적으로 바뀐 함수들이 isInTrap(x, y, trap)showTrapInRed(trap) 함수들이다. 좌표를 나타내던 4개의 숫자 값 (x1, y1, x2, y2)을 전달하는 대신 리스트를 한 개 전달하는 형태로 수정되었다.
  • 두 개 함수가 바뀌면서 두 함수를 호출하는 코드 역시 모두 함정을 나타내는 리스트를 전달하도록 수정되었다.
  • isInTrap() 함수와 showTrapInRed()함수는 trap이라는 리스트를 사용한다.
  • 함정의 위치는 두 개의 점에 대한 좌표를 한 개의 리스트로 묶는 형태로 구현하였다. 즉 한 개의 리스트에 사각형을 표현하는데 사용된 두 개 점의 위치가 모두 들어있다.
  • 함정을 나타내는 trap변수를 [x1, y1, x2, y2] 형태로 표현하여 [109, -239, 188, -298]로 값을 지정하였다.

7 미로게임 수정 (함정이 여러 개이고 리스트를 활용하는 경우)

이번에는 함정이 여러 개 있는 경우를 가정해서 프로그램을 작성해보자. 함정의 위치는 아래 표와 그림에서 보인 것과 같다고 가정하자.

두 점의 이미지 좌표 거북이 프로그램에서의 좌표 파이썬의 리스트 형태로 표기
(63,68), (123,121) (-242.5, 237.5), (-182.5, 184.5) [-242.5, 237.5, -182.5, 184.5]
(126,304), (184,379) (-179.5, 1.5), (-121.5, -73.5) [-179.5, 1.5, -121.5, -73.5]
(182,487), (243,547) (-123.5, -181.5), (-62.5, -241.5) [-123.5, -181.5, -62.5, -241.5]
(302,306), (369,366) (-3.5, -0.5), (63.5, -60.5) [-3.5, -0.5, 63.5, -60.5]
(484,242), (550,309) (178.5, 63.5), (244.5, -3.5) [178.5, 63.5, 244.5, -3.5]

프로그램이 동작하는 방법은 사용자가 거북이 로봇을 움직여서 여러 개 있는 함정 중에 한 곳에 빠지게 되면 해당 함정을 표시한다. 단 이때 다른 함정들의 위치는 보여주지 않는다. 계속해서 움직이다가 함정에 빠지게 될 때마다 그 함정을 빨강색 사각형으로 표시한다. 만약 이미 빠져서 빨강색으로 표시된 함정에 다시 들어갈 때에는 반복적으로 사각형을 그리는 것으로 한다.

함정의 위치가 여러 개 만들어지면서 다시 리스트를 구성해야 하므로 좀 복잡해진다. 코드에서는 traps라는 변수를 만들어서 함정들의 리스트에 이름을 붙인다. 먼저 코드를 살펴보자.

# mazeWithTraps.py
# 1
traps = [
    [-242.5, 237.5, -182.5, 184.5], 
    [-179.5, 1.5, -121.5, -73.5],
    [-123.5, -181.5, -62.5, -241.5],
    [-3.5, -0.5, 63.5, -60.5], 
    [178.5, 63.5, 244.5, -3.5]
    ]

def moveTo(x, y):
    # mazeWithATrap.py 의 moveTo와 동일함    

# v1과 v2 중에서 작은 값을 반환
def getMinValue(v1, v2):
    # mazeWithATrap.py 의 getMinValue와 동일함

# v1과 v2 중에서 큰 값을 반환
def getMaxValue(v1, v2):
    # mazeWithATrap.py 의 getMaxValue와 동일함

def showTrapInRed(trap):
    # mazeWithATrap.py 의 showTrapInRed와 동일함

# 거북이의 위치가 x, y이고 함정의 위치가 x1, y1, x2, y2일때 min, max를 구해서 함정에 들어갔는 지 확인
def isInTrap(x, y, trap):
    # mazeWithATrap.py 의 isInTrap과 동일함

# 2
# 리스트로 만들어져 있는 함정들 중 한 개에 거북이가 들어가는지 확인. 거북이의 위치가 x, y이고 함정의 위치가 traps라는 리스트에 있다고 가정할 때, isInTrap함수를 각 함정에 대해서 호출하며 함정에 들어갔는지 확인
def isInTraps(x, y, traps):
    for trap in traps:
        if isInTrap(x, y, trap):
            return trap
    return None



# 3
# isInTraps함수를 호출해서 거북이 로봇이 함정에 빠지는 지 확인. 만약 함정에 빠졌다면 해당 함정을 isInTraps함수가 반환함. 반환하는 값을 trap이라는 변수에 받아서 실제 함정인지 아니면 None인지 확인. 함정이면 빨간색 사각형으로 함정을 표시. 
def keyeast():
    position = t1.pos()
    t1.goto(position[0] + 10, position[1])
    trap = isInTraps(position[0] + 10, position[1], traps)
    if trap != None:
        showTrapInRed(trap)

def keywest():
    position = t1.pos()
    t1.goto(position[0] - 10, position[1])
    trap = isInTraps(position[0] - 10, position[1], traps)
    if trap != None:
        showTrapInRed(trap)

def keysouth():
    position = t1.pos()
    t1.goto(position[0], position[1] - 10)
    trap = isInTraps(position[0], position[1] - 10, traps)
    if trap != None:
        showTrapInRed(trap)

def keynorth():
    position = t1.pos()
    t1.goto(position[0], position[1] + 10)
    trap = isInTraps(position[0], position[1] + 10, traps)
    if trap != None:
        showTrapInRed(trap)

import turtle

win = turtle.Screen()
t1 = turtle.Turtle()

win.bgpic("12.maze.gif")

# 이벤트 처리 함수 등록
win.onkey(keyeast, 'Right')   # 동
win.onkey(keywest, 'Left')    # 서
win.onkey(keysouth, 'Down')   # 남
win.onkey(keynorth, 'Up')     # 북

win.listen()
turtle.mainloop()

기존 코드에서 바뀌는 부분을 주석문으로 1, 2, 3이라고 표시해두었다. 각각 바뀐 부분에 대해서 아래에서 설명한다.

  1. 여러 개의 함정이 있는 만큼 관리를 목적으로 리스트로 구성하였다. 모두 5개의 함정에 대해 리스트의 요소로 추가하고 이름을 traps라고 붙였다.
  2. isInTraps() 함수는 새롭게 추가된 코드이다. 리스트로 주어지는 함정들 중 한 개에 거북이 로봇이 들어가는 지 확인하고 만약 함정에 빠지는 경우에는 해당 함정에 대한 리스트를 반환한다. 기존에는 한 개의 함정만 있었으므로 해당 함정에 대해서만 확인하면 되었지만, 새롭게 만들어지는 프로그램에서는 여러 개의 함정이 리스트 형태로 존재하므로, 리스트의 요소들인 각각의 함정에 대해서 기존에 있던 isInTrap()함수를 호출하여 거북이 로봇이 들어갔는 지 확인한다. 만약 함정에 빠진 것이 중간에 발견된다면 나머지 함정에 대해서는 더 이상 확인할 필요 없이 해당 함정을 나타내는 리스트를 반환하고 함수를 빠져나간다. 만약 반복문을 모두 실행시켜도 반환되지 않았다면, 거북이 로봇은 함정에 빠지지 않았다는 의미이므로 파이썬의 None 객체를 반환한다.

showTrapInRed() 함수의 가장 마지막 코드에는 거북이를 함정 바깥쪽 (정확히 말하면 오른쪽 하단 꼭지점에서 (10, 10) 만큼 이동한 위치)으로 거북이를 이동시키는 코드가 있다. 거북이가 함정에 빠지면 같은 거북이로 하여금 함정 영역을 그리도록 하고 있다. 그러다 보니 거북이의 위치가 다시 함정 영역의 상단 위쪽 꼭지점에 마지막으로 위치하게 된다. 거기서 조금만 잘못 움직이면 다시 함정에 빠지게 되고, 거북이는 또 다시 그 자리로 돌아가게 된다. 그래서 마치 늪에 빠진 것처럼 함정에서 빠져 나오지 못하는 것처럼 보일 수 있다. 그래서 여기서는 일단 거북이가 함정에 빠지면 오른쪽 하단 꼭지점에서 약간 오른쪽 위((10, 10) 정도 위)로 옮겨놓는다. 이렇게 되면 거북이는 일단 함정 영역에서 벗어나기 때문에 자유롭게 움직일 수 있게 된다. 이 방법도 약간의 문제가 있지만, 그 문제를 해결하는 것은 연습문제로 남겨둔다.

  1. 키보드 이벤트를 처리하는 keyeast(), keywest(), keysouth(), keynorth() 함수들에서는 기존에 거북이 로봇을 이동시키는 코드를 그대로 사용한다. 다만 거북이 로봇이 움직일 때 마다 예전처럼 함정 한 개에 대해서만 확인하는 것이 아니라 여러 개의 함정을 모두 확인해야 하므로, isInTraps() 함수를 호출하여 함정에 빠지는 지를 확인한다. isInTraps() 함수의 결과물을 trap이라는 변수에 받고, 그 결과를 조사해서 None 객체가 아니라면 함정에 빠졌음을 의미하므로, 빨간색 사각형으로 함정을 그려서 화면에 나타낸다.

이렇게 세 가지 부분에 대해서만 주의하면 나머지 코드는 그대로 사용할 수 있다. 함정이 여러 개 일 때 리스트를 활용하였고, 그 부분에 대해서 코드가 바뀐 부분을 주의해서 살펴보면 코드를 이해하는데 큰 어려움이 없을 것으로 보인다. 마지막으로 파이썬에는 None이라는 객체가 존재하는데 아래 설명을 보면 될 것이다.

@@박스처리

파이썬의 None 객체는 아무것도 없음을 의미하는 객체라고 생각하면 된다. 리스트 역시 객체 중 한 개이므로, None을 반환하면 내용이 없는 리스트라는 의미로 여기서는 사용된다. @@박스처리

아래 그림은 실제 거북이 로봇을 이동시키면서 함정에 빠졌는지 확인한 것을 보인다.

연습문제

  1. 11장에 있는 mazeWithATrap.py, 12장에 있는 mazeWithATrap.py, mazeWithTraps.py 게임 모두 비슷한 문제들을 가지고 있다. 일단 거북이가 함정에 빠지면 거북이가 어떤 쪽에서 함정에 빠지던 항상 함정의 오른쪽으로 이동시킨다. 하지만 차라리 함정에 빠지게 된 쪽의 반대편으로 옮겨놓으면 오히려 함정에 빠졌다고 하더라도 가던 길을 계속 갈 수 있지 않을까?

위에 있는 그림을 보면 거북이는 동쪽에서 서쪽으로 이동 중이었다가 함정에 빠지게 된다. 그렇다면 거북이를 다시 동쪽으로 보내면 결국 거북이는 함정의 서쪽은 가는 것이 어렵게 된다. 따라서 동쪽에서 서쪽으로 이동 중이었다면 함정에 빠졌을 때 거북이를 함정의 서쪽 밖으로 보내면 더 좋을 것이다. 반대로 서쪽에서 동쪽으로 이동 중이었다면 동쪽 밖으로 보내는 것이 나을 것이다. 이렇게 동서남북에 대해서 모두 처리해주면, 미로 게임에서 함정에 빠지더라도 김을 찾는 것이 수월하지 않을까?

이번 문제에서는 이러한 기능을 미로 게임 코드에 추가한다.

  1. 1~1000 사이에 있는 정수 중에서 4로 나누어지고 5로 나누어지지 않는 수를 골라서 리스트에 넣는 작업을 한다. 1000까지의 숫자를 모두 살펴보았다면, 리스트에 있는 모든 수를 출력하는 프로그램을 작성하라. 단 리스트의 숫자는 한 줄에 한 개씩 출력하도록 한다.

  2. 미로 게임 코드(mazeWithATrap.py나 mazeWithTraps.py)를 수정해서 거북이가 이동한 위치들을 리스트에 담았다가 화면에 출력하는 프로그램을 작성하라.

  3. 삼각형 3개의 정보를 리스트에 넣고, 그 정보를 이용해서 화면에 삼각형을 그리는 프로그램을 작성하라.

  4. 미로 게임 코드(mazeWithATrap.py나 mazeWithTraps.py)를 수정해서 거북이의 이동 속도를 6으로 지정한다. 그리고 거북이가 이동한 위치들을 리스트에 담았다가, 'r' 또는 'R' 키를 누르면 거북이가 다녔던 길을 모두 지우고, 다시 재현(replay) 하는 프로그램을 작성하라. 이때 마치 스포츠 경기에서 중요한 장면을 슬로우 모션으로 다시 보여주듯이, 거북이의 이동 경로를 재현할 때에는 거북이의 속도를 3으로 맞춘다.

  5. 파이썬의 문자열은 문자로만 구성된 리스트이다. 사용자로부터 문자열을 한 개 입력 받고, 글자 수를 세서 화면에 출력하는 프로그램을 작성하라.

  6. 사용자로부터 영문 문자열을 입력 받고, 그 안에 있는 알파벳 글자 별 개수를 세서 출력하는 프로그램을 작성하라. 예) Hello가 입력되었을 때 아래에서 보인 것처럼 출력됨. 단 대문자와 소문자를 따로 구별하지 않는다. h: 1 e: 1 l: 2 o: 1

results matching ""

    No results matching ""