2013년 7월 17일 수요일

Python의 List, Tuple, 집합

 여기에서는 주요 Python sequence type인 list와 tuple을 다룰 것이다. 첫째로, 당신이 list를 본다면 다른 language의 배열을 떠올릴 수도 있다. 하지만 Python의 list는 일반 배열보다 훨씬 유연하고 강력하다. 그리고 Python의 collection type인 set도 다룰 예정이다. set은 collection에 있는 object의 구성원을 구하려고 할 때 유용하다.
 tuple은 list와 같지만 수정이 불가능하다. tuple은 제한된 list나 기본 record type으로 볼 수 있다. 왜 이런 제한된 data type이 필요한지 추후에 설명할 것이다..
 이 글에서는 list를 중점적으로 다룰 것이다. list를 이해하면 tuple을 이해하기 훨씬 수월하기 때문이다. 이 글의 마지막 부분에는 list와 tuple의 차이점을 기능적인 측면과 design 적인 측면에서 살펴볼 것이다.

배열과 유사한 list

 Python에서 list는 Java나 C와 같은 다른 language에서의 배열과 상당히 유사하다. list는 object들의 정렬된 collection이다. 꺾쇠괄호([])로 묶고 요소들을 쉼표(,)로 구분하여 나열하는 것으로 list를 생성할 수 있다.

# 이는 세 개의 요소로 된 list를 x에 할당한다.
x = [1, 2, 3]

 list를 선언하거나 사전에 크기를 정할 필요는 없다. 위의 line은 list를 생성하고 할당한다. 그리고 크기도 필요에 따라 자동으로 늘어나거나 줄어든다.
 다른 language들의 list와는 달리 Python의 list는 다른 type을 가진 요소들을 포함할 수 있다. list 요소는 모든 Python object가 될 수 있다. 다음은 다양한 요소를 포함하는 list 이다.

# 첫 번째 요소는 숫자, 두 번째는 문자열, 세 번째는 다른 list
x = [2, “two”, [1, 2, 3]]

 대표적인 내장 list 함수는 len 함수라고 할 수 있다. len 함수는 list 내 요소들의 개수를 반환한다.

>>> x = [2, “two”, [1, 2, 3]]
>>> len(x)
3

 len 함수는 list 내의 list 요소의 항목들은 계산하지 않는다는 것을 주의하라.

list index

 list index들이 어떻게 동작하는지 이해한다면 Python을 더욱더 유용하게 사용할 수 있을 것이다. 이 글 전체를 잘 읽어보기를 바란다.
 Python list에서 C의 배열 indexing과 같은 표기법을 사용하여 요소를 추출할 수 있다. C나 다른 language들처럼 Python도 0에서부터 count를 시작한다. 0번 요소를 요청하면 첫 번째 항목을 반환하고 1번 요소를 요청하면 두 번째 항목을 반환한다. 실제로 반환하는 요소는 요청하는 번호에 1을 더한 순서의 항목을 반환한다는 것을 주의하라.

>>> x = [“first”, “second”, “third”, “fourth”]
>>> x[0]
‘first’
>>> x[2]
‘third’

 Python의 indexing은 C의 indexing 보다 더 유연성을 가지고 있다. index를 음수로 나타내면 list의 끝에서부터 count하여 항목을 반환한다. -1번 요소를 요청하면 list의 마지막 항목을 반환하고 -2번 요소를 요청하면 마지막에서 두 번째 항목을 반환한다. 동일한 list x에서 다음과 같이 count하는 것을 볼 수 있다.

>>> a = x[-1]
>>> a
‘fourth’
>>> x[-2]
‘third’

 단일 list index operation에서 특정 요소를 지정하는데 Python의 index를 지정하는 방식은 사용하기에 편리하다. index 개념에서 요소 간의 위치를 나타내는데 고급 operation을 사용할 수 있다. list [“first”, “second”, “third”, “fourth”]에서 다음과 같이 위치를 나타낼 수 있다.











x = [

“first”,

“second”,

“third”,

“fourth”]
양수 index
0

1

2

3

음수 index
-4

-3

-2

-1



 하나의 요소만 추출한다면 관계가 없는 내용이지만 Python은 slicing이라는 operation을 통해 sublist 전체를 추출하거나 할당할 수 있다. list[index]를 사용하여 항목을 추출하는 것 외에 list[index1:index2]를 사용하여 index1부터 index2 전까지의 항목들을 포함하는 새로운 list를 추출할 수도 있다. 다음 예제에서 사용법을 간단하게 살펴보자.

>>> x = [“first”, “second”, “third”, “fourth”]
>>> x[1:-1]
[‘second’, ‘third’]
>>> x[0:3]
[‘first’, ‘second’, ‘third’]
>>> x[-2:-1]
[‘third’]

 두 번째 index가 첫 번째 index 전의 위치를 나타낸다면 index들을 반대로 나타내서 요소들을 역순으로 반환할 수 있을 것이라고 생각할 수도 있지만, 그렇게 되지는 않는다. 대신에 이는 빈 list만 반환할 뿐이다.

