콘텐츠로 건너뛰기
Home » Python 101 : 리스트

Python 101 : 리스트

지난 글에서 파이썬에서 사용되는 가장 기본적인 값의 유형에 대해 살펴보았다. 오늘은 리스트에 대해서 이야기해보려고 한다. 리스트는 여러 개의 값이 연속적으로 배치된 일종의 집합이다. 다른 프로그래밍 언어에서는 배열(Array)이라고 하기도 한다. 사실 배열과 다른 리스트라는 자료 구조가 별도로 존재하기는 하는데 (연결 리스트 같은 걸 들어본 적이 있을지도 모르겠다.) 파이썬의 리스트는 C의 배열과 크게 다르지 않다고 생각하면 된다.

특징

리스트는 다음과 같은 특징을 가지고 있다.

  1. 일련의 값(객체)들의 순서가 있는 집합이다. 각 원소는 정수 인덱스로 지정할 수 있다. 이 인덱스는 0부터 시작한다.
  2. 변경이 가능(mutable)하다. 리스트는 만들어진 후원에 원소를 추가하거나, 제거/변경할 수 있다.
  3. 각 원소들은 리스트에 의해 참조된다. 만약 리스트를 복사한다면, 원소들을 참조하고 있는 리스트가 복사되지 각각의 원소들까지 모두 복사되는 것은 아니다. (이를 얕은 복사라 한다.)
  4. 각각의 원소는 타입이 달라도 상관없다. 많은 프로그래밍 언어에서 배열이 그 원소가 모두 동일한 타입이어야 하는 경우가 있지만 파이썬에는 그런 제약이 없다. 그래서 이름을 배열이라 하지 않고 리스트라 했는지도 모른다.1사실 C언어 레벨에서 본다면 리스트는 그 자체로 배열인 것이 사실이다. ‘PyObject’ 타입에 대한 포인터의 배열이다.

리스트는 가장 먼저 접하는 자료 구조인 동시에, 파이썬에서는 가장 중요한 데이터 타입 중 하나이다. 일단 이 리스트를 만들고 쓰는 법에 대해서 조금 살펴보자.

리스트 만들기

리스트를 만드는 방법은 크게 두 가지인데, 하나는 리터럴을 사용하는 것이고 다른 하나는 list() 함수를 사용하는 것이다. list() 함수는 마치 리스트와 비슷한 성질을 가지고 있는 데이터를 리스트로 변환하는 역할을 한다. ‘리스트와 비슷하다’고 하는 것은 결국 “원소를 한 줄로 세울 수 있는 집합”이라는 것으로, 문자열이나 range 객체 같은 것을 리스트로 변환할 때 사용한다.

리스트 리터럴은 [1, 2, 3] 과 같이 대괄호 속에 각각의 콤마로 구분하여 쓴다. 리스트를 만드는 문법으로 파이썬에는 리터럴 외에 리스트 축약(list comprehension)이라는 문법이 있다. 사실 이 리스트 축약 문법은 너무 중요해서 아무리 강조해도 지나치지 않는데, 이에 앞서서 리스트 내의 각 원소들에는 어떻게 접근하는지 살펴보자.

인덱스를 통한 원소 액세스

리스트의 각각의 원소는 정수 인덱스를 통해서 액세스할 수 있다. 예를 들어 [3, 1, 4, 5] 의 원소를 갖는 리스트 a 에 대해서 첫번째 원소인 3은 a[0], 두 번째 원소인 1은 a[1] 과 같은 식이다. 인덱스가 0부터 시작하기 때문에 리스트의 길이 이상의 인덱스에 대해서는 원소를 찾을 수 없으며, 이때에는 IndexError 예외가 발생한다.

정수 인덱스는 음의 값을 갖는 것이 가능하다. -0은 0과 같으므로 음수 인덱스는 -1 부터 시작하여 1씩 작아진다. 이 때 -1은 (리스트의 길이 – 1) 과 같이 계산하여 a[-1] 은 맨 마지막 원소를 의미한다. a[-1] == 5, a[-2] == 4 와 같다.

슬라이싱

리스트에서 하나의 값이 아닌 특정한 범위를 지정해서 부분집합/부분열을 얻는 방법으로 ‘슬라이싱’이 있다. 슬라이싱은 인덱스 사용 문법 (보통 영어로는 subscription 이라고 한다)에서 정수 인덱스 대신에 범위를 지정하는 문법을 사용한다.

  • 시작:끝의 범위를 사용한다. 이 때 끝 범위에 해당하는 위치는 포함되지 않는다. : a[2:7]
  • 시작이나 끝은 각각 생략할 수 있다. 생략된 범위는 한쪽 끝까지를 의미한다. : a[:7] , a[2:]
  • 시작과 끝 모두 생략할 수 있다. 이 경우는 리스트 전체의 복사본이다: a[:]
  • 확장된 문법으로 “시작 : 끝 : 단계”를 사용할 수 있다. : a[2:7:2] => [a[2], a[4], a[6]])
  • 역순으로 된 부분열을 만드는 경우 단계를 음수로 지정한다. : a[7:4:-2] => [a[7], a[5]]
  • 역순으로 뒤집은 복사본을 만드는 방법 : a[::-1]

슬라이싱은 인덱스를 확장하는 문법인 동시에 리스트와 관련된 파이썬 코드의 성능을 극대화 시킬 수 있는 방법이기도 하다. 보통 파이썬에서 리스트를 조작하는 코드를 for 문을 통해서 각각의 원소를 순회하는 방식으로 작성하기 쉬운데, 슬라이싱을 사용할 수 있는 코드라면 슬라이싱을 사용하는 것이 훨씬 빠르게 작동한다. 리스트의 subscription이나 for loop는 모두 파이썬 코드에서 사용되는 정보들로 인한 오버헤드가 있지만, 슬라이싱 문법은 C 코드 수준에서 작동하는 문법이므로 훨씬 빠르다. 이런 성질을 사용해서 소수를 판별을 위한 에라스토테네스의 체는 파이썬으로도 매우 빠르게 작동하는 코드를 만들 수 있다.

