기본 자료형: 리스트

안내: Think Python 10장 내용의 일부를 번역 및 요약수정하여 정리한 내용입니다.

파이썬 프로그래밍언어는 정수, 실수, 진리값(bool) 등의 기본 자료형 이외에 리스트(list), 문자열(str), 튜플(tuple), 집합(set), 사전(dict) 등 여러 개의 값들을 묶어서 하나의 값으로 취급하는 모음(collection) 자료형을 제공한다. 모음 자료형은 컨테이너(container) 자료형으로 불리기도 한다.

모음 자료형은 크게 두 종류르 구분된다.

  • 항목들 사이에 순서가 있는 자료형: 문자열, 리스트, 튜플
  • 항목들 사이에 순서가 없는 자료형: 집합, 사전

여기서는 파이썬이 제공하는 자료형 중에서 가장 유용한 리스트 자료형을 다룬다.

순차(서열) 자료형: 리스트

리스트에 포함되는 값들 사이의 순서는 절대적으로 중요하다. 항목들의 순서, 항목들 사이의 순차(서열)이 중요하다는 의미로 리스트 자료형을 순차(서열) 자료형에 포함시킨다. 순차(서열) 자료형을 시퀀스(sequence) 자료형이라고 부르기도 한다.

리스트의 형식은 다음과 같다.

[ 항목1, 항목2, ..., 항목n ]

아래 예제와 같이 순서 또는 항목의 개수가 다르면 서로 다르다고 처리된다.

In [1]:
[1, 3] == [3, 1]
Out[1]:
False
In [2]:
[1] != [1,1]
Out[2]:
True

리스트 자료형의 주요 특징

리스트의 항목으로 임의의 값이 올 수 있으며, 항목들의 자료형이 달라도 된다. 따라서, 리스트 항목으로 다른 리스트가 사용될 수도 있다.

In [3]:
a_list = ['spam', 2.0, 5, [10, 20], True]

항목이 전혀 없는 비어 있는 리스트인 빈 리스트는 []로 표시된다.

In [4]:
print(type([]))
<class 'list'>

물론 리스트를 변수에 할당할 수도 있다.

In [5]:
myList = [1, 2, True, 6.5, "abc"]
print(myList)
[1, 2, True, 6.5, 'abc']

리스트 관련 함수

리스트 자료형은 가장 많이 사용되는 자료형인 만큼 리스트와 관련된 다양한 함수들이 존재한다. 특히 아래에 소개된 함수들은 리스트를 포함하여 모든 순차(서열) 자료형이 공통적으로 사용되며, 문자열 자료형에서 소개한 방식과 동일하게 작동한다.

  • [k] : 인덱싱
  • [i:j:k]: 슬라이싱
  • + : 이어 붙이기(concatenation)
  • * : 반복해서 이어붙이기
  • in : 항목 포함여부 확인
  • len : 항목 개수 확인
In [6]:
myList[0]
Out[6]:
1
In [7]:
myList[1:5:2]
Out[7]:
[2, 6.5]
In [8]:
myList + a_list
Out[8]:
[1, 2, True, 6.5, 'abc', 'spam', 2.0, 5, [10, 20], True]
In [9]:
myList * 2
Out[9]:
[1, 2, True, 6.5, 'abc', 1, 2, True, 6.5, 'abc']
In [10]:
2 in myList
Out[10]:
True
In [11]:
len(myList)
Out[11]:
5

수정 가능성: 리스트

리스트는 수정이 가능한 자료형이다. 아래 예제는 인덱싱을 활용하여 특정 인덱스의 값을 다른 값으로 수정할 수 있음을 보여준다. 심지어 수정된 항목의 자료형이 달라질 수도 있다.

In [12]:
myList[0] = 'Hi'
myList
Out[12]:
['Hi', 2, True, 6.5, 'abc']

리스트 자료형 메서드

아래의 메서드들이 리스트와 관련된 기본적인 메서드 들이다. 앞서 설명하였듯이, 아래 메서드들을 실행하면 리스트 자체가 수정될 수 있다.

  • append: 리스트 끝에 새로운 값을 추가하는 데에 사용됨.
In [13]:
myList.append(False)
print(myList)
['Hi', 2, True, 6.5, 'abc', False]
  • insert: 특정 인덱스의 위치에 항목을 추가한다.
In [14]:
myList.insert(2, 4.5)
print(myList)
['Hi', 2, 4.5, True, 6.5, 'abc', False]
  • pop: 특정 인덱스의 위치에 있는 항목을 리스트에서 삭제한 후 삭제된 값 리턴. 인자를 받지 않으면 마지막 항목을 대상으로 한다.