>>> x[-1:2]
[]

 list slicing에서 index1이나 index2를 생략하는 것도 가능하다. index1을 생략하면 “list 시작 요소부터” 라는 것을 나타내는 것이고 index2를 생략하면 “list 마지막 요소부터”라는 것을 나타내는 것이다.

>>> x[:3]
[‘first’, ‘second’, ‘third’]
>>> x[2:]
[‘third’, ‘fourth’]

 index들을 모두 생략하면 원래 list의 시작부터 끝까지를 포함하는 새로운 list를 생성한다. 즉, 해당 list를 복사한다. 이는 원래 list에 영향을 주지 않고 list를 수정하기 위해서 복사를 해야하는 상황에서 유용하다.

>>> y = x[:]
>>> y[0] = ‘1 st’
>>> y
[‘1 st’, ‘second’, ‘third’, ‘fourth’]
>>> x
[‘first’, ‘second’, ‘third’, ‘fourth’]

list 변경

 list의 요소를 추출하는 것처럼 index 표기법을 사용하여 list를 변경하는 것도 가능하다. index를 사용한 요소에 할당 연산자를 주고 값을 변경할 수 있다.

>>> x = [1, 2, 3, 4]
>>> x[1] = “two”
>>> x
[1, ‘two’, 3, 4]

 slice 표기법을 사용할 수도 있다. “listA[index1:index2] = listB”는 listA의 index1에서 index2까지의 모든 요소들을 listB의 요소들로 교체한다. listB는 listA에서 교체되는 요소들의 개수보다 크거나 작을 수도 있다. 이 경우에는 listA의 길이가 수정될 것이다. slice 할당을 다양한 방식으로 사용할 수 있다. 다음은 그 예제이다.

>>> x = [1, 2, 3, 4]
>>> x[len(x):] = [5, 6, 7]    # list의 마지막에 list를 추가한다.
>>> x
[1, 2, 3, 4, 5, 6, 7]
>>> x[:0] = [-1, 0]    # list의 처음에 list를 추가한다.
>>> x
[-1, 0, 1, 2, 3, 4, 5, 6, 7]
>>> x[1:-1] = []    # list에서 요소들을 삭제한다.
>>> x
[-1, 7]

 일반적인 operation으로 append라는 method를 사용하여 list에 하나의 요소를 추가할 수 있다.

>>> x = [1, 2, 3]
>>> x.append(“four”)
>>> x
[1, 2, 3, ‘four’]

 만약 당신이 하나의 list를 다른 list에 추가하려고 한다면 한 가지 문제가 있다. 추가하려는 list는 main list에 단일 요소로 추가된다는 점이다.

>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7]
>>> x.append(y)
>>> x
[1, 2, 3, 4, [5, 6, 7]]

 extend method는 append method와 유사하지만 하나의 list를 다른 list에 추가할 수 있게 해준다.

>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7]
>>> x.extend(y)
>>> x
[1, 2, 3, 4, 5, 6, 7]

 기존의 요소들 사이나 list의 앞에 새로운 list 요소를 삽입하기 위한 insert라는 method도 존재한다. insert는 list들의 method로 사용되며 두 개의 추가 argument들이 필요하다. 첫 번째는 새로운 요소를 삽입할 index 위치이며, 두 번째로 새로운 요소가 온다.

>>> x = [1, 2, 3]
>>> x.insert(2, “hello”)
>>> print(x)
[1, 2, ‘hello’, 3]
>>> x.insert(0, “start”)
>>> print(x)
[‘start’, 1, 2, ‘hello’, 3]

 insert는 위에서 예기한 slice 표기법으로 list index들을 인식하지만, 대부분의 사용자들에게는 list.insert(n, elem)을 list의 n번째 요소 전에 elem을 insert한다는 의미로 생각하는 것이 더 쉽게 느껴질 것이다. insert는 상당히 편리한 method이다. insert로 할 수 있는 것은 slice 할당을 사용하는 것으로도 가능하다. 즉, list.insert(n, elem)에서 n이 음수가 아니라면 list[n:n] = [elem]은 동일하게 작동된다. insert를 사용하면 좀 더 가독성이 높은 code를 만들 수 있다. insert는 음수 index들도 취급이 가능하다.

>>> x = [1, 2, 3]
>>> x.insert(-1, “hello”)
>>> print(x)
[1, 2, ‘hello’, 3]

 del 문은 list 항목들이나 slice들을 삭제하는데 선호되는 method이다. del 문은 slice 할당에서 할 수 없는 것을 할 수는 없지만 일반적으로 기억하거나 읽기가 더 쉽다.

