불변형 자료구조 보호와 Set 최적화

박태정·7일 전

Python Deep Dive

목록 보기
6/9

오늘(3월 30일) 데이터를 아예 수정 못하도록 보호하는 방법Set을 제대로 쓰는 법에 대해 공부했다.


데이터 수정 방지 (Read-Only)

코드를 짜다 보면 "이 데이터는 절대 바뀌면 안 된다"는 상황이 생긴다고 한다. 특히 현업에 들어가면 그런 작업을 해야할 수도 있고, 기존에 짜여진 코드를 보면 그런 처리가 된 코드가 있을 수 있다고 한다. 파이썬에서는 이런 상황을 위한 도구가 따로 준비되어 있다.

수정 불가 딕셔너리 (MappingProxyType)

dict는 기본적으로 가변형이라 언제든 값을 추가하거나 바꿀 수 있다. 근데 이 딕셔너리를 외부에 넘겨줄 때 "읽기만 하고 수정은 하지 마"라고 강제하고 싶을 때 MappingProxyType을 사용한다.

from types import MappingProxyType

d = {'key1': 'value1'}

# Read Only 뷰 생성
d_frozen = MappingProxyType(d)

print(d, id(d))
print(d_frozen, id(d_frozen))
# 원본 d는 수정 가능
d['key2'] = 'value2'
print(d)  # {'key1': 'value1', 'key2': 'value2'}

# d_frozen은 수정 불가
d_frozen['key2'] = 'value2'  # TypeError 발생

여기서 중요한 포인트가 있다. d_frozen은 단순히 복사본이 아니라 원본 d를 바라보는 뷰(View) 다. 그래서 원본 d가 수정되면 d_frozen에서도 그 변경 사항이 실시간으로 반영된다. 수정을 막되, 원본의 최신 상태는 항상 볼 수 있다는 게 핵심이다.

  • 그리고 변수명도 뒤에 _frozen으로 붙여주는게 필수는 아니지만 다들 그렇게 한다고 한다.

수정 불가 집합 (frozenset)

set에도 불변 버전이 있다. 바로 frozenset이다.

s5 = frozenset({'Apple', 'orange', 'Apple', 'Orange', 'Kiwi'})

s5.add('Melon')  # AttributeError 발생 -> 추가 불가

일반 set과 다르게 요소를 추가하거나 삭제하는 게 완전히 막혀있다. 완전히 불변이기 때문에 딕셔너리의 Key다른 집합의 요소로도 사용할 수 있다. 일반 set은 가변형이라 그게 불가능하다.


세트(Set) 생성과 {}

set을 처음 배울 때 중괄호 {}로 만들 수 있다고 배웠는데, 조심해서 써야한다. 좀 헷갈린다.

s1 = {'Apple', 'orange', 'Kiwi'}  # set
s4 = {}                           # ???

print(type(s4))  # 결과: <class 'dict'>

{}만 덩그러니 쓰면 파이썬은 이걸 빈 딕셔너리로 인식한다. 빈 집합을 만들려면 반드시 set()을 명시해야 한다. 이게 꽤 자주 헷갈리는 부분이다.

여러 방식으로 set을 선언해보면 이렇다.

s1 = {'Apple', 'orange', 'Apple', 'Orange', 'Kiwi'}  # 리터럴
s2 = set(['Apple', 'orange', 'Apple', 'Orange', 'Kiwi'])  # 함수 호출
s3 = {3}       # 원소가 하나인 set (딕셔너리 아님)
s4 = {}        # 빈 딕셔너리 (주의!)
s5 = frozenset({'Apple', 'orange', 'Apple', 'Orange', 'Kiwi'})  # 불변 set

print(s1, type(s1))  # set
print(s2, type(s2))  # set (중복 제거됨)
print(s4, type(s4))  # dict
print(s5, type(s5))  # frozenset

선언 최적화: 바이트코드로 직접 확인 (약간 번외)

파이썬 코드가 실행될 때 내부적으로 어떤 과정을 거치는지 dis 모듈로 직접 들여다볼 수 있다.

from dis import dis

print(dis('{10}'))       # 리터럴 방식
print(dis('set([10])'))  # 함수 호출 방식

결과를 보면 차이가 확연하다.

# {10} 리터럴 방식
LOAD_CONST    0 (10)
BUILD_SET     1
RETURN_VALUE
# -> 딱 3단계

# set([10]) 함수 호출 방식
LOAD_NAME     0 (set)   # set이라는 이름을 글로벌에서 찾고
PUSH_NULL
LOAD_CONST    0 (10)
BUILD_LIST    1          # 리스트를 먼저 만들고
CALL          1          # 함수를 호출하고
RETURN_VALUE
# -> 더 많은 단계

리터럴 방식 {10}은 파이썬 인터프리터가 BUILD_SET 명령어 하나로 바로 처리한다. 반면 set([10])set이라는 이름을 찾고, 리스트를 먼저 생성하고, 함수를 호출하는 과정을 전부 거친다. 결론은 set을 만들 때는 리터럴 방식 {}을 쓰는 게 성능상 유리하다.

이렇게 바이트코드를 직접 까보는 게 처음에는 어색했는데, "왜 이게 더 빠르냐"에 대한 답을 눈으로 직접 확인할 수 있어서 신기했다. 그리고 되게 직관적이다.


지능형 집합 (Set Comprehension)

리스트 컴프리헨션처럼 Set도 컴프리헨션으로 만들 수 있다. Set의 중복 제거 특성이 자동으로 적용된다.

from unicodedata import name

# 0~255번 유니코드 문자들의 이름을 set으로 수집 (중복 자동 제거)
print({name(chr(i), '') for i in range(0, 256)})

이번 챕터를 마무리하면서 든 생각은, 파이썬에는 같은 기능을 하는 것처럼 보이지만 용도가 미묘하게 다른 도구들이 생각보다 많다는 거다. dict vs MappingProxyType, set vs frozenset, {10} vs set([10]) 같은 것들이 다 그 예시다. 이런 차이를 모르면 코드는 돌아가지만 내가 의도한 대로 동작하지 않거나, 불필요하게 느린 코드를 짜게 된다. 그리고 다른 사람(or LLM)이 작성한 코드를 내가 읽지 못하고 LLM한테만 의존하게 될까봐 걱정이다. 그래서 이런 기초적이면서 딥한 부분에 대해 더 많이 보고 더 익숙해져야겠다는 생각이 계속든다.

0개의 댓글