파이썬을 배워보자 4일차 - 리스트

0

Python

목록 보기
4/18

점프 투 파이썬 : https://wikidocs.net/book/1
파이썬 기본을 갈고 닦자 : https://wikidocs.net/16031

리스트

다른 언어에서 사용하는 배열과 비슷한 역할을 하는 것이 바로 리스트이다. 리스트의 구현은 매우 신박하다. c언어의 포인터들로 묶여서 굉장히 자유롭고, 이게 되?? 할만한 것들이 된다. 즉, 배열과 링크드 리스트들이 가진 한계점들을 부순 구현이다.

그러나, 치명적인 단점은 느리다. 매우 느리다. 이걸로 시스템을 만들거나 플랫폼을 만든다면 별로 시스템 성능이 중요하지 않은 프로그램을 만들 때만 사용한다.

이러한 치명적인 단점을 해결하기위해 numpy에서는 c언어로 구현된 배열을 사용한다. 사실 내부적으로는 완벽히 c로 구현되었으며 그냥 인터페이스만 python인 것이다.

하지만 리스트가 주는 편의성을 고려한다면 성능과도 맞바꿀만큼 매력적인 것들이 많다. 지금부터 자세히 알아보자

1. 리스트란

  • 리스트는 순서가 있는 수정가능한 객체의 집합이다. 즉, 문자열처럼 불변성을 갖지 않는다.
  • 수정, 삭제, 추가가 가능하다.
  • list는 []로 표시되며, 내부 원소는 ,로 구분한다.
  • list안의 원소는 한 가지 타입만이 아니라 다양한 타입들이 들어갈 수 있다.
a = [] # 빈 리스트 생성
a = list() # 빈 리스트 생성
b = [1, 2, 3] # 값을 미리 선언
c = ['life', 'is' , 'gone'] # 문자열도 가능
d = [1, 2.3, 'life', 'is', False] # 다양한 타입들이 하나의 리스트에 있을 수 있다. c/java에서는 불가능하다.
e = [1, 2, ['life', 'is']] # 리스트 안에 리스트도 넣을 수 있다. 

2. 리스트의 인덱싱

문자열처럼 리스트도 인덱스로 접근이 가능하다. 리스트 역시도 음수 인덱스(-값)으로 접근가능하다.

a = [1,2,3,4,5]
print(a[0], a[4], a[-2] , a[-3]) # 1 5 4 3

그러나 문자열은 불변인 반면에 리스트는 가변이다. 즉, 수정 가능하다. 따라서 특정 원소의 값을 바꿀 수 있다.

a = [1,2,3,4,5]
a[2] = 100
print(a) # [1, 2, 100, 4, 5]

이중으로 된 리스트에서 값을 넣을 때는 다음과 같이하면 된다.

a = [1, 2, [3 ,4 ,5]]
print(a[-1][0], a[-1][1], a[-1][2]) # 3 4 5

다음과 같이 a[i][j]이렇게 쓰면 된다.

3. 리스트의 슬라이싱

문자열 슬라이싱과 똑같다.

b = a[start: end: step]

start부터 end-1까지 슬라이스하고 step만큼 건너뛴다. 기본적으로 step은 1이라서 start, start+1 ,start+2 , ... , end-1으로 슬라이싱되는데, step=2로 해두면 start, start+2, start+4, ... end-1로 된다. 만약 start+2n이 end-1보다 크면 end-1은 안들르고 넘어간다.

중요한 것은 슬라이싱은 해당 리스트를 슬라이싱하여 새로운 리스트를 만드는 것이지 원본 리스트를 변형시키는 것은 아니다. 이러한 특성은 python에 있어 매우 중요하다.

a[:] # 전체 리스트를 복사
a[start:] # start부터 끝까지 리스트를 복사
a[:end] # 시작부터 end-1까지 리스트를 복사
a[start:end] # start부터 end-1까지 리스트를 복사
a[start: end : step] # start부터 end-1까지 리스트를 복사하되 start가 커지는 값은 step만큼이다.
리스트 :  1  2   3   4   5
순방향 :  0  1   2   3   4
역방향 : -5 -4  -3  -2  -1
a = [1, 2, 3, 4, 5]
print(a) # [1, 2, 3, 4, 5]
print(a[:]) # [1, 2, 3, 4, 5]
print(a[1:]) # [2, 3, 4, 5]
print(a[:3]) # [1, 2, 3]
print(a[1:4]) # [2, 3, 4]
print(a[1:-1]) # [2, 3, 4]
print(a[1::2]) # [2, 4]

다음의 예제를 살펴보면 된다.

3.1 슬라이싱을 통한 리스트 복사

리스트와 문자열이 다른 점은 리스트는 가변이고, 문자열은 불변이다. 따라서, 리스트는 인덱스를 통해 원소에 접근하여 값을 바꿀 수 있다.

이와 같은 특성 때문에 발생하는 문제가 있는데, 바로 다음이다.

a = [1,2,3,4,5]
b = a 
b[2] =100
print(a) # [1, 2, 100, 4, 5]

