본문 바로가기

진리는어디에/Python

[Python] 시퀀스 자료형 #1 리스트(list)

파이썬에선 값이 연속적으로 이어진 자료형들을 총칭하여 "시퀀스 자료형(sequence type)"이라고 부른다. 이번 강좌에서는 파이썬의 시퀀스 자료 구조 중의 하나인 리스트에 대해 알아 본다.

기본적인 리스트 사용법만 본다면 상관 없지만 리스트의 고급 기법에 대한 설명을 쉽게 이해하기 위해서는 아래 항목들에 대한 이해가 되어 있으면 좋다.
길지 않은 내용들이니 가벼운 마음으로 한번 훑어 보고 오도록 하자. 다 이해하지 못해도 상관 없다. 개념만 어렴풋이라도 알고 있으면 된다.

목차

  1. >> 리스트(list)
  2. 튜플(tuple)
  3. 셑(set)
  4. 딕셔너리(dict)

리스트(list)란?

  • 리스트는 파이썬 시퀀스 자료 구조 중 하나로써 여러 개의 데이터들을 일괄 저장하고 일괄 처리하는데 사용 될 수 있다. 
  • 배열 연산자 [] 를 이용해 표시 된다.
  • mutable 객체로써 요소들을 추가하거나 삭제 할 수 있으며, iteratable 객체로써 반복자(iterator)를 이용하여 순회가 가능하다.
  • 리스트는 여러 데이터를 일괄 저장하고 처리하는 것 뿐 아니라, 함수가 여러 개의 값을 리턴하거나 여러 개의 인자를 받을 때도 활용 될 수 있다.

리스트(list) 생성 방법

1. 타입 이름 'list' 사용

list1 = list()          # 빈 리스트를 생성
list2 = list('ABCD')    # list(iterable object)
                        # ['A','B','C','D']
list3 = list(range(10)) # [0,1,2,3,4,5,6,7,8,9]

def even_generator() :
    for i in range(10) :
        if i % 2 == 0 :
            yield i

list4 = list(even_generator()) # [0, 2, 4, 6, 8]

list5 = list((i for i in range(10) if i % 2 == 0))
list6 = list( i for i in range(10) if i % 2 == 0 ) # ()가 없어도 됨
  • list1(1라인) : list 타입 이름을 이용하여 빈 리스트를 생성한다
  • list2(2라인) : list의 인자로 iterable object를 넘겨주면 iterable object를 개별 요소로 쪼개어 리스트로 만들어 준다. str은 iterable object이므로 ['A','B','C','D'] 와 같은 리스트가 만들어진다.
  • list3(4라인) : range 역시 iterable object이므로 0부터 9까지의 정수가 들어있는 리스트를 만든다.
  • list4(11라인) : generator 또한 iterable object이므로 리스트의 인자로 넘겨질 수 있다. generator 에서 특정 로직에 의해 선별된 값으로 이루어진 리스트를 만들 수 있다. generator 관한 설명은 [여기]를 참고 하도록 한다.
  • list5(13라인) : generator가 가능하므로 generator를 생성하여 리턴하는 generator 표현식으로도 리스트를 생성 할 수 있다.
  • list6(14라인) : generator 표현식은 양끝에 괄호가 필수지만 list 타입을 생성에 사용하면서 이미 괄호가 있으므로 생략이 가능하다.

2. [] 연산자 사용

list1 = [] # list()
list2 = [1, 2, 3]
list3 = [1, 3.4, 'ABC']
list4 = [1, 2, [3, 4, 5]] # list를 포함하는 list

배열 연산자를 이용하여 리스트를 생성할 수도 있다.

  • list1(1라인) : 아무런 인자가 없는 빈 배열 연산자는 빈 리스트를 생성한다. list()와 동일한 의미다.
  • list2(2라인) : 같은 타입의 요소로 채워진 리스트를 생성한다.
  • list3(3라인) : 다른 타입의 요소들로도 채워진 리스틀 생성할 수도 있다.
  • list4(4라인) : 리스트 안에 또 다른 리스트가 요소로 들어갈 수도 있다.

