리스트(List)
리스트는 크기를 동적으로 조절할 수 있는 배열(array)을 말합니다. 파이썬에서 기본적으로 제공하는 데이터 타입으로 데이터의 삽입/삭제가 쉽고 인덱스를 통해 데이터를 조회하거나 탐색하기 편한 장점을 가지고 있습니다. 굵은 글씨에 밑줄까지 쳐 놓고 강조를 했는데, 리스트는 순서 및 내용이 비교적 자유롭게 바뀔 수 있는 가변(mutable) 자료형입니다.
목차
1. 리스트 선언
2. 가변형 객체(mutable)-리스트
3. 시퀀스 데이터 타입 속성-리스트
(1). 멤버십 연산 in
(2). 크기 함수 len(seq)
(3). 슬라이싱 속성 seq[:]
(4). 반복성(iterable)
4. 리스트 연산
(1) 더하기
(2) 곱하기
5. 리스트의 수정과 삭제
6. 리스트 메서드
(1) append( )
(2) extend( )
(3) insert( )
(4) remove( )
(5) pop( )
(6) index( )
(7) count( )
(8) sort( )
(9) reverse( )
7. 리스트 컴프리헨션
8. 리스트 메서드의 성능 측정
1. 리스트 선언
리스트는 항목을 쉼표로 구분하고, 대괄호 [ ]로 감싸서 그 객체를 만들 수 있습니다.
리스트의 항목으로는 어떠한 자료형이 와도 상관이 없습니다. 빈 리스트도 존재할 수 있습니다.
a = ['전영근', '윤영', '김현수', '명경규', '전현우'] b = [1, 3, 2, 7, 4] c = 'a', 'b', 'c' d = [a, b, c] empty_list = [ ] print(a, type(a)) print(b, type(b)) print(d, type(d)) print(empty_list, type(empty_list)) |
결과)
['전영근', '윤영', '김현수', '명경규', '전현우'] <class 'list'> [1, 3, 2, 7, 4] <class 'list'> [['전영근', '윤영', '김현수', '명경규', '전현우'], [1, 3, 2, 7, 4], ('a', 'b', 'c')] <class 'list'> [] <class 'list'> |
a는 문자열 객체들을 원소로 갖는 리스트, b는 정수를 원소로 갖는 리스트이며 d는 리스트 a, 리스트 b 및 문자열로 구성된 튜플 c를 원소로 갖는 리스트입니다.
리스트는 함수 list( )를 이용하여 다른 데이터 타입을 리스트를 만들 수 있습니다.
another_empty_list = list( ) new_tuple = 1, 2, 3 new_list = list(new_tuple) print(another_empty_list) print(new_list) |
결과)
[] [1, 2, 3] |
아래의 예제는 문자열의 한 문자씩을 원소로 갖는 리스트로 변환하는 코드입니다.
cat = 'cat' cat_list = list(cat) print(cat_list) |
결과)
['c', 'a', 't'] |
리스트는 어떠한 자료형이든 원소로 가질 수 있으므로 다른 리스트를 원소로 가질 수도 있습니다. 아래는 리스트 자체를 원소로 갖는 리스트를 만드는 코드입니다.
small_birds = ['hummingbird', 'finch'] extinct_birds = ['dodo', 'passenger pigeon', 'Norwegian Blue'] carol_birds = [3, 'French hens', 2, 'turtledoves'] all_birds = [small_birds, extinct_birds, 'macaw', carol_birds] print(all_birds) |
결과)
[['hummingbird', 'finch'], ['dodo', 'passenger pigeon', 'Norwegian Blue'], 'macaw', [3, 'French hens', 2, 'turtledoves']] |
이렇게 리스트를 원소로 갖는 리스트를 만들게 되면 사용자가 원할 때 해당 위치의 자료형을 뽑아낼 수 있다는 장점이 있습니다.
small_birds = ['hummingbird', 'finch'] extinct_birds = ['dodo', 'passenger pigeon', 'Norwegian Blue'] carol_birds = [3, 'French hens', 2, 'turtledoves'] all_birds = [small_birds, extinct_birds, 'macaw', carol_birds] print(all_birds[0]) print(all_birds[1]) print(all_birds[0][1]) |
결과)
['hummingbird', 'finch'] ['dodo', 'passenger pigeon', 'Norwegian Blue'] finch |
마지막 줄의 all_birds[0][1]은 all_birds[0]의 [1] 인덱싱을 말합니다. all_birds[0]는 small_birds(['hummingbird', 'finch'])이며 따라서 small_birds[1]은 'finch'가 되므로 이를 출력하는 것입니다.
2. 가변형 객체(mutable)-리스트
리스트는 가변(mutable) 타입입니다. 앞서 문자열과 튜플은 불변형(immutable) 객체라 했습니다. 먼저 불변형 객체의 특징을 확인해봅시다. 불변형 객체는 객체에 할당된 값을 수정할 수 없습니다. 변수에 객체가 할당된 뒤 객체에 할당된 값이 수정되면 새로운 객체가 만들어지고 다시 변수에 할당되는 식으로 움직입니다. id() 함수는 객체를 입력값으로 받아서 객체의 고유값(reference)을 반환하는 함수로, 파이썬이 객체를 구별하기 위해 부여하는 일련번호를 의미합니다. 즉, id() 함수는 동일한 객체인지를 판별할 때 사용합니다.
x = 'abc' y = 'abc' print(id(x)) print(id(y)) |
결과)
코드 시행마다 결과는 다르지만 id(x)와 id(y)는 항상 같은값임을 알 수 있습니다.
1794932340472 1794932340472 |
위의 코드에서 x와 y는 변수의 이름입니다. 'abc'라는 문자열 객체는 각각 x와 y라는 변수에 할당되었으며 각각 같은 고유값을 보여줍니다. 즉, x와 y는 동일한 객체가 됩니다.
x = 'abc' y = x print(id(x)) print(id(y)) x += 'de' print(x, id(x)) print(y, id(y)) |
결과)
1941077620472 1941077620472 abcde 1941078614568 abc 1941077620472 |
y = x와 같이 x를 y에 할당해도 동일한 고유값을 보입니다. 그러나 x에 문자열 'de'를 더하기 연산하여 x의 객체가 문자열 'abcde'가 되도록 연산을 진행하면 x의 고유값이 변하는 것을 확인할 수 있습니다. 이 때에도 y의 객체는 문자열 'abc'로 그 고유값 역시 변함이 없습니다. 이것이 불변형 객체의 특징입니다. 이를 두고 call by value로 동작되었다고 합니다.
다음으로 가변형 객체인 리스트에 동일한 코드를 적용해봅시다.
x = [1, 2, 3] y = x print(id(x)) print(id(y)) x += [4, 5] print(x, id(x)) print(y, id(y)) |
결과)
2013212795464 2013212795464 [1, 2, 3, 4, 5] 2013212795464 [1, 2, 3, 4, 5] 2013212795464 |
마찬가지로 y = x와 같이 x를 y에 할당해도 동일한 고유값을 보입니다. 그러나 다른 점은 x에 다른 리스트 [4, 5]를 더하기 연산하여 x의 객체가 리스트 [1, 2, 3, 4, 5]가 되도록 연산을 진행하였음에도 x의 고유값이 변하지 않았다는 것을 확인할 수 있습니다. 이 때 놀라운 것은 연산을 진행하지 않은 y의 객체 역시 [1, 2, 3, 4, 5]로 변하였으며 그 고유값 역시 같은 값을 가리키고 있습니다. 이것이 객체에 할당된 값을 변하더라도 같은 고유값을 보이는 가변형 객체의 특징입니다. 이를 두고 call by reference로 동작되었다고 합니다.
3. 시퀀스 데이터 타입 속성-리스트
(1). 멤버십 연산 in
x = ['전영근', '윤영', '김현수', '명경규'] print('윤영' in x) print('전현우' in x) |
결과)
True False |
(2). 크기 함수 len(seq)
x = ['전영근', '윤영', '김현수', '명경규', '전현우'] print(len(x)) |
결과)
5 |
(3). 슬라이싱 속성 seq[:]
리스트도 시퀀스 자료형이므로 인덱싱 및 슬라이싱이 가능합니다.
x = ['전영근', '윤영', '김현수', '명경규', '전현우'] print(x[0]) print(x[3]) |
결과)
전영근 명경규 |
x = ['전영근', '윤영', '김현수', '명경규', '전현우'] print(x[0:2]) print(x[3:-1]) print(x[2:-1]) |
결과)
['전영근', '윤영'] ['명경규'] ['김현수', '명경규'] |
x = ['전영근', '윤영', '김현수', '명경규', '전현우'] print(x[0:-1:2]) |
결과)
['전영근', '김현수'] |
리스트는 가변 자료형이기 때문에 앞서 언급했듯, 인덱싱을 이용한 자료 변화가 자유롭습니다.
x = ['전영근', '윤영', '김현수', '명경규', '전현우'] x[2] = '김태영' print(x) |
결과)
['전영근', '윤영', '김태영', '명경규', '전현우'] |
문자열은 인덱싱을 통한 특정 위치의 문자열 교체가 불가능합니다. 튜플도 마찬가지입니다. 이들은 불변 자료형이기 때문입니다. 오직 리스트만 가능하다는 것을 알고 있어야 합니다.
(4). 반복성(iterable)
x = ['전영근', '윤영', '김현수', '명경규', '전현우'] for i in x: print(i) |
결과)
전영근 윤영 김현수 명경규 전현우 |
이와 같이 리스트도 시퀀스 타입의 데이터이므로 이 타입의 데이터가 갖는 네 가지 속성을 사용할 수 있습니다.
4. 리스트 연산
(1) 더하기
x = ['전영근', '윤영'] y = ['김현수', '명경규', '전현우'] z = x + y print(z) |
결과)
['전영근', '윤영', '김현수', '명경규', '전현우'] |
(2) 곱하기
x = [1, 2] y = x * 3 print(y) |
결과)
[1, 2, 1, 2, 1, 2] |
5. 리스트의 수정과 삭제
리스트에서 값 수정하기 위해서는 앞에서 사용했던 인덱싱을 활용하면 됩니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] name_list[0] = '김현수' print(name_list) |
결과)
['김현수', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] |
del 선언을 사용해 리스트 내 특정 위치의 객체를 삭제할 수 있습니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] del name_list[1] print(name_list) |
결과)
['전영근', '김태영', '윤영', '강정규', '홍진우', '전현우'] |
remove( ) 메서드를 이용하면 리스트 내 특정 객체를 삭제할 수 있습니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] name_list.remove('김태영') print(name_list) |
결과)
['전영근', '이명현', '윤영', '강정규', '홍진우', '전현우'] |
6. 리스트 메서드
append( ), extend( ), insert( ), remove( ), pop( ), index( ), count( ), sort( ), reverse( )
(1) append( )
A.append(x): 리스트 A 끝에 항목 x를 추가(삽입)합니다. A[len(A):] = [x]와 동일합니다.
marxes = ['Groucho', 'Chico', 'Harpo'] print(id(marxes)) marxes.append('Zeppo') print(marxes) print(id(marxes)) |
결과)
2226033152392 ['Groucho', 'Chico', 'Harpo', 'Zeppo'] 2226033152392 |
id( ) 함수에 대한 출력은 결과값이 사람마다 다를 수 있지만 첫 번째 id( ) 함수의 출력값과 두 번째 리스트에 'Zeppo' 데이터를 삽입한 후의 id( ) 함수의 출력값이 같다는 부분에 주목해야 합니다.
(2) extend( )
A.extend(c): 반복 가능한(iterable) 모든 항목 c를 리스트 A에 추가합니다. A[len(A):] = c 또는 A += c와 동일합니다.
marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo'] others = ['Gummo', 'Karl'] marxes.extend(others) print(marxes) |
결과)
['Groucho', 'Chico', 'Harpo', 'Zeppo', 'Gummo', 'Karl'] |
또한+=를 이용해서 같은 결과를 얻을 수 있습니다.
marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo'] others = ['Gummo', 'Karl'] marxes += others print(marxes) |
결과)
['Groucho', 'Chico', 'Harpo', 'Zeppo', 'Gummo', 'Karl'] |
append( )를 사용하면 리스트 그 자체를 삽입하기 때문에 조금은 다른 결과를 얻게 됩니다.
marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo'] others = ['Gummo', 'Karl'] marxes.append(others) print(marxes) |
결과)
['Groucho', 'Chico', 'Harpo', 'Zeppo', ['Gummo', 'Karl']] |
(3) insert( )
A.insert(i, x): 리스트 A의 인덱스 위치 i에 항목 x를 삽입합니다.
marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo'] marxes.insert(3, 'Gummo') print(marxes) |
결과)
['Groucho', 'Chico', 'Harpo', 'Gummo', 'Zeppo'] |
marxes 리스트의 인덱스 3 위치에 'Gummo'가 삽입된 것을 확인할 수 있습니다. 리스트의 길이를 넘어가는 위치를 지정하면 append( )와 같이 가장 마지막에 객체가 삽입됩니다.
marxes = ['Groucho', 'Chico', 'Harpo', 'Zeppo'] marxes.insert(10, 'Karl') print(marxes) |
결과)
['Groucho', 'Chico', 'Harpo', 'Zeppo', 'Karl'] |
(4) remove( )
A.remove(x): 리스트 A의 항목 x를 제거합니다. 이때 x가 존재하지 않으면 ValueError 예외가 발생합니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] name_list.remove('김태영') print(name_list) |
결과)
['전영근', '이명현', '윤영', '강정규', '홍진우', '전현우'] |
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] name_list.remove('김현수') print(name_list) |
결과)
ValueError: list.remove(x): x not in list |
(5) pop( )
A.pop(x): 리스트 A에서 인덱스 x에 있는 항목을 제거하고 그 항목을 반환합니다. x를 지정하지 않으면, 리스트 맨 끝의 항목을 제거하고 그 항목을 반환합니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] name_list.pop(1) print(name_list) name_list.pop() print(name_list) |
결과)
['전영근', '김태영', '윤영', '강정규', '홍진우', '전현우'] ['전영근', '김태영', '윤영', '강정규', '홍진우'] |
혹시 자료구조를 조금 공부해 보신 분이라면 리스트가 append( ) 메서드와 pop( ) 메서드를 통해 자료의 삽입과 출력을 하는 것이 LIFO(last in, first out) queue로 알려진 자료구조와 같다는 것을 알 수 있습니다. 좀 더 널리 알려진 이름으로는 스택(stack)이 되겠습니다.
(6) index( )
A.index(x): 리스트 A에서 항목 x의 인덱스를 반환합니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] print(name_list.index('윤영')) |
결과)
3 |
(7) count( )
A.count(x): 리스트 A에 항목 x가 몇 개 들어 있는지, 그 개수를 반환합니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] print(name_list.count('전영근')) print(name_list.count('김현수')) snl_skit = ['cheeseburger', 'cheeseburger', 'chickenburger'] print(snl_skit.count('cheeseburger')) |
결과)
1 0 2 |
(8) sort( )
리스트 내 객체들을 일련의 기준으로 정렬할 필요가 있을 때가 있습니다. 파이썬에서는 두 가지 방법을 제공합니다.
- 리스트 메서드 sort( )는 리스트 그 자체 내에서 항목을 정렬합니다. 리스트의 id( ) 값은 변하지 않습니다.
- 함수 sorted( )는 인자로 받는 리스트의 항목을 정렬한 새로운 리스트를 반환합니다. 새로운 리스트이기 때문에 id( ) 값이 변하게 됩니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] sorted_name_list = sorted(name_list) print(sorted_name_list) print(id(name_list)) print(id(sorted_name_list)) |
결과)
['강정규', '김태영', '윤영', '이명현', '전영근', '전현우', '홍진우'] 2450373235080 2450373235592 |
name_list와 sorted_name_list의 id( ) 값이 다르다는 것에 주목하시면 됩니다.
A.sort(key, reverse)에 아무런 인수가 없으면 오름차순으로 정렬합니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] print(id(name_list)) name_list.sort( ) print(name_list) print(id(name_list)) |
결과)
2308581970312 ['강정규', '김태영', '윤영', '이명현', '전영근', '전현우', '홍진우'] 2308581970312 |
한글은 가나다 순으로 오름차순으로 정렬합니다.
인수를 지정할 때는 키워드 인수를 사용해야 합니다. 예를 들어 리스트의 항목을 내림차순으로 정렬하기 위해서는 sort(reverse=True) 형식으로 키워드 인수를 지정해야 합니다.
name_list = ['전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'] print(id(name_list)) name_list.sort(reverse=True ) print(name_list) print(id(name_list)) |
결과)
2720317460872 ['홍진우', '전현우', '전영근', '이명현', '윤영', '김태영', '강정규'] 2720317460872 |
Python에서는 인수의 순서와 용도를 매번 기억하지 않아도 되도록 키워드 인수(keyword argument)라는 기능을 제공합니다. 키워드 인수는 말 그대로 인수에 이름(키워드)을 붙이는 기능이며 키워드=값 형식으로 사용합니다.
(9) reverse( )
A.reverse( ): 리스트 A의 항목들을 반전시켜서 그 변수에 적용합니다. list[::-1]을 사용하는 것과 같습니다.
people = ["잰더", "페이스", "버피"] people.reverse() print(people) people = people[::-1] print(people) |
결과)
['버피', '페이스', '잰더'] ['잰더', '페이스', '버피'] |
7. 리스트 컴프리헨션
리스트 컴프리헨션(list comprehension)은 반복문의 표현식이며 조건문을 포함할 수도 있습니다. 형식은 다음과 같이 대괄호 [ ]로 묶어서 사용합니다.
- [ 항목 for 항목 in 반복 가능한 객체 ]
- [ 표현식 for 항목 in 반복 가능한 객체 ]
- [ 표현식 for 항목 in 반복 가능한 객체 if 조건문 ]
a = [y for y in range(1900, 1940) if y%4 == 0] print(a) |
결과)
[1900, 1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936] |
range(1900, 1940)은 1900부터 1939까지의 정수에 대한 반복 가능한 객체입니다. 즉, 1900부터 1939까지의 정수 모음이라고 생각하시면 됩니다. a는 y로 이루어진 리스트이며 이때 y는 1900부터 1939까지의 정수 모음에 속하는(in) 정수가 됩니다. 이때 뒤에 조건문 if에 의해 y%4 == 0, 즉, 4로 나누었을 때 나머지가 0인 수(4의 배수)들만 y가 됩니다. 결과적으로 a는 1900부터 1939 사이의 정수 중 4의 배수로 이루어진 리스트가 됩니다.
words = 'Buffy is awesome and a vampire slyaer'.split() e = [[w.upper(), w.lower(), len(w)] for w in words] print(e) |
결과)
[['BUFFY', 'buffy', 5], ['IS', 'is', 2], ['AWESOME', 'awesome', 7], ['AND', 'and', 3], ['A', 'a', 1], ['VAMPIRE', 'vampire', 7], ['SLYAER', 'slyaer', 6]] |
표현식이 [w.upper(), w.lower(), len(w)]이므로 문자열 w를 모두 대문자로 바꾼 문자열, 모두 소문자로 바꾼 문자열, 문자열 w의 길이를 각각 원소로 하는 리스트가 리스트 e의 원소가 됩니다. 이때 w는 words라는 리스트에서 취하게 되며, words는 'Buffy is awesome and a vampire slayer'라는 문자열을 공백으로 나눈(split()) 문자열들을 각각 원소로 갖는 리스트(['Buffy', 'is', 'awesome', 'and', 'a', 'vampire', 'slayer'])가 됩니다.
8. 리스트 메서드의 성능 측정
리스트의 메서드를 벤치마킹 테스트하여 성능을 측정할 수 있습니다. timeit 모듈의 Timer 객체를 생성해 사용하면 리스트의 메서드의 성능을 측정할 수 있습니다. Timer 객체의 첫 번째 매개변수는 우리가 측정하고자 하는 코드이며, 두 번째 매개변수는 테스트를 위한 설정문입니다. timeit 모듈은 명령문을 정해진 횟수만큼 실행하는 데 걸리는 시간을 측정하며 기본값은 number = 106입니다. 테스트가 완료되면, 문장이 수행된 시간(단위: ms)을 부동소수점 값으로 반환합니다.
def test1( ): l = [ ] for i in range(1000): l = l + [i] def test2( ): l = [ ] for i in range(1000): l.append(i) def test3( ): l = [i for i in range(1000)] def test4( ): l = list(range(1000)) if __name__=="__main__": import timeit t1 = timeit.Timer("test1( )", "from __main__ import test1") print('concat', t1.timeit(number=1000), 'ms') t2 = timeit.Timer("test2( )", "from __main__ import test2") print('append', t2.timeit(number=1000), 'ms') t3 = timeit.Timer("test3( )", "from __main__ import test3") print('comprehension', t3.timeit(number=1000), 'ms') t4 = timeit.Timer("test4( )", "from __main__ import test4") print('list range', t4.timeit(number=1000), 'ms') |
결과)
concat 1.4880505 ms append 0.06963340000000007 ms comprehension 0.0345723 ms list range 0.014829200000000098 ms |
위의 모든 함수(test1( )부터 test4( )까지)는 l = [0, 1, 2, 3, 4, 5, 6, …, 999]을 만드는 함수입니다. 다만 만드는 방식이 리스트의 덧셈, append( ) 메서드, 리스트 컴프리헨션, list( ) 함수로 그 방법이 모두 다른 것입니다. 소스 코드의 자세한 부분들은 지금은 아직 이해하기 어렵지만 t1, t2, t3, t4는 각각을 1000번 시행하는데 걸리는 시간을 체크한 것입니다. 컴퓨터의 성능마다 그 결과는 다를 수 있지만 range( ) 함수와 list( ) 함수를 이용한 결과가 가장 빨랐으며 리스트 컴프리헨션이 그 다음, append( ) 메서드가 3위이며 가장 오래 걸린 방법이 리스트 덧셈을 이용한 것임을 확인할 수 있습니다.
종합적으로 리스트 메서드의 시간 복잡도는 다음과 같습니다. n은 리스트의 총 항목 수이고, k는 연산(조회 및 추가) 항목 수 입니다.
연산 | 시간복잡도 | 연산 | 시간복잡도 |
인덱스 [ ] 접근 | O(1) | 멤버십 테스트 in | O(n) |
인덱스 할당 | O(1) | 슬라이스 [x:y] 조회 | O(k) |
append( ) | O(1) | 슬라이스 삭제 | O(n) |
pop( ) | O(1) | 슬라이스 할당 | O(n+k) |
pop(i) | O(n) | reverse( ) | O(n) |
insert(i, 항목) | O(n) | 연결 concatenate(+) | O(k) |
del 연산자 | O(n) | sort( ) | O(n log n) |
삽입 | O(n) | 곱하기 | O(nk) |
'Python-기본 > Python-데이터 타입 및 자료 구조' 카테고리의 다른 글
Python-컬렉션 데이터 타입(2. 딕셔너리) (0) | 2021.01.12 |
---|---|
Python-컬렉션 데이터 타입(1. 셋) (0) | 2021.01.05 |
Python-3. 시퀀스 데이터 타입(2. 튜플) (0) | 2020.12.30 |
Python-3. 시퀀스 데이터 타입(1. 문자열) (0) | 2020.12.20 |
Python-2. 숫자 데이터 타입 다루기 (0) | 2020.12.14 |