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를 상속받아 사용하는 게 혁신이라 할만큼 엄청 대단한 건 아니지만 대부분의 케이스에서 몇 줄의 코드를 더 줄일 수 있게 도와주기 때문에 매우 유용하다고 볼 수 있습니다.