3. 리스트 표현식 사용

list1 = [ i for i in range(10) if i % 2 == 0 ]

제너레이터 표현식과 비슷한 방법으로 리스트를 생성하는 것이 가능하다. 이런 표현법을 지능형 리스트(list comprehension)라고 한다. 지능형 리스트를 이용하면 리스트 객체 생성시 로직을 적용할 수 있는데, 아래 예제 처럼 두 개의 리스트로 부터 요소를 하나씩 뽑아 각 요소를 결합한 새로운 리스트를 만들어 낼 수도 있다.

l1 = 'AB'
l2 = '12'

l3 = [ [v1, v2] for v1 in l1 for v2 in l2 ]
print(l3) # [['A', '1'], ['A', '2'], ['B', '1'], ['B', '2']]

# 개행을 사용하면 가독성을 높일 수 있다.
l4 = [ [v1, v2] for v1 in l1 
                for v2 in l2 ]
Tip. Python Comprehension

파이썬의 Comprehension은 한 Sequence가 다른 Sequence (Iterable Object)로부터 (변형되어) 구축될 수 있게한 기능이다. Python 2 에서는 List Comprehension (리스트 내포)만을 지원하며, Python 3 에서는 Dictionary Comprehension과 Set Comprehension을 추가로 지원하고 있다.

from : http://pythonstudy.xyz/python/article/22-Python-Comprehension

리스트의 메모리 구조

아래와 같이 리스트를 생성하면 다음과 같은 리스트를 위한 메모리 레이아웃이 구성 된다. 알면 좋지만 몰라도 리스트를 사용하는데 있어 큰 상관이 없으니 그냥 가볍게 읽고 넘어가자.

ㅣ = [20, 'Jessa', 35.75, [30, 60, 90]]

  • ①번 : 변수 l이 PyListObject 리스트 객체를 가리키고 있다.
  • ②번 : 이 리스트에는 4개의 요소가 들어 있으므로 리스트의 개수를 나타내는 ob_size가 4다.
  • ③번 : ob_item이 각 리스트의 개별 요소들을 가리키는 '이름 없는 변수 집합'을 가리키고 있다. 이름 없는 변수들 역시 변수 l과 마찬가지로 각각 다른 객체들을 가리키고 있다.
  • ④번 : allocated는 실제 할당된 메모리 버킷의 개수를 나타낸다.
    리스트에 요소가 추가 될 때 마다 메모리 할당을 하게 되면 성능저하가 발생할 수 있으므로 미리 필요한것 보다 많은 버킷들을 할당해 놓는다. 위 이미지에서는 하나만 더 할당 되어 있지만 실제 코드를 보면 리스트의 사이즈에 따라 4 또는 8씩 증가한다. 하지만 상세 구현은 파이썬 버전에 따라 다를 수 있으므로 실제 필요한 버킷의 개수보다 여유롭게 alllocated 사이즈를 유지한다는 것만 기억하자.
  • ⑤번 : 리스트에 또다른 리스트가 포함 될 수 있다. 이렇게 되면 다시 똑같은 메모리 구조가 재귀적으로 생성 된다.

리스트 인덱싱

리스트 인덱싱은 이전에 배웠던 문자열의 인덱싱과 동일하다.

s = [1, 2, 3, [4, 5, 6]]

print(s[0]) # 1
print(s[-1]) # [4, 5, 6]
print(s[-1][0]) # 4

간단한 예제라 설명은 생략한다. 인덱싱에 대한 자세한 설명은 [참고:문자열 인덱싱]을 참고하도록 한다.

다만 5라인의 경우 -1 인덱싱을 통해 리턴된 s의 가장 마지막 요소 [4, 5, 6] 또한 리스트다. 이 리스트의 0번 인덱스의 값을 리턴하라는 의미다. 위 예제에서는 4가 리턴된다.

리스트 슬라이싱

리스트의 슬라이싱 또한 문자열의 슬라이싱과 동일하다. 슬라이싱의 결과는 리스트 형태로 반환 된다.

s = [0, 1, 2, 3, 4, 5, 6, 7, 9]