>>> x = [‘a’, 2, ‘c’, 7, 9, 11]
>>> del x[1]
>>> x
[‘a’, ‘c’, 7, 9, 11]
>>> del x[:2]
>>> x
[7, 9, 11]

 일반적으로 del list[n]은 list[n:n+1] = []과 동일하게 동작하며 del list[m:n]은 list[m:n] = []과 동일하게 동작한다.
 remove method는 insert의 반대 method가 아니다. insert는 특정 위치에 요소를 삽입하지만, remove는 주어진 값과 일치하는 첫 번째 instance를 list에서 찾아서 제거한다.

>>> x = [1, 2, 3, 4, 3, 5]
>>> x.remove(3)
>>> x
[1, 2, 4, 3, 5]
>>> x.remove(3)
>>> x
[1, 2, 4, 5]
>>> x.remove(3)
Traceback (innermost last):
    File "<stdin>", line 1, in ?
ValueError: list.remove(x): x not in list

 만약 remove가 제거할 값을 찾지 못한다면 error를 발생시킨다. 당신은 이 error를 Python의 예외 처리 기능을 사용하여 잡아내거나, 제거를 하기 전에 in을 사용하여 list에서 해당 값이 있는 지를 점검하여 error를 발생시키지 않을 수 있다.
 reverse method는 list를 수정하는데 더욱 특화된 method이다. 이 method는 list를 효과적으로 뒤집어 정렬해준다.

>>> x = [1, 3, 5, 6, 7]
>>> x.reverse()
>>> x
[7, 6, 5, 3, 1]

list 정렬

 Python 내장 sort method를 사용하여 list를 정렬할 수 있다.

>>> x = [3, 8, 4, 0, 2, 1]
>>> x.sort()
>>> x
[0, 1, 2, 3, 4, 8]

 sort method는 제자리 정렬을 수행한다. 즉, 값들을 정렬하여 list를 변경하는 것이다. 원래 list를 변경하지 않고 list를 정렬하려면 먼저 list를 복사한다.

>>> x = [2, 4, 1, 3]
>>> y = x[:]
>>> y.sort()
>>> y
[1, 2, 3, 4]
>>> x
[2, 4, 1, 3]

 문자열도 정렬할 수 있다.

>>> x = [“Life”, “Is”, “Enchanting”]
>>> x.sort()
>>> x
[‘Enchanting’, ‘Is’, ‘Life’]

 Python은 거의 모든 값들을 비교할 수 있으므로 sort method는 거의 모든 값들을 정렬할 수 있다. 하지만 정렬시에 한가지 지켜야할 절차가 있다. sort에서 사용되는 기본 key method는 list 내의 모든 항목들이 비교할 수 있는 type들임을 요구한다. 이것은 즉 숫자와 문자열을 모두 가진 list에 sort method를 사용한다면 exception을 발생시킨다는 것을 의미한다.

>>> x = [1, 2, 'hello', 3]
>>> x.sort()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() < int()

 다른 한 편으로는 list 내의 list도 정렬이 가능하다.

>>> x = [[3, 5], [2, 9], [2, 3], [4, 1], [3, 2]]
>>> x.sort()
>>> x
[[2, 3], [2, 9], [3, 2], [3, 5], [4, 1]]

 복합 object들을 비교하는 Python 내장 규칙에 의해서 sublist들의 첫 번째 요소를 오름차순으로 정렬한 후에 두 번째 요소를 오름차순으로 정렬한다.
 sort는 여기에 기술된 것들 외에 유연한 기능들을 더 갖추고 있다. list의 요소들을 당신이 정한 key 함수로 정렬하는 것도 가능하다.

Custom 정렬

 custom 정렬하려면 아직 예기하지 않은 함수(function) 정의를 할 수 있어야 한다. 여기에서는 문자열의 문자 개수를 반환하는 len(string) 함수도 사용할 것이다. 문자열(string) 연산에 대해서는 추후 설명할 기회가 있을 것이다.
 기본적으로 sort는 대부분의 목적에 부합하는 정렬 방식을 정하기 위해서 Python 내장 비교 함수들을 사용한다. 반면에 이런 기본 배치법에 해당되지 않는 방식으로 list 정렬이 필요한  할 때도 있을 것이다. 예를 들면 단어들의 list를 일반적으로 Python에서 수행되는 사전식 순서가 아니라 각 단어의 문자 개수로 정렬하려고 하는 경우가 이에 해당될 수 있다.
 위의 경우에는 정렬을 위한 값이나 key를 반환하는 함수(function)를 작성하여 sort method에 사용한다. sort 문맥 내의 함수는 하나의 argument를 받아서 sort 함수가 사용할 key나 값을 반환한다.
 다음은 문자 개수로 정렬하기 위한 key 함수(function)이다.