재밌게도, b라는 변수에 a리스트를 넣고 b의 원소를 100으로 바꾸었는데 a에도 영향을 미친다. 이와 같은 일이 왜 발생했을까??

아주 단순한 이유인데, C/C++ 개발을 하신 분들은 당연하지. 라고 말씀하실 것이다. 사실 같은 이유이다.

python의 list를 대입 연산자를 통해 할당시키면 이는 해당 list에 대한 참조(포인터)가 넘어가는 것이다. 때문에 b는 참조 변수를 통해 리스트 값들을 바꿀 수 있고, 이는 a가 참조하고 있는 리스트를 변경하는 것이기 때문에 a에도 영향이 있을 수 밖에 없다.

따라서, python에서 많은 분들이 얉은 복사, 깊은 복사로 스트레스를 받는 것이다. 위와 같은 경우가 얉은 복사(참조 변수만 넘어간 상황)인데, 이를 해결할 수 있는 것이 바로 '슬라이스' 이다.

이유는 아주 단순하다. '슬라이스'는 슬라이싱하여 새로운 리스트를 만든다고 했다. 이건 문자열도 마찬가지였다.

이러한 특성을 이용하여 '슬라이싱'을 통해 새로운 리스트를 할당해주는 것이다.

a = [1,2,3,4,5]
b = a[:] 
b[2] =100
print(a) # [1, 2, 3, 4, 5]
print(b) # [1, 2, 100, 4, 5]

b = a[:]이 부분이 조금 낯설을 수 있는데, 사실 위의 슬라이싱 예제와 똑같다. 그저 start~end-1까지의 원소들을 채운 새로운 리스트를 반환한 것 뿐이다. 이제 a, b 둘이 서로 다른 리스트를 참조하고 있으니 b의 원소를 하나 변경해도 a에 영향이 가지 않는 것이다.

이렇게 슬라이싱을 사용하여 copy하게되면 그냥 for문으로 copy하는 것보다 훨씬 더 속도도 빠르고 파이써닉한 코드를 만들 수 있다.

물론 내부에도 만약 리스트가 있다면, 이는 또 내부에 슬라이싱[:]을 해주어 복사해주어야 하므로 매우 귀찮은 일이다. 따라서, 리스트 내부의 객체마저도 새롭게 만들어 새로운 리스트를 반환해주는 deepcopy라는 것을 이용해주면 된다. 이는 추후에 따로 배우도록 하자.

4. 리스트 연산

4.1 +,* 기호를 통한 리스트 연산

리스트는 자료형이기 떄문에 +, * 기호를 사용할 수 있다.

a = [1, 2, 3]
b = [4, 5, 6]
print(a + b) # [1, 2, 3, 4, 5, 6]
print(a * 2) # [1, 2, 3, 1, 2, 3]

4.2 리스트의 길이 구하기

리스트의 길이는 곧 원소의 개수이다. 이는 len()을 통하여 구할 수 있다.

a = [1, 2, 3]
b = [4, 5, 6]
print(len(a + b)) # 6

4.3 리스트에서 요소 삭제하기

del 원소로 요소를 삭제할 수 있다. 참고로 원소말고 슬라이스를 써도 된다. 이외에도 pop과 remove가 있는데 이는 뒤에 설명한다.

a = [1, 2, 3]
b = [4, 5, 6]
del a[1]
del b[:2]
print(a) # [1, 3]
print(b) # [6]

5. 리스트 관련 함수들

리스트 역시도 메서드를 갖고 있는데, 굉장히 자주 사용하므로 중요하다.

5.1 리스트에 요소 추가(append)

list.append(원소)로 list에 원소를 추가할 수 있으며, 맨 마지막에 원소가 추가된다.

a = [1, 2, 3]
a.append(4)
print(a) # [1,2,3,4]

5.2 리스트의 정렬(sort)

sort()라는 메서드가 있고, sorted()라는 함수가 있는데, sort()list.sort()로 호출하지만 sortedsorted(list)로 호출하여 새롭게 정렬된 리스트를 반환한다. 즉, sort는 원본 리스트를 정렬하는 것이고, sorted는 원본 리스트는 가만히 두고, 정렬된 새로운 리스트를 반환하는 것이다.

a = [1,5,2,10,42,9,12,45,990,-1,2,0]
b = sorted(a)
print(a.sort()) # None
print(a) # [-1, 0, 1, 2, 2, 5, 9, 10, 12, 42, 45, 990]
print(b) # [-1, 0, 1, 2, 2, 5, 9, 10, 12, 42, 45, 990]

None이 나오는 이유는 a.sort()는 반환값이 없고, 원본 리스트인 a의 원소들을 정렬하기 때문이다.

그렇다면 반대로 정렬하고 싶으면 어떻게 할까?? 또, 정렬 기준을 다르게 바꾸고 싶다면 어떻게할까?

