Python 3의 새로운 자료구조 알아보기
Python 3은 더이상 새로운 언어가 아닙니다. 실제로 얼마전 Python 3은 3000일을 맞이하기도 했습니다. 많은 사람들이 Python 3을 외면하기도 했었지만 꽤 오랜 시간이 지난 지금 Python 3은 엄청난 인기를 끌고 있습니다. 이 인기에 힙입어 더 이상 Python 2에 머물러 있을 것이 아니라 Python 3에 새로이 추가된 자료 구조를 알아보는 것이 좋다고 생각합니다.
types.MappingProxyType
types.MappingProxyType
은 읽기 전용 dict로 사용되고 Python 3.3부터 지원됩니다.
읽기 전용이 의미하는 것은 이 객체는 직접적으로 수정/삭제가 이뤄질 수 없으며 만약 변경을 원할 경우 직접 객체를 복사하여 수정하여야만 합니다. 즉, 데이터를 어딘가에서 사용하는데 그곳에서 원본 데이터를 수정할 수 있게 하고 싶지 않은 경우에 사용합니다.
사용 예:
>>> from types import MappingProxyType
>>> data = {'a': 1, 'b':2}
>>> read_only = MappingProxyType(data)
>>> del read_only['a']
TypeError: 'mappingproxy' object does not support item deletion
>>> read_only['a'] = 3
TypeError: 'mappingproxy' object does not support item assignment
여기서 read_only
객체는 직접 수정할 수 없기 때문에 데이터를 다른 함수나 스레드에 넘기면서 수정되지 않게 하고 싶은 경우 원래 dict
객체가 아닌 MappingProxyType
객체를 넘기면 됩니다.
>>> def my_threaded_func(in_dict):
>>> ...
>>> in_dict['a'] *= 10 # oops, a bug, this will change the sent-in dict
...
# in some function/thread:
>>> my_threaded_func(data)
>>> data
data = {'a': 10, 'b':2} # note that data['a'] has changed as an side-effect of calling my_threaded_func
위 예에서 보듯 mutable한 객체는 reference로 넘어가기 때문에 이 값을 다른 함수에 넘기는 경우 해당 함수에서 직접 값을 수정할 수 있게 됩니다. 이 때 my_threaded_func
함수에 원래 객체 대신 mappingproxy
를 넘겨 해당 함수에서 값을 수정하려 한다면
>>> my_threaded_func(MappingProxyType(data))
TypeError: 'mappingproxy' object does not support item deletion
와 같은 에러를 보게 됩니다. 이를 통해 이러한 에러를 막기 위해 my_threaded_func
에서 in_dict
를 복사한 후 값을 수정해야 한다는 것을 알 수 있게 됩니다.
참고로 read_only
객체는 read-only(읽기전용)임에도 불구하고 immutable(불변)하지는 않습니다. 따라서 data
를 변경한다면 read_only
객체 또한 변하게 됩니다 (proxy가 하는 역할이기도 합니다):
>>> data['a'] = 3
>>> data['c'] = 4
>>> read_only # changed!
mappingproxy({'a': 3, 'b': 2, 'c': 4})
typing.NamedTuple
typing.NamedTuple
은 낡아빠진 collections.namedtuple
의 새 버전입니다. Python 3.5에 추가됐지만 Python 3.6부터 제대로 사용할 수 있습니다.
collections.namedtuple
와 비교해서 다음과 같은 특징을 갖습니다 (Python >= 3.6):
- 더 나은 문법
- 상속
- 타입 정의(type annotations)
- 기본값 설정(Python >= 3.6.1)
아래 예제를 봅시다:
>>> from typing import NamedTuple
>>> class Student(NamedTuple):
>>> name: str
>>> address: str
>>> age: int
>>> sex: str
>>> tommy = Student(name='Tommy Johnson', address='Main street', age=22, sex='M')
>>> tommy
Student(name='Tommy Johnson', address='Main street', age=22, sex='M')
기존 함수 기반의 문법에서 클래스 기반의 문법으로 변경되어 더 읽기가 좋아졌습니다. 참고로 클래스로 변했어도 저 객체는 여전히 tuple
입니다:
>>> isinstance(tommy, tuple)
True
>>> tommy[0]
'Tommy Johnson'
조금 더 깊게 가서 Student
객체를 상속받고 기본값을 설정(Python >= 3.6.1)해봅시다:
>>> class MaleStudent(Student):
>>> sex: str = 'M' # default value, requires Python >= 3.6.1
>>> MaleStudent(name='Tommy Johnson', address='Main street', age=22)
MaleStudent(name='Tommy Johnson', address='Main street', age=22, sex='M') # note that sex defaults to 'M'
간단히 설명해서 typing.NamedTuple
은 가장 현대적인 namedtuple이고, 미래에 표준 namedtuple로 자리매김될 것으로 보입니다.
types.SimpleNamespace
types.SimpleNamespace
는 Python 3.3에 추가된 네임스페이스 속성에 대한 접근과 완전한 표현(repr)을 제공하는 간단한 클래스입니다.
>>> from types import SimpleNamespace
>>> data = SimpleNamespace(a=1, b=2)
>>> data
namespace(a=1, b=2)
data.c = 3
>>> data
namespace(a=1, b=2, c=3)
간단히 말해 types.SimpleNamespace
는 완전한 repr을 제공하는 변경 가능한 자유로운 클래스입니다. 글 쓴 본인은 쉽게 읽고 쓸 수 있는 객체로 dict
대신에 사용하거나, 상속해서 사용합니다:
>>> import random
>>> class DataBag(SimpleNamespace):
>>> def choice(self):
>>> items = self.__dict__.items()
>>> return random.choice(tuple(items))
>>> data_bag = DataBag(a=1, b=2)
>>> data_bag
DataBag(a=1, b=2)
>>> data_bag.choice()
(b, 2)
types.SimpleNamespace
를 상속받아 사용하는 게 혁신이라 할만큼 엄청 대단한 건 아니지만 대부분의 케이스에서 몇 줄의 코드를 더 줄일 수 있게 도와주기 때문에 매우 유용하다고 볼 수 있습니다.