In [15]:
myList.pop()
Out[15]:
False
In [16]:
myList.pop(2)
Out[16]:
4.5
In [17]:
print(myList)
['Hi', 2, True, 6.5, 'abc']
  • sort: 리스트 항목들을 크기 오름차순으로 정렬. reverse 옵션 변수의 값을 True' 지정하면 내림차순 정렬. 리턴값은None` 임에 주의할 것.

주의: 크기를 서로비교할 수 있는 값들만 항목으로 사용된 경우만 작동한다. 그렇지 않으면 오류가 발생한다.

In [18]:
myList.sort()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-56aa534bad77> in <module>
----> 1 myList.sort()

TypeError: '<' not supported between instances of 'int' and 'str'
In [19]:
newList = [3.6, 1.7, 8.1, 6.5]
In [20]:
print(f'sort 메서드의 리턴값은 {newList.sort()}임.')
print(f'하지만 리스트 자체는 {newList}로 정렬됨.')
sort 메서드의 리턴값은 None임.
하지만 리스트 자체는 [1.7, 3.6, 6.5, 8.1]로 정렬됨.

내림차순으로 정렬하고자 할 경우 reverse라는 키워드 인자를 사용한다.

In [21]:
newList.sort(reverse=True)
print(newList)
[8.1, 6.5, 3.6, 1.7]
  • reverse 메서드는 리스트의 순서를 뒤집는다. sort 메서드와 마찬가지로 리턴값은 None이다.
In [22]:
myList.reverse()
print(myList)
['abc', 6.5, True, 2, 'Hi']
  • del: 특정 인덱스의 값을 삭제하는 함수이다. del 엄격한 의미에서는 리스트 관련 메서드가 아니다. 다만 pop과 비슷한 기능을 갖고 있어서 여기서 소개한다.
In [23]:
del(myList[2])
myList
Out[23]:
['abc', 6.5, 2, 'Hi']
  • index: 특정 값이 나타나는 인덱스 값 중에서 가장 작은 인덱스 내주기. 지정된 값이 항목이 아닌 경우 오류(ValueError) 발생.
In [24]:
myList.index(6.5)
Out[24]:
1
In [25]:
myList.index(4.7)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-25-f1ecf4000074> in <module>
----> 1 myList.index(4.7)

ValueError: 4.7 is not in list
  • count: 특정 값이 리스트에서 몇 번 사용되었는지 확인
In [26]:
myList.append(6.5)
print(myList.count(6.5))
2

리스트 해체

리스트의 길이를 정확히 알고 있는 경우 각각의 항목을 따로따로 떼어내어 변수에 저장할 수 있다.

In [27]:
x, y, z = [1, 2, 3]
print(f"x = {x}", 
      f"y = {y}",
      f"z = {z}",
      sep='\n')
x = 1
y = 2
z = 3

주의: 리스트의 길이와 변수의 개수가 다르면 오류가 발생한다.

In [28]:
x, y = [1, 2, 3]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-28-43d2015d9ce2> in <module>
----> 1 x, y = [1, 2, 3]

ValueError: too many values to unpack (expected 2)
In [29]:
x, y, z, w = [1, 2, 3]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-29-6891ae448c90> in <module>
----> 1 x, y, z, w = [1, 2, 3]

ValueError: not enough values to unpack (expected 4, got 3)

리스트를 해체하면서 앞으로 사용하지 않고 버릴 항목은 굳이 이름을 주지 않아도 된다. 이를 위해 밑줄(underscore, 언더스코어) 기호를 사용한다.

In [30]:
x, _, z = [1, 2, 3]
print(f"x = {x}", 
      f"z = {z}",
      sep='\n')
x = 1
z = 3

range 함수

리스트와 함께 많이 사용되는 함수이다. range 함수가 생성한 값은 range 자료형이며, 리스트와 유사한 기능을 수행한다.

사용법

range 함수는 한 개에서 최대 세 개의 인자를 받을 수 있다.

  • 인자가 한 개이면 0부터 인자까지의 구간을 의미한다. 단, 구간의 끝은 포함하지 않는다.
In [31]:
range(10)
Out[31]:
range(0, 10)

list 함수를 이용하여 0부터 9까지의 숫자로 이루어진 리스트를 얻을 수 있다.