리스트 축약 (List Comprehension)

원소를 콤마로 구분해서 직접 나열하는 리스트 리터럴은 수학에서 집합을 표시할 때 사용하는 “원소 나열법”에 해당한다고 볼 수 있다. 이런 관점에서 리스트 축약은 집합을 표시하는 또 다른 방법인 조건 제시법과 비슷하다.

사실 “축약” 이라는 단어가 “comprehension”의 번역은 아니다. 개인적으로는 다른 곳에서 많이 쓰고 있는 “지능형 리스트”라는 표현이 마음에 들지 않아서 ‘축약’이라는 표현을 쓰는데, 원소 나열식 리터럴을 조건 제시 형태로 축약했다는 의미도 있고, 이렇게 축약된 구문이 리스트 리터럴로 해석된다는 의미에서는 “지능형 리스트”보다는 훨씬 나은 표현이라고 생각한다.

사실 “List Comprehension”의 의미는 이 문법을 수학적으로 이해하려는 데에서 찾아야 한다. 어떤 리스트 I 의 원소들을 [i1, i2, i3, i4, ...] 라고 하자. 그리고 i --> j 로 변환하는 어떤 함수 f(i) 가 있다고 하자. 리스트 I 의 모든 원소를 함수 f() 를 사용해 j1, j2, ... 로 변환하여 리스트 J 를 만들 수 있을 것이다. “List Comprehension”은 결국 “리스트 I“를 “리스트 J“로 “번역”하는 문법이라고 할 수 있다.

# 리스트 i1, i2, i3,... 를 번역한다
[ f(i)  for  i  in [i1, i2, i3...]]

# [ i * 2 for i in [1,2,3,4]]
# ==> [2, 4, 6, 8]

# 뒤에 "if {조건식}"을 붙여서 특정한 원소만 추려낼 수 있다.
[ i  for i in range(100) if i % 2 > 1]

값을 변환하는 표현식을 원본 리스트의 각 원소에 적용하여 리스트 내부를 변형하는 아이디어는 집합 내부로 어떤 함수를 사상(mapping)하는 함수형 프로그래밍의 기본적인 연산방법인 map에 해당하며, ‘if’ 절을 사용하여 원소를 필터링하는 것은 filter 에 대응한다. 이 각각의 기능을 하는 두 함수는 map()filter() 인데, 동일한 리스트를 생성하는 두 가지 방식의 문법 차이는 다음과 같다.

# 1~100 사이의 3의 배수를 제곱한 리스트
a = [i * i for i in range(1, 101) if i % 3 == 0]

# filter(), map() 함수를 사용한 표현
a = list(
      map(lambda x: x * x, 
        filter(lambda i: i % 3 == 0, range(1, 101)
      )
    )

리스트와 같이 어떤 값들을 순서대로 줄지어 놓은 것을 연속열(Sequence)이라 한다. 리스트 축약은 사실 이러한 연속열로부터 리스트를 만들어내는 방법이라고 볼 수 있다. 그리고 다양한 소스로부터 리스트를 만들 수 있는 것처럼, set, dict, generator 등의 다양한 타입을 비슷한 문법으로 만들 수 있다. 앞으로 다양한 문제와 코드를 접하면서 깨달아가게 되겠지만, 리스트 축약은 그 문법의 활용도 뿐 아니라 그곳에서 파생되는 맥락이 파이썬을 공부하는데 있어서 가장 기본적인 사고원리와 관련이 깊기 때문에 아주 중요하다 하겠다.

리스트와 관련되는 함수와 리스트의 메소드

  • len(a) : 리스트의 길이(원소의 개수)를 구하는 함수. 리스트 뿐만 아니라 문자열의 길이 등을 구할 때에도 사용한다.
  • sorted(a) : 리스트를 정렬한 사본을 만드는 함수.
  • max(a), min(a) : 리스트 a 의 최대/최소 값을 구하는 함수
  • map(f, a) : 리스트 a의 모든 원소에 대해 함수 f를 적용해서 새로운 반복객체를 만드는 함수
  • filter(f, a) : 리스트 a의 모든 원소에 대해 함수 f가 True인 값만 골라서 새로운 반복 객체를 만드는 함수
  • reversed(a) : 리스트의 순서를 뒤집은 사본을 만드는 함수
  • a.append(b) : 리스트 a의 맨 끝에 새 원소 b를 추가
  • a.pop() : 리스트 a의 맨 끝의 원소를 떼어내어 반환함
  • a.insert(idx, b) : 리스트 a에서 인덱스 idx 의 위치에 새 원소 b를 삽입함
  • a.remove(b) : 리스트 a에서 b를 제거함. 만약 b와 같은 값이 2개 이상이면 맨 처음 하나만 제거됨
  • a.index(b) : 리스트 a 에서 b가 처음 나타나는 인덱스 위치를 리턴함

이러한 함수에 대해서는 각자 공식 문서등을 잘 읽어보는 것이 도움이 될 것이다. 참고로 a.index(b) 의 동작에 대해서 언급하고 싶은데, 보통 배열에서 특정한 원소의 위치를 찾는 이러한 메소드를 사용할 때에는 존재하지 않는 원소값을 찾으려할 때 -1 과 같은 값을 리턴하여 존재하지 않는다는 것을 알려준다. 하지만 파이썬은 이러한 방법을 사용하기 보다는 존재하지 않는 값을 리스트에서 위치를 찾으려했다며 ValueError 예외를 일으킨다.