print(s[1:3])   # [1,2]
print(s[1:9:3]) # [1,4,7]

sc = slice(1, 9, 3)
print(s[sc])    # [1,4,7]

간단한 예제라 설명은 생략한다. 슬라이싱에 대한 자세한 설명은 [참고:문자열 슬라이싱]을 참고하도록 한다.

리스트 unpacking

인덱싱과 슬라이싱은 문자열과 완전 동일하다. 하지만 unpacking은 리스트만의 고유 오퍼레이션으로써 리스트의 각 요소들을 개별 변수로 풀어 놓는다. 말로는 이해가 어려울테니 예제 통해 쉽게 이해하도록 하자.

s = [1, 'AB', [2, 3]]

e1, e2, e3 = s # unpacking

print(f'{e1}, {e2}, {e3}')  # 1, AB, [2, 3]

e1, e2 = s                  # ValueError: too many values to unpack (expected 2)
e1, e2, e3, e4  = s         # ValueError: not enough values to unpack (expected 4, got 3)

3라인에서 3개의 요소를 가진 리스트를 3개의 변수로 언패킹하는 것을 볼 수 있다. 주의할 점은 언패킹시에는 리스트 요소의 개수와 언패킹 요속를 받는 변수들의 개수가 같아야 한다는 것이다. 개수가 다를경우 7,8라인과 같은 예외를 발생 시킨다.

+ 와 * 연산자

리스트의 +, * 연산자 또한 문자열의 +, * 연산자와 비슷하다.

s1 = [1, 2, 3]
s2 = [4, 5, 6]

print(s1 + s2)       # [1, 2, 3, 4, 5, 6]
print(3 * s1)        # [1, 2, 3, 1, 2, 3, 1, 2, 3]
print(s1 * 3)        # [1, 2, 3, 1, 2, 3, 1, 2, 3]
print(s1 + s2 * 3)   # [1, 2, 3, 4, 5, 6, 4, 5, 6, 4, 5, 6]
print((s1 + s2) * 3) # [1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]

s1 += s2             # s1 = s1 + s2
print(s1)            # [1, 2, 3, 4, 5, 6]

print(3 in s1)       # True

list 주요 메소드

  • len() : 리스트 길이 리턴
s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(len(s)) # 10
  • del() : 리스트 요소 삭제
s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

del s[0]     # 인덱스로 삭제
print(s)     # [1,2,3,4,5,6,7,8,9]

del s[1:9:3] # 슬라이싱으로 범위 삭제
print(s)     # [1,3,4,6,7,9]
  • append : 리스트의 맨 뒤에 요소 추가
  • insert : 리스트의 특정 인덱스에 요소 추가
s = [1, 3, 5]

s.append(2)        # [1,3,5,2]
s.append([5,6])    # [1,3,5,2,[5,6]]
s.insert(1, [7,8]) # [1,[7,8],3,5,2,[5,6]], 1번 인덱스에 [7,8] 삽입​
  • remove : 가장 먼저 만나는 값을 삭제
  • pop : 리스트의 맨 마지막 요소 리턴 및 삭제
s = [1, 3, 5, 2, 4, 5, 6]

del s[0]    # 0번째 요소 1을 제거
s.remove(5) # 왼쪽에서 부터 오른쪽으로, 첫번째 나오는 5를 제거

print(s)    # [3, 2, 4, 5, 6]

n = s.pop()
print(n)    # 6, 가장 마지막 요소를 리턴함과 동시에 리스트에서 삭제
print(s)    # [3, 2, 4, 5]
  • extend : + 연산자와 같지만 새로운 리스트를 리턴하는 것이 아니라 extend 호출 리스트의 값이 변경 된다.
s1 = [1, 2, 3]
s2 = [4, 5]

s3 = s1 + s2    # s1, s2는 변하지 않음
s1.extend(s2)   # s1 += s2와 동일

print(s1)       # [1, 2, 3, 4, 5]
  • count, index
s = [1, 3, 5, 7, 9, 1, 3, 5, 7, 9]