In [32]:
list(range(10))
Out[32]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 인자가 두 개이면 구간의 처음과 끝을 나타낸다. 단, 구간의 끝은 포함하지 않는다.
In [33]:
list(range(5,10))
Out[33]:
[5, 6, 7, 8, 9]
  • 인자가 세 개이면 마지막 인자는 몇 개씩 뛰어넘을지를 정하는 스텝으로 사용된다. 예를 들어, 마지막 인자가 2이면 두 개씩 건너 뛰면서 구간을 정한다는 의미이다.
In [34]:
list(range(3, 10, 2))
Out[34]:
[3, 5, 7, 9]
  • 마지막 인자가 음수이면 역순으로 구간을 정한다는 의미이다.
In [35]:
list(range(10,1,-1))
Out[35]:
[10, 9, 8, 7, 6, 5, 4, 3, 2]
In [36]:
list(range(10,1,-3))
Out[36]:
[10, 7, 4]

예제

1부터 10까지의 정수를 출력하고자 할 때 아래와 같이 사용한다.

In [37]:
for num in range(1, 11):
    print(num, end=', ')
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 

비슷한 방식으로 1부터 10까지의 정수들의 리스트를 생성할 수 있다.

In [38]:
square_1_to_10 = []

for num in range(1, 11):
    square_1_to_10.append(num**2)
In [39]:
print(square_1_to_10)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

리스트 활용 예제

리스트를 활용하여 셰익스피어가 사용한 단어의 개수를 확인하고자 한다.

먼저 사이트에서 shakespeare.txt 파일을 다운받아 저장한다. (여기서는 data라는 하위 폴더에 저장한다고 가정한다.) shakespeare.txt 파일은 셰익스피어의 작품인 '한여름밤의 꿈'의 내용을 담고 있다.

먼저 open 함수를 이용하여 파일을 불러들인후 내용 전체를 하나의 문자열로 text라는 변수에 저장한다.

주의: 텍스트 파일을 읽어와서 저장하는 방식은 데이터셋 다루기를 참조한다.

In [40]:
shakespeare = open("data/shakespeare.txt")
text = shakespeare.read()
shakespeare.close()

주의사항

shakespeare.txt 파일을 다운로드 하지 않고 직접 확인할 수도 있다. 2장에서 다룬 urllib.request 라는 모듈에 포함되어 있는 urlopen 이란 함수를 이용하면 된다. 따라서 아래와 같이 실행하면 된다.

from urllib.request import urlopen
shakespeare = urlopen("shakespeare.txt")    

이제 text는 문자열 자료형이며, '한여름밤의 꿈' 작품 내용 전체를 담은 하나의 문자열이 할당되어 있다.

In [41]:
type(text)
Out[41]:
str

text에 할당된 문자열을 print(text) 등을 이용하여 확인할 수 있다. 하지만 여기서는 실행하지 않는다. 텍스트 내용이 너무 길기 때문이다. 그런데 '한여름밤의 꿈'에 사용된 단어의 수는 몇 개일까?

사용된 단어의 수를 확인하는 방법 중의 하나는 text에 할당된 문자열을 스페이스, 탭, 줄바꾸기 등을 기준으로 쪼개는 것이다. 문자열 클래스의 split 메서드가 그런 기능을 갖고 있다.

In [42]:
text_words = text.split()

len 함수를 이용하여 text_words 리스트에 포함된 단어들의 개수를 확인할 수 있다.

In [43]:
len(text_words)
Out[43]:
980637

무려 거의 100만 개의 단어가 포함되어 있다. 그런데 과연 셰익스피어가 100만개의 단어를 알고 있을까? 그럴 수가 없다. 실제로 영어 단어가 100만개가 넘는다고 알려져 있지만 한 명의 사람이 100만개의 단어를 활용할 수는 없다. 실제로 셰익스피어는 자신의 작품 전체를 통틀어 28,829개의 단어를 사용했다고 알려져 있다.

자료 근거: https://www.opensourceshakespeare.org/stats/

그렇다면 100만과 2만9천 사이의 오차는 어떻게 발생하였을까?

중복 단어 제거

먼저 리스트에는 항목이 여러 번 중복해서 사용될 수 있다. 따라서 중복된 항목들을 제거해야만 실제로 사용된 서로 다른 단어들의 개수를 확인할 수 있을 것이다.

여기서는 집합 자료형을 활용하여 중복을 제거하는 방법을 소개한다.

주의사항

  • 집합 자료형은 중학교에서 배운 집합 개념을 사용하며, 동일한 원소가 두 번 사용되어도 한 번 사용한 것으로 간주한다.
  • 집합 자료형을 활용하는 다양항 방식이 존재하지만 여기서는 리스트 자료형을 집합 자료형으로 형변환시켜서 중복된 항목을 제거하는 용도로만 사용한다.
  • 집합 자료형으로의 형변환은 set이란 함수를 사용하면 된다.
