이전에 파이썬 함수나 클래스에서 사용할 수 있는 매직 메서드와 튜플의 단점(?)을 보완할 수 있는 각 요소에 이름을 붙여줄 수 있는 네임드 튜플에 대해서 공부를 했었다. 오늘(2월 22일)은 Sequence에 대해 공부했다.
Sequence는 순서가 있는 데이터 구조를 의미 한다.
자료형을 구분하는 방법은 크게 두 가지가 있다.
변경 가능 여부에 따른 분류저장 방식에 따른 분류C 나 Java를 사용하면 우리는 보통 배열을 활용해서 데이터를 담는다.
그리고 Python을 활용하면 보통 리스트를 사용한다.
이 두 개를 사용해보면 처음에는 같아 보이지만 다르다는걸 금세 알아차릴 수 있다. 리스트로 정의한 변수에 값을 넣을 때는 변수의 타입을 고려하지 않고 값을 넣어도 큰 문제가 발생하지 않는다.
list_a = [1, 1.0, '코코']
파이썬에는 이런 타입의 자료형들이 있는데 대표적으로
list,tuple,collections.deque등 이 있다. 이들을컨테이너(Container)라고 한다.
- 정리하면 컨테이너는 서로 다른 자료형의 참조(주소값)를 저장
반면 배열을 사용하면 변수를 정의해줄 때 내가 그 통안에 어떤 자료형의 값들을 넣어줄지 미리 정의를 해줘야한다. 그리고 그 자료형만 넣을 수 있다.
from array import array
array_a = array('i', [1,2,3,4])
파이썬에서 Array를 사용할 때 자료형을 지정해줄 때 특정 매칭되어있는 코드를 사용해야한다. 아래와 같다.
| 코드 | 의미 |
|---|---|
'i' | signed int |
'f' | float |
'd' | double |
'b' | signed char |
파이썬에는 이런 타입의 자료형들이 있는데 대표적으로
str,bytes,bytearray,array등이 있다고 한다. 이들을플랫(Flat)이라고 한다.'
- 정리하면 플랫은 당닐 자료형의 값 자체를 연속된 메모리에 저장
데이터를 저장하는 방식뿐만 아니라 '생성된 이후에 내부 값을 수정할 수 있는가'도 아주 중요한 분류 기준이 된다.
우리가 가장 흔하게 사용하는 list를 생각해보면 리스트는 한 번 만들어 둔 다음에 특정 위치의 값을 다른 값으로 원하는 대로 변경할 수 있다.
list_b = [1,2,3]
list_b[0] = 100
파이썬에는 이런 타입의 자료형들이 있는데 대표적으로
list,bytearray,array,collections.deque등이 있다. 이들을가변형(Mutable)이라고 한다.
- 정리하면 가변형은 객체가 한 번 생성된 후에도 내부의 값을 자유롭게 수정가능
반면 튜플이나 문자열은 한 번 통 안에 값을 정의하고 나면, 그 안에 있는 특정 요소를 임의로 바꾸는 것이 불가능하다. 만약 억지로 값을 변경하려고 시도하면 에러가 발생한다.
tuple_b = (1, 2, 3)
tuple_b[0] = 100 # TypeError: 'tuple' object does not support item assignment
str_b = "hello"
str_b[0] = "H" # TypeError: 'str' object does not support item assignment
파이썬에서 이런 자료형들을 사용할 때는 값을 수정하는 것이 아니라, 아예 새로운 값을 만들어 다시 덮어씌우는 방식으로 동작하게 된다.
str_b = 'heloo'
str_b = 'hi'
파이썬에는 이런 타입의 자료형들이 있는데 대표적으로
tuple,str,bytes등 이 있다. 이들을불변형(Immutable)이라고 한다.
- 정리하면 불변형은 한 번 생성된 후에는 절대 내부의 값을 수정할 수 불가능
우리가 보통 리스트에 값을 채워 넣을 때 가장 많이 사용하는 방식은 빈 리스트를 만들고 for문을 돌면서 append()로 하나씩 집어넣는 방식이다.
chars = 'abcdefg'
code_list1 = []
for s in chars:
code_list1.append(s)
# 결과
# ['a','b','c','d','e','f','g']
하지만 파이썬에서는 이것보다 내부 속도가 좀 더 빠르고 깔끔한 방법이 있는데, 바로 리스트 컴프리헨션이다.
code_list2 = [s for s in chars]
기존의
for문과append를 사용하는 것보다 코드가 간결해질 뿐만 아니라, 파이썬 내부적으로 최적화되어 동작하기 때문에 속도가 약간 더 우세하다. 필요하다면map,filter함수와 결합하거나 아예 대체할 수도 있다.
map과filter는 챕터06의 제너레이터에서 더 자세히 다룬다.- 리스트 컴프리헨션은 리스트를 가장 파이썬답고 빠르게 만드는 최적의 방법 -> 이걸 Pythonic 하다고 말을 한다고 한다.
리스트 컴프리헨션이 좋긴 하지만, 만약 다뤄야 할 데이터가 1억 개라면 어떻게 될까? 리스트는 1억 개의 데이터를 메모리에 전부 올려두기 때문에 프로그램이 뻗어버릴 수 있다. 이때 사용하는 것이 제네레이터다. 리스트 컴프리헨션에서 대괄호 []를 소괄호 ()로만 바꿔주면 된다.
tuple_g = (ord(s) for s in chars)
print(tuple_g) # <generator object <genexpr> at 0x1033313c0>
print(next(tuple_g)) # 값을 호출할 때마다 하나씩 생성
제네레이터는 모든 데이터를 메모리에 미리 올려두지 않는다. 대신 어떤 값을 출력할지 준비만 하고 있다가,
next()로 호출할 때마다 그 순간에 하나씩 값을 생성해서 뱉어낸다.
- 제네레이터는 대용량 데이터 처리 시 메모리를 거의 쓰지 않는 매우 효율적인 생성 방식
- 나는
C언어계열의 포인터 느낌을 생각하면 이해했다.
파이썬에서 2차원 리스트(배열)를 만들 때 사람들이 가장 많이 하는 실수가 바로 곱하기(*) 연산자를 사용하는 것이다.
marks2 = [['~'] * 3] * 4
이렇게 만들면 우리가 원하는 대로 4x3 크기의 리스트가 만들어진 것처럼 보인다. 하지만 특정 위치의 값을 변경해보면 대참사가 일어난다.
marks2[0][1] = 'X'
print(marks2)
"""
결과: [
['~', 'X', '~'],
['~', 'X', '~'],
['~', 'X', '~'],
['~', 'X', '~']]
"""
첫 번째 행의 값만 바꿨는데 모든 행의 값이 똑같이 바뀌어버린다.
[['~'] * 3]이라는 리스트 객체를 딱 하나만 생성한 뒤, 그 주소값만 4번 복사해서 넣었기 때문이다.id()로 주소값을 확인해보면 4개의 행이 모두 동일한 주소를 가리키고 있다.
이게 곱하기 연산자를 활용한 다차원 리스트 생성은 주소값만 복사하는 '얕은 복사' 라서 분신술을 한 느낌이다. (같은 놈이 여러 개)
이 문제를 해결하고 안전하게 다차원 리스트를 만드려면, 앞서 배운 리스트 컴프리헨션을 사용해야 한다.
marks1 = [['~'] * 3 for _ in range(4)]
marks1[0][1] = 'X'
print(marks1)
"""
결과: [
['~', 'X', '~'],
['~', '~', '~'],
['~', '~', '~'],
['~', '~', '~']]
"""
이번에는 첫 번째 행의 값만 아주 정상적으로 변경된 것을 확인할 수 있다.
컴프리헨션을 사용하면 반복문(
for)이 4번 돌 때마다 매번 메모리에 독립적인 새로운 리스트를 할당하여 생성한다.id()로 확인해보면 4개의 행이 모두 다른 주소값을 가진다.
다차원 리스트를 만들 때는 컴프리헨션을 사용한 '깊은 복사' 방식을 사용하면 쉽고 안전하게 만들 수 있다.
DataFrame을 다른 변수에 넣어줄 때 df2 = df1.copy()를 해주지 않고 그냥 df2 = df1을 해줘서 많이 애먹었던 기억이 난다.시퀀스란 한마디로 '순서가 있는 데이터의 묶음'을 뜻한다.
'시퀀스는 단순히 데이터를 나열하는 것 뿐인데 왜 갑자기 제네레이터나 주소값 복사 같은 내용을 다뤘을까?'
공부를 하면서 이런 의문이 들었다.
이유는 수많은 데이터를 순서대로 다루는 시퀀스의 특성상 성능, 메모리, 버그 방지가 가장 중요하기 때문이라고 한다.
다시 정리하면 시퀀스는
리스트, 튜플, 배열같이 데이터를 담는 그릇을 의미 한다. 이번에 공부하면서 이러한 시퀀스 자료들을 어떻게 하면 효율적이고 안전하게 사용하는 방법에 대해 배웠다.