def compare_num_of_chars(string1):
    return len(string1)

 위의 key 함수는 문자열 그 자체가 아니라 각 문자열의 길이를 sort method에 전달하도록 단순하게 작성되었다.
 위의 key 함수을 정의하고 이를 key 예약어에 사용하여 sort에 전달하도록 할 것이다. 함수는 Python object이기 때문에 다른 Python object와 마찬가지로 sort에 전달된다. 다음 예제는 기본 sort와 위의 방식으로 정렬하는 sort의 차이점을 보여준다.

>>> def compare_num_of_chars(string1):
... return len(string1)
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
>>> word_list.sort()
>>> print(word_list)
['C', 'Python', 'better', 'is', 'than']
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
>>> word_list.sort(key=compare_num_of_chars)
>>> print(word_list)
['C', 'is', 'than', 'Python', 'better']

 처음 list는 사전식 정렬(대문자가 소문자 앞에 정렬된다.)로 나열되어지며, 두 번째 list는 문자들의 개수의 오름 차준으로 나열된다.
 custom 정렬은 매우 유용하다. 하지만 성능을 중요시한다면 custom 정렬은 기본 정렬보다는 느릴 수도 있다는 것을 유념해두기 바란다. 일반적으로 미치는 영향은 미미하지만 key 함수가 상당히 복잡하다면 생각보다 영향을 더 미칠 수 있다. 특히 만 이나 백만 단위의 요소들을 custom 정렬을 한다면 영향이 있을 수도 있다.
 만약 list를 오름차순 정렬이 아니라 내림차순 정렬을 한다면 custom 정렬을 사용할 필요는 없다. 이 경우에는 sort method의 reverse parameter를 True로 설정하여 사용하면 된다. 이 방식을 사용하지 않으려면 list를 일반 sort method로 정렬한 후에 reverse method를 사용하면 list를 역순으로 정렬할 수 있다. 표준 sort와 reverse 연산을 같이 사용하는 것이 custom 정렬보다는 훨씬 빠르다.

sorted() 함수

 list는 자체로 내장 정렬 method를 가지고 있지만, dictionary의 key와 같은 Python의 다른 iterable들은 sort method를 가지고 있지 않다. 하지만 Python은 어떤 iterable도 정렬된 list로 반환하는 내장 함수 sorted()도 가지고 있다. sorted()는 sort method와 마찬가지로 key와 reverse parameter들을 동일하게 사용한다.

>>> x = (4, 3, 1, 2)>>> y = sorted(x)>>> y[1, 2, 3, 4]

다른 일반 list 연산

 다른 많은 list method들은 종종 유용하게 사용되지만 어떤 특정 category로 나뉘지는 않는다.

in 연산자를 이용한 list 구성원 연산

 list 내에 특정 값의 존재여부는 in 연산자를 이용하면 쉽게 알 수 있다. in 연산자는 Boolean 값을 반환한다. 반대 연산은 not in 연산자를 사용하여 구할 수 있다.

>>> 3 in [1, 3, 4, 5]
True
>>> 3 not in [1, 3, 4, 5]
False
>>> 3 in ["one", "two", "three"]
False
>>> 3 not in ["one", "two", "three"]
True

+ 연산자를 이용한 list 접합

+(list 접합) 연산자를 사용하여 두 list를 접합하여 하나의 list를 생성할 수 있다. 이는 argument list들을 변경하지 않는다.

>>> z = [1, 2, 3] + [4, 5]
>>> z
[1, 2, 3, 4, 5]

* 연산자를 이용한 list 초기화

*연산자를 사용하여 주어진 값의 크기를 가지도록 초기화하여 list를 생성할 수 있다. 이는 사전에 지정된 값의 크기를 가지는 list를 처리하는 일반 연산자이다. append를 사용하여 요소들을 추가하여 자동으로 list를 필요한 만큼 확장할 수도 있지만, program 시작시에 *를 사용하여 정확하게 list의 크기를 정하는 것이 훨씬 효율적이다. list의 size를 변경하게 되면 memory 재할당에 대한 overhead가 발생하게 된다.

>>> z = [None] * 4
>>> z
[None, None, None, None]

 list에 *(여기에서는 list 곱셈 연산자라고 부른다.)를 사용하면 다음에 오는 숫자만큼 list를 복제하고 모든 복제본을 연결하여 새로운 list를 생성한다. 이는 사전에 정해진 크기의 list를 정의하는 표준 Python method 이다. 일반적으로 단일 instance None을 가지는 list가 이 곱셈에 사용되지만 다른 값을 가지는 list도 사용될 수 있다.

>>> z = [3, 1] * 2
>>> z
[3, 1, 3, 1]