리스트는 두 가지 매개변수를 갖는데 하나는 key로 어떤 값을 기준으로 정렬할 것인지를 확인하고, 하나는 오름 또는 내림차순에 관한 것으로 reverse이다. 기본적으로 오름차순으로 False이다. True로 바꾸어 주면 내림차순이 된다.

  • reverse를 True로 주었을 때
a = [1,5,2,10,42,9,12,45,990,-1,2,0]
b = sorted(a)
print(a.sort(reverse=True)) # None
print(a) # [990, 45, 42, 12, 10, 9, 5, 2, 2, 1, 0, -1]
print(b) # [-1, 0, 1, 2, 2, 5, 9, 10, 12, 42, 45, 990]

a가 sort되었을 때 내림차순으로 정렬되는 것을 볼 수 있다.

  • key를 달리주었을 때
def ciriteria(x):
    return x[1], x[0]

a = [[1,2] , [3,5] , [-1,2], [0,3], [100,32], [5,3] , [3,3] , [1,-1]]
a.sort(key=ciriteria)
print(a) # [[1, -1], [-1, 2], [1, 2], [0, 3], [3, 3], [5, 3], [3, 5], [100, 32]]

key에는 함수가 들어간다. 그래서 위에 criteria라는 함수를 만들어주었다. 함수는 뒤에 설명하므로 그냥 그런게 있구나 싶으면 된다. 여기서 함수의 return되는 부분이 바로 정렬하려는 기준이 되는 것이다. 입력으로 들어온 것은 a 리스트이며 a리스트 원소는 리스트이다. 원소 리스트는 두 가지 값으로 이루어져 있는데, 리스트 1번째 요소를 오름차순으로 정렬하고(x[1]), 같은 경우에는 0번째 요소를 기준으로 오름차순 정렬(x[0])하는 것이다.

따라서 정렬된 값을 보면, 두 번째 요소들이 점점 커지는 오름차순이며, 같은 경우에는 첫 번째 요소들의 값을 기준으로 오름차순 한 것을 확인할 수 있다. ex) [3,3] , [5,3]

sort, sorted는 내부적으로 팀소트를 사용하여 최선일 때 O(n), 최악 O(nlogn), 평균 O(nlogn) 시간을 갖는다. 팀소트는 병합 정렬과 선택 정렬을 합친 것으로 거의 정렬이 되어있을 때는 속도의 이득을 볼 수 있다.

5.3 리스트 뒤집기(reverse)

reverse메서드는 리스트를 역순으로 뒤집어 준다. 이 때 리스트 요소들을 순서대로 정렬한 다음 다시 역순으로 정렬하는 것이 아니라, 그저 현재 리스트를 그대로 거꾸로 뒤집는다.

a = ['a' , 'c' , 'b']
a.reverse()
print(a) # ['b', 'c', 'a']

5.4 위치 반환(index)

index(x) 메서드는 리스트에 x 값이 있으면 x의 위치 값을 돌려준다.

a = [ 1, 2, 3]
indexVal = a.index(3)
print(indexVal) # 2
print(a.index(-1)) # ValueError: -1 is not in list

없는 값을 찾으면 에러를 일으키니 조심하도록 하자

5.5 리스트 요소 삽입(insert)

insert(a, b)는 리스트 a번째 위치에 b를 삽입하는 것으로 a는 인덱스 번호이다. 즉, 0부터 시작한다는 것을 잊지말자

a = [ 1, 2, 3, 4, 5]
a.insert(1,10)
a.insert(-1,100)
print(a) # [1, 10, 2, 3, 4, 100, 5]

음수 인덱스로 접근 시, 오른쪽으로부터 오는 방향이므로 해당 인덱스 위치 앞에 넣는다는 것을 잊지말자.

5.6 리스트 요소 제거(remove) , (pop)

remove(x)는 리스트에서 첫 번째로 나오는 x를 삭제하는 함수이다. 여러 개 있을 때 모두 삭제하는 것은 아니고, 한 개만 삭제한다.

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

없는 값을 삭제하려고하면 ValueError: list.remove(x): x not in list에러를 발생시키니 조심하도록 하자

pop은 리스트의 맨 마지막 요소를 돌려주고 그 요소를 삭제한다. 즉, remove와는 달리 반환값이 있다.

a = [ 1, 2, 3, 4, 5]
val = a.pop()
print(a) # [1, 2, 3, 4]
print(val) # 5

더 이상 pop할게 없으면 IndexError: pop from empty list 에러를 발생시키니 조심하자

5.7 리스트에 포함된 요소 x의 개수 세기(count)

count(x)는 리스트 안에 x가 몇 개있는 지 조사하여 그 개수를 돌려준다.

a = [ 1, 2, 1, 3, 4,1]
print(a.count(1)) # 3

없으면 0으로 나온다.

5.8 리스트 확장(extend)

extend(x)로 x는 리스트만 올 수 있어 a리스트에 x리스트를 더하게 된다. + 연산자랑 똑같다.

a = [ 1, 2, 3]
b = [4,5,6]
a.extend(b)
print(a) # [1, 2, 3, 4, 5, 6]

0개의 댓글