print(s.count(3))       # 2 (리스트 요소중 3의 개수를 리턴)
print(s.index(3))       # 1 (리스트에서 가장 처음 만나는 3의 인덱스 리턴)

# 3을 인덱스 3 부터 8 사이에서 찾는다
print(s.index(3, 3, 8)) # 6
  • reverse
s = [1, 3, 5, 6, 7, 9, 2, 4, 6, 8, 10]

# 리스트 요소들의 위치를 좌우 뒤집음
s.reverse() # [10, 8, 6, 4, 2, 9, 7, 6, 5, 3, 1]
  • sort 메소드와 sorted 표준 함수
s = [1, 3, 5, 6, 7, 9, 2, 4, 6, 8, 10]

s1 = sorted(s) # s는 변하지 않고 정렬된 새로운 리스트 반환
print(s)  # [1, 3, 5, 6, 7, 9, 2, 4, 6, 8, 10]
print(s1) # [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10]

# s의 값이 변경
s.sort()
print(s) # [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10]

리스트 복사

리스트에는 얕은 복사와 깊은 복사. 두가지 방법의 복사 방법이 있다.

  • 얕은 복사 : 리스트는 복사 되어 다른 객체가 만들어지지만 리스트가 가지고 있는 요소들은 공유
s1 = [1, 2, [3, 4]]
s2 = s1
s3 = s1.copy()

print(hex(id(s1))) # 변수 s1이 가리키는 객체의 주소
print(hex(id(s2))) # 변수 s2는 s1과 같은 객체를 가리키므로 같은 주소 출력
print(hex(id(s3))) # 변수 s3는 s1의 복사본 객체를 가리키므로 다른 주소 출력

print(hex(id(s1[2]))) # 요소의 참조는 동일
print(hex(id(s2[2])))
print(hex(id(s3[2])))

s1[2].append(5)

print(s1[2]) # [3, 4, 5]
print(s2[2]) # [3, 4, 5]
print(s3[2]) # [3, 4, 5]

얇은 주소는 복사 비용과 메모리 사용을 줄일 수 있지만 리스트의 요소들을 공유하므로 s1에서 발생한 변경이 의도치 않게 s3에 까지 영향을 미칠 수 있다. 

  • 깊은 복사 : 리스트 객체 자체와 리스트가 가리키고 있는 요소들 모두 복사하여 새로운 인스턴스 생성
    copy 모듈의 deepcopy() 함수를 이용한다.
import copy

s1 = [1, 2, [3, 4]]
s2 = s1
s3 = s1.copy()
s4 = copy.deepcopy(s1)  # 리스트 객체가 가리키는 요소들 까지 모두 복사

print(hex(id(s1))) # 변수 s1이 가리키는 객체의 주소
print(hex(id(s2))) # 변수 s2는 s1과 같은 객체를 가리키므로 같은 주소 출력
print(hex(id(s3))) # 변수 s3는 s1의 복사본 객체를 가리키므로 다른 주소 출력
print(hex(id(s4))) # 변수 43는 s1의 복사본 객체를 가리키므로 다른 주소 출력

print(hex(id(s1[2]))) # 요소의 참조는 동일
print(hex(id(s2[2]))) # 요소의 참조는 동일
print(hex(id(s3[2]))) # 요소의 참조는 동일
print(hex(id(s4[2]))) # 복사된 요소를 참조하므로 다른 주소

s1[2].append(5)

print(s1[2]) # [3, 4, 5]
print(s2[2]) # [3, 4, 5]
print(s3[2]) # [3, 4, 5]
print(s4[2]) # [3, 4]

마치며

이상 리스트의 기본 사용법에 대해 알아 보았다. 특히, 리스트의 얕은 복사와 깊은 복사는 제대로 알지 못하고 썼을 때 의도하지 않은 상황을 만들어 낼 수 있으니 반드시 제대로 숙지하고 넘어가자. 다음 강좌 [Python] 파이썬 기초부터 시작하기 - tuple에서는 리스트와 비슷하지만 다른 자료구조 '튜플(tuple)'에 대해 살펴 보도록 하겠다.

부록 1. 같이 보면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!