파이썬에서 굉장히 자주 다루게 되는 자료형 중 List에 대해 알아보자.
Q. List?
하나의 자료형. 리스트는 하나의 객체로 다루어지며, 하나의 객체안에 여러개의 메모리 주소를 갖고 있다. 각각의 메모리 주소에 해당하는 메모리에는 리스트의 원소의 데이터가 담겨있다.
Empty List
[] 즉 아무것도 원소르 갖지 않은 리스트를 Empty List 라고 말한다.
Index
리스트에 담겨있는 원소들은 각각 번호를 갖고 있다. 왼쪽 괄호 바로 뒤부터 0번째, 1번째, ... , n번째 순이다. 가장 먼저 오는 원소의 인덱스가 0인 것에 유의하자!
이때, 리스트의 원소의 개수의 범위를 벗어나는 인덱스를 입력하여 리스트의 원소를 호출할 경우, IndexError가 발생한다.
다음의 예를 보자.
>>> series = [9,8,7,6,5,4,3,2,1]
>>> series[0]
9
>>> series[3]
6
>>> series[11]
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
series[11]
IndexError: list index out of range
이 코드블럭에서 볼 수 있듯, 가장 첫번째 원소의 인덱스는 0이다. 또한 리스트의 원소 개수를 벗어나는 범위의 인덱스를 사용해 원소를 호출하면 당연히 IndexError가 발생한다. (원소의 개수를 벗어난 인덱스에 대한 정보를 해당 리스트가 갖고있지 않기 때문이다.)
*음수 인덱스
음수 인덱스를 생각해보자.
앞에서부터 차례대로 0,1,2,...,n이라는 인덱스를 n+1개의 원소에 할당했다고 하자.
그럼 뒤에서부터 인덱스를 생각해볼 수는 없을까? 이와 같은 방식의 접근은 때때로 유용하다.
따라서 파이썬에서는 뒤에서 부터 인덱스를 센다는 의미에서 음수 인덱스를 지원한다.
-1의 경우 가장 마지막의 원소를 가리키는 인덱스이며, -2의 경우 뒤에서 두번째의 원소, -m의 경우 뒤에서 m번째의 원소를 가리킨다. 이때 m은 -(n+1)보다 작을 수 없다. 이보다 작은 m을 인덱스로 하여 원소를 불러오려하면 IndexError가 발생한다. 이유는 양수 인덱스에서와 동일하다.
아래의 예시를 보자.
>>> series = [9,8,7,6,5,4,3,2,1]
>>> series[-1]
1
>>> series[-3]
3
>>> series[-11]
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
series[-11]
IndexError: list index out of range
List에는 어떤 데이터들을 담을 수 있는가?
파이썬의 리스트는 어떤 종류의 데이터타입을 가진 데이터이든 모두 담을 수 있다. 또한, 서로 다른 종류의 데이터타입을 가진 데이터를 함께 담을 수 있다. (이는 파이썬의 굉장히 큰 특징 중 하나이다.)
아래의 예시를 한번 보자.
>>> anything = [1,'hello',True,3.141592]
>>> anything[0]
1
>>> anything[1]
'hello'
>>> anything[-2]
True
>>> anything[-1]
3.141592
List 원소를 수정하는 방법 - Lists are mutable
여러경우에 리스트에 이미 담긴 원소를 수정해야될 필요가 있다.
그럴 때에는 인덱스를 통해 우리가 지금까지 다뤄온 변수들처럼 수정해줄 수 있다.
아래의 코드를 보자.
>>> anything = [1,'hello',True,3.141592]
>>> anything[0] = False
>>> anything
[False, 'hello', True, 3.141592]
>>> anything[1] = 12012
>>> anything
[False, 12012, True, 3.141592]
이와 같은 동작이 가능한 이유는 다음과 같다. 기본적으로 리스트는 담고있는 데이터 그 자체를 메모리에 저장하는 것이 아니라, 담고있는 데이터가 존재하는 메모리의 위치를 저장하고 있다. 따라서, 우리가 대입연산자를 통해 값을 덮어쓴다면(이때, 담고있는 데이터의 자료형이 변해도 아무런 상관이 없다.) 덮어쓴 값이 존재하는 메모리의 위치로 기존의 메모리 위치 정보를 새롭게 갱신한다.
위와 같은 특징을 영어로 'Lists are mutable' 이라 표현한다.
이 특징은 Strings 자료형과 굉장히 다른 특징을 보여준다.
String의 경우 중간의 원소를 대입연산자를 통해 다른 값으로 덮어쓸 수 없다!
아래의 코드를 보자.
>>> word1 = ['P','y','t','h','o','n']
>>> word2 = 'Python'
>>> word1
['P', 'y', 't', 'h', 'o', 'n']
>>> word2
'Python'
>>> word1[0]
'P'
>>> word1[0] = 'z'
>>> word1
['z', 'y', 't', 'h', 'o', 'n']
>>> word2[0]
'P'
>>> word2[0] = 'z'
Traceback (most recent call last):
File "<pyshell#34>", line 1, in <module>
word2[0] = 'z'
TypeError: 'str' object does not support item assignment
이 코드에서와 같이 리스트에서는 각각의 원소를 대입연산자를 통해 덮어쓸 수 있지만, 문자열의 경우 각각의 원소를 대입연산자를 통해 덮어쓸 수 없다!
문자열의 경우 문자열 전체를 다시 써주어야 한다.
>>> word1 = ['P','y','t','h','o','n']
>>> word2 = 'Python'
>>> word1
['P', 'y', 't', 'h', 'o', 'n']
>>> word2
'Python'
>>> word[1]
Traceback (most recent call last):
File "<pyshell#29>", line 1, in <module>
word[1]
NameError: name 'word' is not defined
>>> word1[0]
'P'
>>> word1[0] = 'z'
>>> word1
['z', 'y', 't', 'h', 'o', 'n']
>>> word2[0]
'P'
>>> word2 = 'zython'
>>> word2
'zython'
그렇다면 이전 포스트에서 다룬 문자열 메소드의 경우, 문자열 값을 덮어쓴 것일까?
Python-12. Str Class의 메소드 종류와 사용법 / find / count / strip / replace / lower / upper / etc. / method Nesting
Python-10 Methods란? / 메소드 / 클래스 / 모듈과 클래스의 차이 Q. Class? 이전의 포스트에서 여러가지 자료형들을 설명하였다. 파이썬에서는 이 자료형들을 어떻게 내부적으로 구현하고 있을까? 파이
dot-learning.tistory.com
정답은 아니다 이다.
문자열의 값을 변경하는 메소드의 경우 메소드를 통해 값을 덮어쓴 것과 같은 결과를 보이기는 하지만, 그 과정이 다르다.
문자열 값을 변경하는 메소드는 기존의 문자열 대신 메소드의 목적에 따른 새로운 문자열을 생성하고 생성된 문자열을 그대로 기존의 문자열 변수에 다시 저장하는 것이다. 아래의 예시를 통해 보다 더 그 차이를 확실히 알 수 있다.
*이 점을 헷갈린다면 이로 인해 굉장히 많은 버그를 경험하게 될 것이다!
>>> name = 'Darwin'
>>> name.upper()
'DARWIN'
>>> name
'Darwin'
>>> capitalized = name.upper()
>>> capitalized
'DARWIN'
>>> name
'Darwin'
Operation on lists
리스트 타입을 다루는 연산자들이 파이썬 내부함수로 미리 구현되어 있다. 어떤 종류들이 있을까?
len(lists)
리스트의 원소의 수를 반환한다. 아래의 코드를 보자.
>>> a = [5,1,7,2,8,9]
>>> len(a)
6
max(lists)
리스트의 원소 중 가장 큰 값을 반환한다. 아래의 코드를 보자.
>>> a = [5,1,7,2,8,9]
>>> max(a)
9
min(lists)
리스트의 원소 중 가장 작은 값을 반환한다. 아래의 코드를 보자.
>>> a = [5,1,7,2,8,9]
>>> min(a)
1
sum(lists)
리스트의 원소들의 합을 반환한다. 아래의 코드를 보자.
>>> a = [5,1,7,2,8,9]
>>> sum(a)
32
sorted(lists)
리스트의 원소들을 정렬한 리스트를 반환한다. 아래의 코드를 보자.
>>> a = [5,1,7,2,8,9]
>>> sorted(a)
[1, 2, 5, 7, 8, 9]
>>> a
[5, 1, 7, 2, 8, 9]
이때, 주의해야하는 점은 원래의 리스트가 변경되는 것이 아니라는 점이다. 변경된 리스트를 새롭게 반환하는 것이다.
위의 코드에서도 리스트 a가 변하지 않았음을 확인할 수 있다.
만약 내림차순으로 정렬한 리스트를 반환받고 싶다면, 하나의 파라메터를 추가해(reverse = True) 그렇게 할 수 있다.
>>> a = [5,1,7,2,8,9]
>>> sorted(a,reverse = True)
[9, 8, 7, 5, 2, 1]
+
리스트들을 합치는 오퍼레이터이다. 아래의 코드를 보자.
>>> a1 = [1,1,1]
>>> a2 = [2,2]
>>> a1 + a2
[1, 1, 1, 2, 2]
이처럼 리스트 + 리스트 는 +기준 왼쪽의 리스트가 먼저, 오른쪽의 리스트가 나중의 순서로 합해진다.
이때 주의해야하는 점은 반드시 리스트끼리만 더해야 한다는 점이다. 하나의 원소를 추가할 때 뒤에 나올 append메소드를 제외한 방법은 그 원소를 하나의 원소를 갖는 리스트로 선언해 리스트의 합을 이용하는 것이다.
*
리스트들을 곱하는 횟수만큼 연속해서 더해주는 오퍼레이터이다. 아래의 코드를 보자.
>>> test = ['A','B']
>>> test * 3
['A', 'B', 'A', 'B', 'A', 'B']
del
해당하는 인덱스의 원소를 삭제하는 오퍼레이터이다. 아래의 코드를 보자.
>>> a = [5,1,7,2,8,9]
>>> del a[0]
>>> a
[1, 7, 2, 8, 9]
그러나 del 연산자는 가급적 사용을 자제하는 것이 좋다. del연산자의 경우 원소를 삭제하면서 뒤의 인덱스를 하나씩 끌어오는 결과를 가져온다. 인덱스가 가리키는 원소가 계속해서 변화한다면 이를 감안하면서 프로그래밍하는 것이 어려워진다. 이로인해, 프로그래밍된 논리에 오류가 발생할 가능성이 높다. 다시말해 버그가 발생할 가능성이 커진다!
in
특정 데이터(반드시 원소의 자료형이어야 한다.) 가 리스트 안에 포함되어 있는지 확인하고, 있다면 True를 없다면 False를 (Boolean) 반환하는 연산자이다.
아래의 코드를 보자.
>>> alphabet = ['a','b','c']
>>> 'a' in alphabet
True
>>> 'd' in alphabet
False
이를 조건문과 결합하여 활용할 수 있다.
아래의 실수를 조심하자. 리스트 안에서 리스트를 찾을 수는 없다!
>>> 'a' in alphabet
True
>>> ['a'] in alphabet
False
또한, 문자열에서 작용하는 in연산자의 작동과 리스트에 작용하는 in연산자의 작동은 차이가 존재한다.
>>> test1 = 'python'
>>> test2 = ['py','thon']
>>> 'py' in test1
True
>>> 'yt' in test1
True
>>> 'py' in test2
True
>>> 'yt' in test2
False
Slicing lists
문자열과 마찬가지로 리스트도 슬라이싱하여 사용할 수 있다.
아래의 코드를 보자.
>>> hello = ['hi','ni hao','Bonjour','안녕']
>>> hello
['hi', 'ni hao', 'Bonjour', '안녕']
>>> foreign_hello = hello [0:3]
>>> foreign_hello
['hi', 'ni hao', 'Bonjour']
주의할 점은 슬라이싱 인덱스의 마지막을 가리키는 인덱스의 원소는 포함되지 않는다는 것이다.
슬라이싱 인덱스의 경우를 다음과 같이 나눌 수 있다.
[:n]
n-1번째 인덱스의 원소까지 전부
[m:]
m번째 인덱스의 원소부터 끝까지 전부
[::2]
0번째부터 마지막 인덱스까지 2씩 띄워가면서 전부!
즉 인덱스0, 2, 4, 6, ... 의 인덱스의 원소들을 전부
[::-1]
마지막 원소부터 첫번째 원소까지 전부! 즉 리스트의 배열순서를 뒤집는 슬라이싱방법이다.
Copying lists
우선, 대입연산자를 통해 리스트를 복사(Copying Lists)한다고 생각해보자.
새로운 리스트 = 원래리스트[:]의 문법으로 리스트를 복사할 수 있다. 아래의 코드를 보자.
>>> A = ["a","b","c"]
>>> B = A[:]
>>> A
['a', 'b', 'c']
>>> B
['a', 'b', 'c']
이 경우, 새롭게 만들어지는 리스트는 원래 리스트가 갖고있던 정보 즉, 원소들의 메모리 정보를 가져오게 된다.
다시말해, 복사된 리스트와 원래의 리스트는 메모리의 주소를 가지고 있을 뿐, 메모리에 담겨있는 데이터를 직접 저장하고 있지 않다는 것을 알 수 있다.
또, 복사된 리스트는 원래의 리스트와 같이 원소들이 위치한 메모리의 주소를 복사하고, 그 주소를 저장한다는 것을 알 수 있다.
그렇다면, 원래의 리스트가 수정되었다면 어떨까?
원래의 리스트의 인덱스5에 해당되는 원소의 값을 수정하였다면, 파이썬은 원래의 원소가 저장되어있는 메모리의 위치에 새로운 데이터를 덮어쓰는 것이 아니라, 새로운 메모리 위치에 수정된 값을 저장하고, 인덱스5에는 새로운 메모리 위치의 주소를 저장한다.
따라서, 원래의 리스트를 수정한다고 해서 복사된 리스트의 값에는 영향을 주지 않는다! 반대의 경우도 마찬가지이다.
아래의 코드에서 그 점을 확인할 수 있다.
>>> A = ["a","b","c"]
>>> B = A[:]
>>> A
['a', 'b', 'c']
>>> B
['a', 'b', 'c']
>>> A[2] = 'd'
>>> A
['a', 'b', 'd']
>>> B
['a', 'b', 'c']
Aliasing
그런데, 파이썬의 리스트에는 Copying과 비슷한 Aliasing라는 기능이 있다.
새로운 리스트 = 원래의 리스트 라는 문법으로 이를 사용할 수 있다. 아래의 코드를 보자.
>>> A = ["a","b","c"]
>>> B = A
>>> A
['a', 'b', 'c']
>>> B
['a', 'b', 'c']
그렇다면 Aliasing은 Copying과 어떤 점이 다를까?
Aliasing된 새로운 리스트는 변수명만 다를뿐 같은 메모리 주소를 갖는다.
아래의 그림을 보자.
이를 코드로 한번 더 확인해보자.
>>> A = ["a","b","c"]
>>> B = A
>>> A
['a', 'b', 'c']
>>> B
['a', 'b', 'c']
>>> A[2] = 'd'
>>> A
['a', 'b', 'd']
>>> B
['a', 'b', 'd']
같은 메모리 주소를 갖기 때문에, 원래의 리스트를 변경한다면 Aliasing된 리스트의 원소도 변경된다. 역의 경우도 같다.
함수와 리스트 / Copying과 Aliasing의 차이를 활용하는 법
함수의 파라메터로 리스트를 넣을 때, 함수는 리스트를 Copying하지 않고 Aliasing해서 가져온다.
즉, 함수 안에서 입력 파라메터로 받은 리스트를 수정한다면, 함수 바깥의 원래 리스트 역시 변경된다는 의미이다.
이를 이용해서 프로그밍할 수도 있고, 이것을 막기 위해 함수에 넣을 리스트를 미리 Copying해서 파라메터로 넣을 수 있다.
아래의 코드를 보고 함수가 리스트에 어떻게 작용하는지 알아보자.
>>> A
['a', 'b', 'c']
>>> def remove_last_item(L):
del L[-1]
return L
>>> A
['a', 'b', 'c']
>>> res = remove_last_item(A)
>>> res
['a', 'b']
>>> A
['a', 'b']
List methods
리스트도 하나의 자료형이고, 클래스이다. 따라서 리스트 역시 메소드를 갖는다. 이제 어떤 메소드가 있는지 알아보자.
L.append(v)
리스트 L에 값v를 마지막 원소로 첨가한다.
L.clear()
리스트 L을 Empty list로 비워준다.
L.count(v)
리스트 L에 값v가 몇개 존재하는지를 int형으로 반환한다.
L.extend(v)
리스트 L에 리스트 v를 병합한다. 이때, 결과적으로는 리스트 L의 가장 뒷부분에 v의 원소들이 순서대로 추가되는 것이다. +연산자와 비슷한 역할을 한다. (작동 과정은 다르다. extend는 메소드이고 +는 operator이기 때문이다. 다시말해, extend는 병합의 결과물을 새로운 리스트로 반환하는 것이 아니라, 기존의 리스트를 수정하는 것이다. +는 병합의 결과물을 새로운 리스트로 반환한다.)
L.index(v)
리스트 L에 값v가 있는지 확인하고, 있다면 몇번째 인덱스에 존재하는지 반환한다.
이때, 값 v가 존재하지 않는다면 에러가 발생한다..!
L.index(v,beg)
리스트 L의 beg번째 인덱스와 그 이후부터 값v가 있는지 확인하고 있다면 몇번째 인덱스에 존재하는지 반환한다.
이때 값v가 존재하지 않는다면 에러가 발생한다..!
L.index(v,beg,end)
리스트 L의 beg번째 인덱스와 그 이후부터 end번째 이전 인덱스까지 값v가 있는지 확인하고 있다면 몇번째 인덱스에 존재하는지 반환한다.
이때 값v가 존재하지 않는다면 에러가 발생한다..!
L.insert(i,v)
리스트 L의 i번째 인덱스에 값v를 집어넣는다. 기존의 i번째 포함 그 이후의 원소들은 모두 인덱스가 1씩 증가한다.
L.pop()
리스트 L의 마지막 원소를 반환하면서, 리스트 L의 마지막 원소를 삭제한다.
L.remove(v)
리스트 L에서 값v를 찾아서 삭제한다.
L.sort()
리스트 L의 원소들을 오름차순으로 정렬한다. 이때, 연산자 sorted와 다른점은 sorted는 정렬된 리스트를 새롭게 만들고 그 리스트를 반환하는 것이지만, 메소드 sort의 경우 기존의 리스트를 변경하는 것이라는 점이 다르다! (객체와 메소드의 의미를 생각해보자!)
L.sort(reverse = True)
리스트 L의 원소들을 내림차순으로 정렬한다. 이때, 연산자 sorted와 다른점은 sorted는 정렬된 리스트를 새롭게 만들고 그 리스트를 반환하는 것이지만, 메소드 sort의 경우 기존의 리스트를 변경하는 것이라는 점이 다르다! (객체와 메소드의 의미를 생각해보자!)
L.reverse()
리스트의 원소들의 순서를 정반대로 다시 배열한다.
중첩리스트
리스트는 리스트를 원소로 가질 수 있다.
이 그림은 리스트 안의 리스트가 어떤 형태로 구현되어 있는지를 잘 나타낸다.
이때, 인덱스를 표시하는 방법은, 가장 상위의 리스트에서의 인덱스를 첫번째, 그 아래의 리스트에서의 인덱스를 두번째, 그 아래의 리스트에서의 인덱스를 세번째,... 의 형식으로 표시한다.
아래의 코드를 보자.
>>> test = [['a','b','c'],[1,2,3,4,5]]
>>> test[0][1]
'b'
>>> test[1][3]
4
>>> test[0]
['a', 'b', 'c']
>>> test[1]
[1, 2, 3, 4, 5]
중첩리스트의 Aliasing
우선 아래의 코드를 보자.
>>> life = [['canada',76.5],['united states',75.5],['mexico',72.0]]
>>> canada = life[0]
>>> canada
['canada', 76.5]
>>> life
[['canada', 76.5], ['united states', 75.5], ['mexico', 72.0]]
>>> canada[1] = 80.0
>>> canada
['canada', 80.0]
>>> life
[['canada', 80.0], ['united states', 75.5], ['mexico', 72.0]]
이처럼, 상위 리스트에서 원소로 존재하는 리스트를 가리키는 인덱스만 사용한다면, Copying이 아닌 Aliasing이 일어난다는 점을 확인할 수 있다. 메모리 구조로 표현하면 아래의 그림과 같다.
이를 피해가고 싶다면, 상위 리스트에서 원소로 존재하는 리스트를 가리키는 인덱스를 적고, 그 뒤에 [:]를 통해 Copying하겠다는 점을 명시해주면 된다!