max과 min를 이용한 list 최대/최소값 도출

 max와 min을 사용하여 list 내에서 가장 큰 요소와 가장 작은 요소를 찾아낼 수 있다. 일반적으로 이 연산자들은 숫자 list에 사용되지만 어떤 type의 요소를 가진 list에도 마찬가지로 사용될 수 있다. 하지만 다른 type들의 비교는 불가능하므로 다른 type들을 가진 object들의 집합에서 최대/최소 object를 찾으려고 한다면 error를 발생시킬 것이다.

>>> min([3, 7, 0, -2, 11])
-2
>>> max([4, "Hello", [1, 2]])
Traceback (most recent call last):
    File "<pyshell#58>", line 1, in <module>
        max([4, "Hello",[1, 2]])
TypeError: unorderable types: str() > int()

index를 이용한 list 검색

 주어진 값이 list 내에서 어디에 있는지를 알아내려면 index method를 사용하면 된다. 이 method는 list 내에서 주어진 값과 동일한 요소를 찾아서 해당 요소의 위치를 반환한다.

>>> x = [1, 3, "five", 7, -2]
>>> x.index(7)
3
>>> x.index(5)
Traceback (innermost last):
    File "<stdin>", line 1, in ?
ValueError: list.index(x): x not in list

 list에 존재하지 않는 요소의 위치를 찾으려고 한다면 위와 같이 error를 발생시킨다. 이는 remove method에서 없는 값을 제거하려고 할 때 발생하는 유사 error와 동일한 방식으로 취급할 수 있다.

count를 이용한 list matching

 count도 주어진 값을 list에서 검색하지만 위치 정보가아니라 list에서 값이 발견된 횟수를 셈하여 반환한다.

>>> x = [1, 2, 2, 3, 5, 2, 5]
>>> x.count(2)
3
>>> x.count(5)
2
>>> x.count(4)
0

list 연산자 요약

 list는 기존 일반 배열보다 더 많은 기능을 내포하는 매우 강력한 data 구조라는 것을 느낄 수 있을 것이다. Python programming에서 중요한 list 연산자들을 쉽게 참조할 수 있도록 다음 표로 나열하였다.

표 5.1 list 연산자
list 연산자
설명
예제
[]
 list 생성.
x = []
len
list 길이를 반환.
len(x)
append
list 끝에 하나의 요소를 추가.
x.append(‘y’)
insert
새로운 요소를 list 내의 주어진 위치에 삽입.
x.insert(0, ‘y’)
del
list 요소 또는 slice 제거.
del(x[0])
remove
주어진 값을 list에서 찾아서 제거.
x.remove(‘y’)
reverse
list 역순으로 나열.
x.reverse()
sort
list 오름차순으로 정렬.
x.sort()
+
 list 합친다.
x1 + x2
*
list 복제한다.
x = [‘y’] * 3
min
list 내에서 최소값을 가진 요소를 반환.
min(x)
max
list 내에서 최대값을 가진 요소를 반환.
max(x)
index
주어진 값의 list  위치를 반환.
x.index[‘y’]
count
list 내에서 주어진 값을 가진 요소의 개수를 센다.
x.count(‘y’)
in
list 내에서 해당 항목의 존재여부를 반환.
y’ in x


 이 list 연산자들에 친숙해진다면 더 수월하게 Python code를 작성할 수 있게 될 것이다.

내포된 list(nested list)와 깊은 복사

 여기서는 좀 더 고급 주제를 다루도록 하겠다.
 list들은 내포가 가능하다. 이차원 행렬을 나타내는데 이 기능을 응용할 수 있다. 내포된 list의 항목들은 이차원 index를 사용하여 참조가 가능하다. 이차원 index는 다음과 같이 동작한다.

>>> m = [[0, 1, 2], [10, 11, 12], [20, 21, 22]]
>>> m[0]
[0, 1, 2]
>>> m[0][1]
1
>>> m[2]
[20, 21, 22]
>>> m[2][2]
22

 여기에서 예상할 수 있듯이 이 mechanism은 더 높은 차원으로 확장이 가능하다.
 다음은 내포된 list(nested list)에서 주의해야 할 것에 대해서 설명하도록 하겠다. 당신이 겪을 수도 있는 내포된 list(nested list)에 관련된 issue가 하나 존재한다. 변수들이 object들을 참조하는 방식의 조합 결과에 대한 것이며 이는 (list와 같이) 몇몇 object들이 수정될 수 있다는 사실이다. (이들은 자주 변경될 수 있다.) 이를 잘 설명하기 위해서 예를 들도록 하겠다.

>>> nested = [0]
>>> original = [nested, 1]
>>> original
[[0], 1]

 다음 그림은 위의 형상을 도식화한 것이다.


그림 5.1 내포된 list(nested list)를 참조하는 첫 번째 항목을 가지는 list

 내포된 list(nested list) 내의 값은 nested 변수나  original 변수를 사용하여 변경이 가능하다.

