2장 시퀀스의 배열
파이썬은 모든 시퀀스를 일관성이 있게 처리하는 ABC 언어의 특징을 물려받았다. 문자열, 리스트, 바이트 시퀀스, 배열, XML 요소, 데이터베이스 결과에는 모두 반복, 슬라이싱, 정렬, 연결 등의 연산을 일관되게 적용할 수 있다.
파이썬에서 제공하는 다양한 시퀀스를 이해하면 코드를 새로 구현할 필요가 없으며, 시퀀스의 공통 인터페이스 기존 혹은 향후에 구현될 시퀀스형을 적절히 지원하고 활용하도록 API 를 정의하게 이끌어 준다.
2.2 내장 시퀀스 개요
파이썬 표준 라이브러리는 C 로 구현된 다음과 같은 시퀀스형을 제공한다.
컨테이너 시퀀스
서로 다른 자료형의 항목을 담을 수 있는
list,tuple,collections.deque형
균일 시퀀스
단 하나의 자료형만 담을 수 있는
str,bytes,array.array형
컨테이너 시퀀스는 객체에 대한 참조를 담으며, 객체는 어떠한 자료형도 될 수 있지만, 균일 시퀀스는 객체에 대한 참조 대신 자신의 메모리 공간에 각 항목의 값을 직접 담는다.
따라서 균일 시퀀스가 메모리를 더 적게 사용하지만, 바이트, 정수, 실수 등 기본적인 자료형만 담을 수 있다.
시퀀스는 다음과 같이 가변성에 따라 분류할수도 있다.
가변 시퀀스
list,bytearray,array.array,collections,deque형
불변 시퀀스
tuple,str,bytes형
아래 그림을 보면 가변 시퀀스가 불변 시퀀스를 상속하면서 여러 메서드를 추가로 구현함을 알 수 있다. 내장된 구상 시퀀스형이 실제로 Sequence, MutableSequence 추상 베이스 클래스(ABC) 를 상속받지는 않지만, 이 두 추상 베이스 클래스의 가상 서브클래스이다.
이 부분이 파이썬의 유연함을 보여주는 대목이다.
실제로
list클래스의 소스코드를 열어보면class list(MutableSequence):라고 명시적으로 적혀 있지는 않다. (이를 '직접 상속받지 않는다' 고 표현한다.)하지만 파이썬은 "직접 상속받지 않았더라도, 필요한 기능을 모두 갖추고 있다면 그 가족으로 인정해준다" 는 규칙을 가지고 있다.
때문에
issubclass(list, abc.MutableSequence)를 실행하면True가 나오는 것이다.

따라서 tuple, list 는 다음 테스트를 통과한다.