In [44]:
words_set = set(text_words)

이제 len 함수를 이용하여 words_set 집합의 원소의 개수를 알아낼 수 있다.

In [45]:
len(words_set)
Out[45]:
33505

이제, 서로 다른 단어의 개수가 33,505개임을 확인할 수 있다. 하지만 여전히 2만 9천여개 보다는 많이 크다.

무엇이 문제일까?

기호 제외하기

첫 40개 단어를 아래와 같이 확인하면 단어 이외에 쉼표, 느낌표, 콜론 등이 리스트에 포함되어 있음을 알 수 있다.

In [46]:
text_words[:30]
Out[46]:
['A',
 "MIDSUMMER-NIGHT'S",
 'DREAM',
 'Now',
 ',',
 'fair',
 'Hippolyta',
 ',',
 'our',
 'nuptial',
 'hour',
 'Draws',
 'on',
 'apace',
 ':',
 'four',
 'happy',
 'days',
 'bring',
 'in',
 'Another',
 'moon',
 ';',
 'but',
 'O',
 '!',
 'methinks',
 'how',
 'slow',
 'This']

따라서 쉼표(,), 마침표(.), 느낌표(!), 물음표(?), 콜론(:), 세미콜론(;) 등을 리스트에서 제거해야 한다.

그리고 경우에 따라 단어 끝에 쉼표, 느낌표 등이 붙어 있을 수도 있는데, 동일한 단어에 그런 기호가 붙는 경우와 그렇지 않은 경우도 중복으로 처리해야 한다.

그런데 어떻게 할까? 그리고 어떻게 어떤 기호들이 사용되었는지 알아낼 수 있을까?

완벽하지는 않겠지만 아래와 같이 하나의 기호로 구성된 문자열은 길이가 1이라는 사실을 이용하여 사용된 기호들만 따로 하나의 리스트로 만들어보면 사용된 기호를 거의 알아낼 수 있을 것이다.

In [47]:
word_of_length_1 = []

for word in text_words:
    if len(word) == 1:
        word_of_length_1.append(word)

그런데 word_of_length_1 리스트의 길이가 매우 크다.

In [48]:
len(word_of_length_1)
Out[48]:
203281

하지만 역시 중복이 많을 것이므로 집합으로 형변환하여 확인하면 개수가 확 줄어든다.

In [49]:
len(set(word_of_length_1))
Out[49]:
39

39개 뿐이라 직접 확인할 수 있다.

In [50]:
print(set(word_of_length_1))
{'C', 'D', 'N', 'H', 'O', 'I', 'A', ':', 'T', 'b', 's', '.', 'h', 'd', 'a', '!', 'u', 'c', '2', ',', '[', 'M', 'v', 't', 'o', 'V', 'G', 'y', ';', 'i', '?', 'l', 'B', 'e', 'R', ']', 'r', 'j', 'p'}

위에서 확인한 결과 사용된 기호는 ",.!?:;[]" 문자열에 포함된 8개의 기호임을 알 수 있다. 이제 text_words에서 위 기호들을 제거하면 되며, 이를 위해 문자열의 strip 메서드를 활용한다.

In [51]:
# 제거한 문자들
symbols = r" \n\t,.!?:;[]"

# 기호를 제거한 단어를 저장할 변수
text_words_stripped = []

# text_words에 포함된 단어들에서 기호를 제거한 후 빈문자열이 아닌 것만 추가
for word in text_words:
    word_stripped = word.strip(symbols)
    if len(word_stripped) > 0:
        text_words_stripped.append(word_stripped)

리스트의 길이는?

In [52]:
len(text_words_stripped)
Out[52]:
813736

중복되지 않는 단어의 개수는?

In [53]:
len(set(text_words_stripped))
Out[53]:
32308

여전히 좀 많다.

이번엔 대문자와 소문자를 구분하지 않고 단어의 동일성 여부를 따져 보자.

In [54]:
# 제거한 문자들
symbols = r" \n\t,.!?:;[]"

# 기호를 제거한 단어를 저장할 변수
text_words_stripped = []

# text_words에 포함된 단어들에서 기호를 제거한 후 빈문자열이 아닌 것만 추가
# 단, 모두 소문자화 해서 추가
for word in text_words:
    word_stripped = word.strip(symbols)
    if len(word_stripped) > 0:
        text_words_stripped.append(word_stripped.lower())