>>> nested[0] = 'zero'
>>> original
[['zero'], 1]
>>> original[0][0] = 0
>>> nested
[0]
>>> original
[[0], 1]

 하지만 nested가 다른 list에 지정되면 이들의 연결은 끊어지게 된다.

>>> nested = [2]
>>> original
[[0], 1]

 다음 그림은 이를 도식화 한 것이다.


그림 5.2 original list의 첫 번째 항목은 여전히 내포된 list(nested list)이지만, nested 변수는 다른 list를 참조한다.

 전체 slice(즉, x[:])를 통해 list의 복사본을 얻을 수 있다는 것을 당신은 이미 알고 있을 것이다. 또한 +나 * 연산자를 사용하여 list의 복사본을 얻을 수도 있다. (예를 들면 “x + []”이나 “x * 1”) 연산자를 통한 복사 방식이 slice 방식보다 조금 더 효율적이다. 위의 세 가지 방식 모두 얕은 복사(shallow copy)라고 부르는 list의 복사본을 생성한다. 당신은 거의 대부분 이 방식을 사용하게 될 것이다. 하지만 해당 list를 다른 list들에 내포하야 한다면 깊은 복사(deep copy)를 사용해야 한다. 이는 copy module의 deepcopy 함수를 사용하여 수행할 수 있다.

>>> original = [[0], 1]
>>> shallow = original[:]
>>> import copy
>>> deep = copy.deepcopy(original)

다음 그림은 이를 도식화 한 것이다.


그림 5.3 얕은 복사(shallow copy)는 내포 list(nested list)들을 복사하지 않는다.

 original이나 shallow 변수들을 가르키는 list들은 연결되어져 있다. 이들 중의 하나를 통해 내포 list(nested list)의 값을 변경하면 다른 쪽도 영향을 받는다.

>>> shallow[1] = 2
>>> shallow
[[0], 2]
>>> original
[[0], 1]
>>> shallow[0][0] = 'zero'
>>> original
[['zero'], 1]

 깊은 복사(deep copy)는 원본에서 독립되어져 있으며, 이를 변경해도 원본 list는 영향을 받지 않는다.

>>> deep[0][0] = 5
>>> deep
[[5], 1]
>>> original
[['zero'], 1]

 이는 list 내에서 dictionary와 같은 수정이 가능한 다른 내포 object들에도 동일하게 동작한다.
 지금까지 list로 무엇을 할 수 있는지를 살펴보았다. 다음부터는 tuple에 대해서 알아볼 것이다.

tuple

 tuple은 list와 유사하지만 수정할 수 없는 data 구조이다. tuple은 생성만 가능하다. tuple은 list와 상당히 유사한데 여기서 당신은 왜 Python은 이들을 포함하는지 궁금해할 수도 있을 것이다. 그 이유는 list로는 효과적으로 채울 수 없는 dictionary의 key들과 같은 중요한 역활들을 가지고 있기 때문이다.

tuple의 기본

 tuple은 list를 생성하는 것과 유사하게 일련의 값들을 변수에 할당하여 생성한다. list는 [와 ]로 일련의 값들을 둘러싸며 tuple은 (와 )로 일련의 값들을 둘러싼다.

>>> x = ('a', 'b', 'c')

 위의 line은 세 개의 항목을 가지는 tuple을 생성한다.
 tuple을 생성 후의 사용법은 list 사용법과 매우 유사하여 서로 다른 data type이라는 것을 잊어버리기 쉽다.

>>> x[2]
'c'
>>> x[1:]
('b', 'c')
>>> len(x)
3
>>> max(x)
'c'
>>> min(x)
'a'
>>> 5 in x
False
>>> 5 not in x
True

 tuple과 list의 주요 차이점은 tuple은 변경을 할 수 없다는 점이다. tuple을 수정하려고 하면 tuple은 항목에 할당을 지원하지 않는다는 message를 출력하며 TypeError를 발생시킨다.

>>> x[2] = 'd'
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

기존의 tuple로부터 +와 * 연산자를 사용하여 다른 tuple을 생성할 수 있다.

>>> x + x
('a', 'b', 'c', 'a', 'b', 'c')
>>> 2 * x
('a', 'b', 'c', 'a', 'b', 'c')

 tuple은 list와 동일한 방식으로 복사본을 생성할 수 있다.

>>> x[:]
('a', 'b', 'c')
>>> x * 1
('a', 'b', 'c')
>>> x + ()
('a', 'b', 'c')

 tuple 자체는 수정할 수 없다. 하지만 tuple이 list나 dictionary 같이 변경이 가능한 object를 포함하고 있다면 변경이 가능한 요소의 값들은 변경할 수 있다. 변경이 가능한 object들을 포함하는 tuple은 dictionary의 key가 될 수 없다.

