파이썬 리스트 중첩, 복사, 분류하기

파이썬 리스트 중첩, 복사, 분류하기

리스트와 튜플 중첩으로 만들기

리스트와 튜플은 어떤 유형의 값이라도 담을 수 있다. 즉, 리스트와 튜플은 리스트와 튜플을 값으로도 담을 수 있다. 중첩 리스트(nested list) 혹은 중첩 튜플(nested tuple)은 또 다른 리스트나 튜플을 값으로 담고 있는 리스트나 튜플을 뜻한다.

다음 예제의 리스트는 다른 두 리스트를 값으로 가진다.

>>> two_by_two = [[1, 2], [3, 4]]

>>> # two_by_two has length 2
>>> len(two_by_two)
2

>>> # Both elements of two_by_two are lists
>>> two_by_two[0]
[1, 2]
>>> two_by_two[1]
[3, 4]

two_by_two[1]가 리스트 [3, 4]를 반환하므로, 중첩 리스트 안에 요소를 접근하기 위해 이중 인덱스 표기법(double index notation)을 사용할 수 있다.

>>> two_by_two[1][0]
3

먼저, 파이썬은 two_by_two[1]을 평가하여 [3, 4]를 반환한다. 그리고 파이썬은 다시 [3, 4][0]을 평가하여 첫 번째 요소인 3을 반환한다.


리스트 복사하기

하나의 리스트를 다른 리스트로 복사해야하는 상황이 있을 수 있다. 하지만, 하나의 리스트 객체를 다른 리스트 객체로 단순히 재할당 할 수는 없다. 왜냐면, 다음과 같은 예상치 못한 결과를 얻게되기 때문이다.

>>> animals = ["lion", "tiger", "frumious Bandersnatch"]
>>> large_cats = animals
>>> large_cats.append("Tigger")
>>> animals
['lion', 'tiger', 'frumious Bandersnatch', 'Tigger']

위 예제에서 변수 animals에 저장된 리스트를 변수 large_cats에 할당한 후 새로운 문자열을 리스트에 추가했다. 하지만 원본 변수인 animals를 출력한 결과 위와 같이 원본에 또한 새로운 문자열이 추가된 것을 확인할 수 있다.

이 부분은 객체 지향 프로그래밍(object-oriented programming)의 특이한 점이지만, 이렇게 의도된 설계이다. 위 예제에서 large_cats = animal를 선언했을 때, 변수 large_catsanimals는 같은 객체를 참조하게 된다.

변수의 이름은 컴퓨터 메모리의 특정한 위치를 참조하는 것이다. 리스트 객체의 모든 컨텐츠를 복사해서 새로운 리스트를 생성하는 대신, large_cats = animalsanimals가 참조하는 메모리 위치를 large_cats에게 할당한다. 즉, 두 변수 다 메모리 안의 같은 객체를 참조하고, 어느 한 쪽에서 변경사항이 있으면 다른쪽에도 영향을 미친다.

animal 리스트를 복사해 별개의 새로운 리스트를 생성하려면 슬라이싱을 사용할 수 있다.

>>> animals = ["lion", "tiger", "frumious Bandersnatch"]
>>> large_cats = animals[:]
>>> large_cats.append("leopard")
>>> large_cats
['lion', 'tiger', 'frumious Bandersnatch', 'leopard']
>>> animals
["lion", "tiger", "frumious Bandersnatch"]

슬라이스 안의 어떤 인덱스도 명시되지 않았음으로, 리스트 안의 모든 요소들이 처음부터 끝까지 반환된다. 그러므로 large_cats 리스트는 animals같은 요소를 가지게 되고, append() 메서드를 사용해 animals에 할당된 리스트에 영향을 미치지 않고 새로운 항목을 추가할 수 있게된다.

만약 중첩 리스트를 복사하고 싶다면 다음과 같이 슬라이싱을 사용할 수 있다.

>>> matrix1 = [[1, 2], [3, 4]]
>>> matrix2 = matrix1[:]
>>> matrix2[0] = [5, 6]
>>> matrix2
[[5, 6], [3, 4]]
>>> matrix1
[[1, 2], [3, 4]]

matrix2 안의 두 번째 리스트의 첫 번째 요소를 변경하면 어떻게 되는지 확인해보자.

>>> matrix2[1][0] = 1
>>> matrix2
[[5, 6], [1, 4]]
>>> matrix1
[[1, 2], [1, 4]]

matrix1의 두 번째 리스트 또한 변경되었음을 확인할 수 있다. 이 문제는 목록에 실제로 개체 자체가 포함되어 있지 않고, 메모리 안의 개체에 대한 참조가 포함되어 있기 때문에 발생한 것이다. 슬라이싱을 사용해 리스트를 복사했을 때, 원본 리스트와 동일한 참조를 포함한 새로운 리스트가 반환된다. 프로그래밍 용어에서는 이런 방식의 리스트 복사를 얕은 복사(shallow copy)라고 부른다.

리스트와 리스트에 포함된 모든 요소의 복사본을 만드려면 깊은 복사(deep copy)라고 불리는 방식을 사용할 수 있다. 깊은 복사는 복사 과정을 재귀적으로 만든다. 즉,  첫 번째로 새로운 컬렉션 객체를 구성하고, 다음으로 원본에서 발견된 하위 객체의 복사본을 재귀적으로 생성한다.


리스트 정렬하기

파이썬에서는 리스트의 모든 항목들을 오름차순 정렬해주는 sort() 메서드를 제공한다. 리스트는 리스트 안의 요소의 타입에 따라 알파벳순 혹은 숫자순으로 정렬된다.

>>> # 문자열로 구성된 리스트는 알파벳순으로 정렬
>>> colors = ["red", "yellow", "green", "blue"]
>>> colors.sort()
>>> colors
['blue', 'green', 'red', 'yellow']

>>> # 숫자로 구성된 리스트는 숫자순으로 정렬
>>> numbers = [1, 10, 5, 3]
>>> numbers.sort()
>>> numbers
[1, 3, 5, 10]

sort() 메서드는 원본 리스트를 변경해주므로 결과를 다시 할당하지 않아도 된다. 더 나아가서, sort() 메서드는 리스트가 정렬되는 방식을 조절하는 key라는 선택적인 매개변수를 가진다. key 매개변수는 함수를 허용하고, 함수의 리턴값을 기반으로 리스트는 정렬된다.

예를 들어, 다음과 같이 문자열로 이루어진 리스트를 각 문자열의 길이로 정렬할 수 있다.

>>> colors = ["red", "yellow", "green", "blue"]
>>> colors.sort(key=len)
>>> colors
['red', 'blue', 'green', 'yellow']

위 예제처럼, 함수의 이름을 괄호 없이 전달할 수 있다. 즉, len()이 아닌 len만 전달 되어도 된다. key로 전달되는 함수는 단일 인수만 허용한다.

또한, 사용자가 정의한 함수key의 인수로 전달할 수 있다. 다음의 예제는 get_second_element()라고 불리는 함수가 튜플로 구성된 리스트를 정렬하는 데 사용된다.

>>> def get_second_element(item):
... 	return item[1]
...
>>> items = [(4, 1), (1, 2), (-9, 0)]
>>> items.sort(key=get_second_element)
>>> items
[(-9, 0), (4, 1), (1, 2)]
key에 전달하는 모든 함수는 하나의 인수만 허용한다는 것을 기억하자.

Reference