자료형 하나에 '가변형과 불변형' 그리고 '컨테이너형과 균일형' 을 만드는 이런 공통적인 방식을 기억해 두면, 다른 시퀀스형의 계층구조도 쉽게 가늠할 수 있다.
가장 핵심 시퀀스형은 list 로, 가변적이며 혼합된 자료형을 담을 수 있다. 리스트형은 이미 잘 안다고 가정하고, 바로 지능형 리스트로 넘어가자. 지능형 리스트는 리스트를 만드는 막강한 방법이지만, 낯선 구문 때문에 그리 많이 사용되지는 않는다. 지능형 리스트를 제대로 알면 제너레이터 표현식도 쉽게 이해할 수 있다.
2.3 지능형 리스트와 제너레이터 표현식
리스트형이라면 지능형 리스트, 그 외 다른 시퀀스형이라면 제너레이터 표현식을 사용해 시퀀스를 간단히 생성할 수 있다. 이런 구문을 사용하지 않는다면, 가독성이 좋고 때로는 실행 속도도 빠른 코드를 만들 기회를 놓치는 것이다.
2.3.1 지능형 리스트와 가독성
아래 두 코드 예제 중 어느것이 가독성이 좋은가?
처음 예제는 파이썬을 조금만 공부해도 알 수 있다. 그러나 지능형 리스트를 배운 후에는 아래 예제가 더 가독성이 좋다고 생각할 것이다. 의도가 명확하기 때문이다.
for 루프는 시퀀스를 읽고 개수를 세거나 어떤 항목을 골라내거나 합계를 구하는 등 아주 다양한 일에 사용할 수 있다. 첫번째 예제는 for 루프를 이용해 리스트를 만든다. 이와 대조적으로 두번째 예제(지능형 리스트)의 의도는 명확하다. 오로지 새로운 리스트를 만들 뿐이다.
물론 지능형 리스트를 남용해 정말 난해한 코드를 만들 수 있다. 지능형 리스트를 사용해서 단지 한 블록의 코드를 반복적으로 수행하는 파이썬 코드를 본 적도 있다. 생성된 리스트를 사용하지 않을 것이라면, 지능형 리스트 구문을 사용하지 말아야 한다. 그리고 코드는 짧게 만들어야 한다.
지능형 리스트 구문이 2줄을 넘어간다면 코드를 분할하거나 for 문을 사용하는 편이 더 낫다.
2.3.2 지능형 리스트와 map() / filter() 조합의 비교
지능형 리스트를 이용하면 기능적으로 제한된 파이썬 람다를 사용하지 않고도 내장 함수 map() / filter() 가 수행하는 모든 작업을 구현할 수 있다. 아래 코드를 보자.
필자는 map() / filter() 가 동급의 지능형 리스트보다 빠르다고 생각했지만, 알렉스 마르텔리는(적어도 이 예제에서는) 그렇지 않다고 지적했다.
2.3.3 데카르트 곱
예를 들어, 두 가지 색상과 세가지 크기의 티셔츠 리스트를 만든다고 생각해 보자. 지능형 리스트를 이용해 생성하는 방법은 아래 예제와 같다. 결국 여섯 개의 항목이 만들어진다.
지능형 리스트는 단지 리스트만 만들 수 있다. 다른 시퀀스를 만들려면 제너레이터 표현식을 사용해야 한다. 다음 절에서는 리스트 이외의 시퀀스를 생성하는 제너레이터 표현식을 간단히 살펴본다.
2.3.4 제너레이터 표현식
튜플, 배열 등의 시퀀스형을 초기화하려면 먼저 지능형 리스트를 사용할 수도 있지만, 제너레이터 표현식이 메모리를 더 적게 사용한다. 다른 생성자에 전달할 리스트를 통째로 만들지 않고 이터레이터 프로토콜을 사용해 항목을 하나씩 생성하기 때문이다.
제너레이터 표현식은 지능형 리스트와 똑같은 구문을 사용하지만, 대괄호 대신 소괄호를 사용한다.
지능형 리스트
[x for x in data]메모리에 모든 항목을 즉시 생성하여 저장
제너레이터 표현식
(x for x in data)필요할 때 하나씩 생성(Lazy evaluation)
아래 예제는 튜플과 배열을 생성하는 기본적인 제너레이터 표현식이다.
아래 예제에서는 테카르트 곱에 제너레이터 표현식을 사용해 두 가지 색상과 세가지 크기의 티셔츠 목록을 출력한다. 이전 예제와 달리 티셔츠 리스트의 여섯 개 항목을 메모리 안에 생성하지 않는다. 제너레이터 표현식은 한 번에 한 항목을 생성하도록 for 루프에 데이터를 전달하기 때문이다.
대중적인 파이썬 프로그래밍 지침(Best Practice)에서도 "리스트가 꼭 필요한 상황이 아니라면 제너레이터를 우선 고려하라"고 권장
데카르트 곱을 만드는 데 사용할 리스트에 각기 천 개의 항목이 있을 때, 제너레이터 표현식을 사용하면 단지 for 루프에 전달하려고 항목이 백만 개 들어 있는 리스트를 생성하는 일을 피할 수 있다.
2.4 불변 리스트를 뛰어넘는 튜플
파이썬 입문서 중에서 튜플을 '불변 리스트' 로 설명하는 책도 있지만, 이 설명만으로는 부족하다. 튜플은 불변 리스트로 사용할 수도 있지만, 필드명이 없는 레코드로 사용할 수도 있다. 레코드로 사용하는 경우를 간과할 때가 종종 있으므로, 이를 먼저 살펴보자.
2.4.1 레코드로서의 튜플
튜플은 레코드를 담는다. 튜플의 각 항목은 레코드의 필드 하나를 의미하며, 항목의 위치가 의미를 결정한다.
튜플을 단지 불변 리스트로 생각한다면, 때에 따라 항목의 크기와 순서가 중요할 수도 있고 그렇지 않을 수도 있다. 하지만, 튜플을 필드의 집합으로 사용할 때는 보통 항목 수가 고정되며 항목의 순서가 항상 중요하다.
아래 예제는 순서가 곧 의미!
때문에, 위 경우 튜플을 정렬하면 정보가 파괴된다!
('홍길동', 25, '서울')(이름, 나이, 주소)
흔히 레코드는 명명된 필드로 구성된 데이터 구조체로 간주되고는 한다. 5장에서는 명명된 필드로 구성된 튜플을 만드는 방법 두 가지를 소개한다.
2.4.2 불변 리스트로서의 튜플
파이썬 인터프리터와 표준 라이브러리는 튜플을 불변 리스트로 많이 사용하므로 프로그래머들은 이 방식을 따라야 한다. 이 때 다음과 같은 두 가지 장점이 있다.
명확성
코드 안에 튜플이 보이면 그 리스트의 길이가 절대 바뀌지 않음을 알 수 있다.
성능
튜플은 똑같은 항목을 담은 리스트보다 메모리를 더 적게 소비하므로 파이썬 인터프리터가 최적화를 수행할 수 있다.
그러나 튜플의 불변성은 그 안에 포함된 참조에만 적용된다는 점을 주의해야 한다. 튜플 안에 참조는 삭제되거나 바뀔 수 없다. 하지만 이러한 참조가 가변 객체를 가리키고, 해당 객체의 값이 바뀌면 튜플 값도 바뀐다.
다음 코드는 처음에는 값이 똑같은 a,b 두 개의 튜플을 생성한다. 하지만 b 안에 마지막 항목이 바뀌면 b와 a는 달라진다.
튜플 내용 자체는 불변형인데, 이 말은 튜플이 항상 동일한 객체를 참조한다는 의미일 뿐이다. 그렇지만 튜플 리스트와 같은 가변형 객체를 참조한다면, 참조된 객체의 내용은 바뀔 수 있다.
가변 항목이 있는 튜플은 버그의 원인이 될 수 있다. 3.4.1 절 '해시 가능한 객체' 에서 설명하겠지만, 값이 절대 바뀌지 않는 객체만 해시 가능하다. 해시 불가능한 튜플은 dict 키나 set 항목으로 추가할 수 없다.
이러한 주의점이 있지만, 튜플은 불변 리스트로 널리 사용된다. 파이썬의 핵심 개발자인 레이먼드 헤팅거가 스택 오버플로의 '파이썬에서 튜플은 리스트보다 효율적인가?' 라는 질문에 튜플은 리스트보다 어느 정도 성능상의 장점이 있다고 답변했다.
튜플 리터럴을 평가하기 위해 파이썬 컴파일러는 한 번의 연산으로 튜플 상수에 대한 바이트코드를 생성한다. 그러나 리스트 리터럴의 경우, 생성된 바이트코드는 각 항목을 별도의 상수로 만들어 데이터 스택에 쌓은 후 리스트를 만든다. (튜플은 한 번에 만들고, 리스트는 하나씩 쌓아서 만든다)
튜플
t가 있을 때tuple(t)는 단지t에 대한 참조를 반환할 뿐이며 값을 복사할 필요가 없다. 이와 반대로 리스트l에 대해list(l)생성자는l의 사본을 만들어야 한다. (튜플은 불변이기 때문에, 가능하다)길이가 고정되므로
tuple객체에는 필요한 만큼 메모리가 할당된다. 한편list객체에는 앞으로 추가할 메모리 연산을 고려해 약간의 공간을 더 할당한다.튜플 항목에 대한 참조는 튜플 구조체 배열에 저장되지만, 리스트는 다른 곳에 저장된 참조 배열에 대한 포인터를 가진다.
리스트는현재 할당된 메모리 공간보다 항목이 더 많아지면 공간을 새로 확보하고 참조 배열을 재할당해야 하므로 이런 구조로 이루어진다. 한번 더 간접적으로 참조하므로 CPU 캐시의 효율은 떨어진다.
2.5 시퀀스와 반복형 객체의 언패킹
Last updated