하나의 요소만 가진 tuple은 쉼표(,)가 필요하다.

 하나의 요소만 가진 tuple이 쉼표(,)가 필요한 것은 Python 문법에 연관된 문제이다. list를 둘러싸는데 사용되는 꺾쇠괄호([])는 Python 내에서 다른 곳에 사용되지 않으므로 명확하게 []는 빈 list이고 [1]은 하나의 요소만 가진 list이다. tuple을 둘러싸는 괄호는 list와 동일하지 않다. 괄호는 연산의 우선 순위를 높이기 위해서 표현식 내의 항목들을 묶는데 사용될 수도 있기 때문이다. 만약 Python program 내에서 (x + y) 사용한다면 이는 x에 y를 더하고 하나의 요소를 가지는 tuple에 집어넣을 것인지 아니면 x와 y의 더하기를 우선하기 위해 괄호로 묶어준 것인지를 확실하게 구분해줘야 할 필요가 있다.
 이는 하나의 요소만 가진 tuple에만 문제가 된다. 왜냐하면 하나 이상의 요소를 가진 tuple은 언제나 쉼표(,)로 구분되는 항목들을 가지며 쉼표(,)들은 Python에 괄호가 grouping하는 것이 아니라 tuple을 나타낸다고 나타내주기 때문이다. 하나의 요소만 가진 tuple의 경우에 Python에서는 상황의 차이를 분명히 하기 위해서 tuple 내의 항목 다음에 쉼표(,)를 넣도록 요구된다. 아무 항목도 가지지 않은 tuple의 경우에는 문제가 없다. 괄호들 사이가 비어 있는 것은 tuple 이외의 의미는 존재하지 않으므로 tuple이 확실하다.

>>> x = 3
>>> y = 4
>>>(x + y) # 이는 x와 y를 더한다.
7
>>> (x + y,) # 쉼표(,)를 포함하면 괄호들이 tuple을 나타내는 것이다.
(7,)
>>> () # 빈 tuple을 생성하려면 아무것도 가지지 않은 괄호 쌍을 사용하면 된다.
()

tuple의 packing과 unpacking

 편의상 Python은 왼쪽에 tuple의 변수들과 오른쪽의 tuple에 값들이 각각 matching되는 경우에는 할당 연산자를 사용하는 것을 허용한다. 다음은 간단한 예이다.

>>> (one, two, three, four) = (1, 2, 3, 4) 
>>> one
1
>>> two
2

 Python은 괄호로 둘러싸지 않더라도 할당문 내의 tuple들을 인식하므로 더 단순하게 작성이 가능하다. 오른쪽의 값들이 tuple에 packing 된 후에 왼쪽의 변수들에 unpacking된다.

one, two, three, four = 1, 2, 3, 4

 한 line으로 된 위 code는 다음의 네 line의 code로 대체할 수 있다.

one = 1
two = 2
three = 3
four = 4

이는 변수의 값을 교환하는데 편리하게 사용될 수 있다.

temp = var1
var1 = var2
var2 = temp

위의 code 대신에 다음 code를 사용하여 간단하게 표현이 가능하다.

var1, var2 = var2, var1

 편리함을 위해서 Python 3에서는 왼쪽의 한 항목에 * 표시를 하면 오른쪽의 tuple에서 matching되지 않는 항목들을 흡수하는 확장 unpacking 기능을 제공한다. 다음 예제는 이를 명확하게 설명해줄 것이다.

>>> x = (1, 2, 3, 4)
>>> a, b, *c = x
>>> a, b, c
(1, 2, [3, 4])
>>> a, *b, c = x
>>> a, b, c
(1, [2, 3], 4)
>>> *a, b, c = x
>>> a, b, c
([1, 2], 3, 4)
>>> a, b, c, d, *e = x
>>> a, b, c, d, e
(1, 2, 3, 4, [])

 별표 표시된 왼쪽 항목은 모든 오른쪽 나머지 항목들을 list로 받으며, 나머지 오른쪽 항목이 없다면 빈 list를 받는다.
 packing과 unpacking은 list 구분자를 사용해서도 수행된다.

>>> [a, b] = [1, 2]
>>> [c, d] = 3, 4
>>> [e, f] = (5, 6)
>>> (g, h) = 7, 8
>>> i, j = [9, 10]
>>> k, l = (11, 12)
>>> a
1
>>> [b, c, d]
[2, 3, 4]
>>> (e, f, g)
(5, 6, 7)
>>> h, i, j, k, l
(8, 9, 10, 11, 12)

list와 tuple 전환

 tuple은 list 함수를 사용하여 list로 쉽게 전환할 수 있다. (list 함수는 연속된 값들을 argument로 받아서 동일한 값들을 가지는 새로운 list를 생성한다.) 이와 유사하게 list도 tuple 함수를 사용하여 tuple로 전환이 가능하다. (tuple 함수도 list 함수와 동일하게 동작하지만 새로운 list 대신 tuple을 생성한다는 것이 차이점이다.)