중복되지 않은 단어의 개수는?

In [55]:
len(set(text_words_stripped))
Out[55]:
28094

이제 (거의) 제대로 된 결과를 얻게 되었다.

연습문제

  1. 아래 기준을 만족하는 nested_sum 함수를 구현하라.

    • 리스트를 인자로 사용한다.
    • 리스트의 항목은 정수들의 리스트가 사용된다.
    • 리턴값은 리스트에 사용된 모든 정수들의 합니다.

      예제:

      >>> t = [[1, 2], [3], [4, 5, 6]]
      >>> nested_sum(t)
      21
      

      힌트: sum 함수 활용

  2. 아래 기준을 만족하는 cumsum 함수를 구현하라.

    • 정수들의 리스트를 인자로 사용한다.
    • 리턴값은 리스트에 사용된 정수들을 하나씩 누적해서 합한 값들의 리스트이다.

      예제:

      >>> t = [1, 2, 3]
      >>> cumsum(t)
      [1, 3, 6]
      

      힌트: append 메서드 활용

  3. 아래 기준을 만족하는 middle 함수를 구현하라.

    • 정수들의 리스트를 인자로 사용한다.
    • 리턴값은 리스트의 처음과 끝에 사용된 항목을 제거한 리스트이다.

      예제:

      >>> t = [1, 2, 3, 4]
      >>> middle(t)
      [2, 3]
      

      힌트: 슬라이싱 활용

  4. 아래 기준을 만족하는 chop 함수를 구현하라.

    • 정수들의 리스트를 인자로 사용한다.
    • 리스트의 처음과 끝에 사용된 항목을 제거한다.
    • 리턴값은 None이다.

      예제:

      >>> t = [1, 2, 3, 4]
      >>> chop(t)
      >>> t
      [2, 3]
      

      힌트: del 함수 활용

  5. 아래 기준을 만족하는 is_sorted 함수를 구현하라.

    • 리스트를 인자로 사용한다.
    • 입력된 리스트가 올림차순으로 정렬이 되어 있으면 True를 그렇치 않으면 False를 리턴한다.

      예제:

      >>> is_sorted([1, 2, 2])
      True
      >>> is_sorted(['b', 'a'])
      False
      

      힌트: sort 메서드 또는 sorted 함수 활용

  6. 아래 기준을 만족하는 is_anagram 함수를 구현하라.

    • 두 개의 문자열을 입력받는다.
    • 두 문자열이 서로 애너그램의 관계이면 True를 그렇지 않으면 False를 리턴한다.

      주의: 애너그램의 관계는 동일한 문자를 동일한 개수만큼 사용한 관계이다.
      예제:

      >>> is_anagram('hello', 'eollh')
      True
      >>> is_anagram('hello', 'eoLLh')
      False
      
  7. continue, break, pass 세 계의 특별한 명령문의 이해를 도와주는 문제이다.

    1. 아래 for 반복문을 완성하라.
      for n in range(2,10):
           for x in range(2, n):
               # 코드를 완성하세요
               pass
           else:
               # 코드를 완성하세요.
               pass
      
      실행결과는 다음과 같아야 한다.
       2은 소수다.
       3은 소수다.
       4 = 2*2
       5은 소수다.
       6 = 2*3
       7은 소수다.
       8 = 2*4
       9 = 3*3
      힌트:
       * `for` 반복문과 함께 사용되는 `else`의 역할에 주목할 것.
       * `break` 활용
    2. 아래 for 반복문을 완성하라.
      for n in range(2,10):
           # 코드를 완성하세요
           pass
      
      실행결과는 다음과 같아야 한다.
       짝수 2을(를) 찾았다.
       3을 찾았다.
       짝수 4을(를) 찾았다.
       5을 찾았다.
       짝수 6을(를) 찾았다.
       7을 찾았다.
       짝수 8을(를) 찾았다.
       9을 찾았다.
      힌트:
       * `if` 조건문과 `continue` 명령문 활용
    3. 아래 for 반복문을 완성하라.
      for n in range(2,10):
           # 코드를 완성하세요
           pass
      
      실행결과는 다음과 같아야 한다.
       짝수 2을(를) 찾았다.
       2을 찾았다.
       3을 찾았다.
       짝수 4을(를) 찾았다.
       4을 찾았다.
       5을 찾았다.
       짝수 6을(를) 찾았다.
       6을 찾았다.
       7을 찾았다.
       짝수 8을(를) 찾았다.
       8을 찾았다.
       9을 찾았다.
      힌트:
       * `if` 조건문과 `pass` 명령문 활용