>>> list((1, 2, 3, 4))
[1, 2, 3, 4]
>>> tuple([1, 2, 3, 4])
(1, 2, 3, 4)

 여기서 흥미로운 것은 list 함수는 문자열을 문자들로 편리하게 나눠준다는 점이다.

>>> list("Hello")
['H', 'e', 'l', 'l', 'o']

 이는 list와 tuple 함수는 어떤 Python 연속 값들에도 적용되므로 동작하는 것이며, 문자열은 문자들의 연속된 값이기 때문에 위와 같이 동작하는 것이다.

집합(set)

 Python의 set은 중복이 없는 구성원으로 이루어진 불규칙한 순서의 모임이다. dictionary key와 같이 set 내의 항목들은 변경할 수 없으며 hashable 해야 한다. 이는 int, float, string, tuple은 set의 member가 될 수 있지만 list, dictionary, set 자신은 member가 될 수 없다는 것을 의미한다.

집합 연산

 collection에 일반적으로 적용되는 in, len 같은 연산자들과 모든 요소들이 iterate되면 사용이 가능한 for loop에 추가적으로 집합(set)은 몇몇 집합 연산자도 가지고 있다.

>>> x = set([1, 2, 3, 1, 3, 5])    # list처럼 순서열(sequence)에 set을 사용하여 집합을 생성할 수 있다.
>>> x    # 순서열(sequence)가 집합으로 만들어질 때 중복된 항목들은 제거된다.
{1, 2, 3, 5}
>>> x.add(6)    # 집합 내에 요소를 add를 사용하여 추가할 수 있다.
>>> x
{1, 2, 3, 5, 6}
>>> x.remove(5)    # 집합 내의 요소를 remove를 사용하여 제거할 수 있다.
>>> x
{1, 2, 3, 6}
>>> 1 in x    # in 예약어는 object가 집합의 구성원인지 검사하는데 사용될 수 있다.
True
>>> 4 in x
False
>>> y = set([1, 7, 8, 9])
>>> x | y    # 집합 x와 y의 합집합을 새로운 set으로 생성한다.
{1, 2, 3, 6, 7, 8, 9}
>>> x & y    # 집합 x와 y의 교집합을 새로운 set으로 생성한다.
{1}
>>> x ^ y    # 집합 x와 y의 대칭차집합을 새로운 set으로 생성한다.(합집합 - 교집합)
{2, 3, 6, 7, 8, 9}
>>>

 위의 예제는 집합 연산 전체를 사용한 것은 아니지만 집합(set)이 어떻게 동작하는지 충분하게 설명하고 있다. 더 상세한 정보는 Python 공식 documentation(http://docs.python.org/2/library/sets.html)을 참조하라.

frozenset

 set은 변경이 가능하며 hashable하지도 않으므로 다른 set을 가질 수 없다. 이를 개선하기 위해서 생성 후에는 변경할 수 없으며 set과 유사한 frozenset이라는 다른 set type이 있다. frozenset은 변경할 수 없으며 hashable 하므로 다른 집합(set)의 구성원이 될 수 있다.

>>> x = set([1, 2, 3, 1, 3, 5])
>>> z = frozenset(x)
>>> z
frozenset({1, 2, 3, 5})
>>> z.add(6)
Traceback (most recent call last):
    File "<pyshell#79>", line 1, in <module>
        z.add(6)
AttributeError: 'frozenset' object has no attribute 'add'
>>> x.add(z)
>>> x
{1, 2, 3, 5, frozenset({1, 2, 3, 5})}

정리

 list는 Python language 기본 내장 data structure로 매우 유용하다. list는 표준 배열과 유사하게 동작하는 것뿐만이 아니라 자동 resizing, slice 표기법 사용을 위한 기능, 편리하며 괜찮은 함수(function), method, 연산자들의 세트와 같은 추가 기능을 가지고 있다. 여기에서 다루지 못한 list method들도 있지만 위의 내용들만 익혀둔다면 list 기능을 충분히 활용할 수 있을 것이다.
 tuple은 list와 유사하지만 수정할 수 없다는 차이점이 있다. tuple은 좀더 memory를 적게 차지하며 더 빠르게 access 된다. tuple은 list보다 유연하지는 않지만 더 효율적이다. dictionary key들에 tuple을 일반적으로 사용할 수도 있다.
 set도 순서열(sequence) 구조이지만 list와 tuple과는 다른 두 가지 차이점이 존재한다. set은 순서를 가지고 있음에도 불구하고 그 순서는 임의적이며 programmer가 제어할 수 없다. 두 번째 차이점은 set 내의 요소는 반드시 고유한 값이어야 한다는 것이다.

 list, tuple, set은 요소들의 순서열(sequence)에 대한 개념을 구현한 구조라는 것을 당신이 유념하기 바란다.