Logo

파이썬의 불변(immutable) 자료구조 - tuple, frozenset, namedtuple

Immutability (불변성)

최근 소프트웨어 개발 트랜드를 보면 프로그래밍 언어에 관계없이 불변(imuutable) 데이터 타입의 사용을 권장하는 추세입니다. 여러 가지 이유가 있겠지만 메모리의 가격이 계속해서 싸지면서 데이터를 복제하는 대신에 변경하는 것이 더 이상 큰 이점으로 여겨지지 않고 있습니다. 반면에, 데이터를 변경하는 것에 대한 리스크는 멀티 쓰레드 기반의 동시/병렬 프로세싱 기법이 발달하면서 점점 더 커지고 있습니다. 즉, 여러 쓰레드가 동시에 데이터를 변경할 경우, 소프트웨어가 어떻게 동작할지 예측이 어렵고 버그가 발생할 확률이 높아집니다.

파이썬에서는 프로그램 실행 중에 데이터가 변경되는 것을 방지할 수 있도록 tuple, namedtuple, frozenset과 같은 데이터 타입을 제공되고 있습니다. 이번 포스팅에서는 이러한 불변 데이터 타입을 어떻게 사용하는지 살펴보도록 하겠습니다.

실습 환경 셋업

간편한 가짜 데이터 생성을 위해서 Faker 라이브러리를 설치합니다.

$ pip install Faker

그 다음, 파이썬 인터프리터에서 Faker 패키지를 임포트하고, Faker 인스턴스를 생성합니다.

$ python
>>> from faker import Faker
>>> fake = Faker()

Faker에 대한 자세한 설명은 관련 포스팅를 참고바랍니다.

Tuple

가장 먼저 살펴볼 불변 데이터 타입인 튜플(tuple) 입니다. tuple은 리스트(list)처럼 여러 개의 데이터를 순서대로 저장할 때 사용하는데 데이터 타입입니다.

가변 데이터 타입인 list는 다음과 같이, 저장하고 있는 데이터를 자유롭게 변경할 수 있습니다.

>>> names = []
>>> type(names)
<class 'list'>
>>> names.append(fake.name())
>>> names.append(fake.name())
>>> names.append(fake.name())
>>> names
['Mark Espinoza', 'Joann Thomas', 'Samantha Harper']
>>> names.pop()
'Samantha Harper'
>>> names[1] = fake.name()
>>> names
['Mark Espinoza', 'Susan Wright']

반면에, 불변 데이터 타입인 tuple은, 저장하고 있는 데이터를 마음대로 변경할 수 없습니다. 내장 함수인 tuple()을 이용하면 list 객체를 간단하게 tuple 객체로 변환할 수 있습니다.

>>> immutable_names = tuple(names)
>>> type(immutable_names)
<class 'tuple'>
>>> immutable_names
('Mark Espinoza', 'Susan Wright')
>>> immutable_names[1] = fake.name()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    immutable_names[1] = fake.name()
TypeError: 'tuple' object does not support item assignment

tuple() 내장 함수는 list 뿐만 아니라 순회 가능한(iterable) 모든 타입을 인자로 받을 수 있습니다. 예를 들어, 다음과 같이 generator 객체를 넘기면 굳이 중간에 list 객체를 거쳐지 않기 때문에 보다 메모리 효율이 좋이지겠죠?

tuple(fake.name() for _ in range(3))
('Amanda Willis', 'Cody Costa', 'Patricia Castro')

Frozenset

frozenset은 말 그대로 얼어 붙어서 데이터가 고정되어 있는 set을 의미합니다. set 데이터 타입은 여러 개의 데이터를 중복없이 저장하기 위해서 사용하는데요. frozenset은 set의 이러한 특성을 그대로 지닌체 데이터를 변경할 수 있는 기능만 제거당한 데이터 타입입니다.

가변 데이터 타입인 set은 데이터를 자유롭게 추가하거나 삭제할 수 있습니다.

>>> numbers = {1, 2}
type(numbers)
<class 'set'>
>>> numbers.add(3)
>>> numbers.add(3)
>>> numbers
{1, 2, 3}
>>> numbers.remove(2)
>>> numbers
{1, 3}

반면에, 불변 데이터 타입인 frozenset은, 데이터를 함부로 추가하거나 삭제하는 것이 불가능합니다. 내장 함수인 frozenset()을 이용하면 순회 가능한(iterable) 모든 타입을 frozenset 객체로 변환할 수 있습니다.

>>> immutable_numbers = frozenset(numbers)
>>> type(immutable_numbers)
<class 'frozenset'>
>>> immutable_numbers.remove(2)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    immutable_numbers.remove(2)
AttributeError: 'frozenset' object has no attribute 'remove'

Namedtuple

tuple과 list, frozenset과 set이 서로 대응된다면, namedtuple은 사전(dictionary)의 불변 타입에 가깝습니다. dictionary는 키와 값의 쌍을 저장할 때 사용하는 해시 테이블(hash table) 자료구조 기반 데이터 타입입니다.

가변 자료구조인 dictionary는 새로운 키에 값을 추가하거나, 기존 키의 값을 변경하거나 삭제하는 것이 가능합니다.

>>> user = {"name": fake.name(), "mail": fake.email()}
>>> type(user)
<class 'dict'>
>>> user["address"] = fake.address()
>>> user
{'name': 'Elizabeth Alexander', 'mail': 'johnsonjames@yahoo.com', 'address': '9779 White Landing Suite 853\nRusselltown, DC 51512'}
>>> user["name"] = fake.name()
>>> user
{'name': 'Ana Holloway', 'mail': 'johnsonjames@yahoo.com', 'address': '9779 White Landing Suite 853\nRusselltown, DC 51512'}
>>> del user["address"]
>>> user
{'name': 'Ana Holloway', 'mail': 'johnsonjames@yahoo.com'}

반면에, 불변 데이터 타입인 namedtuple은, 이러한 방식으로 변경하는 것이 허용되지 않습니다. namedtuple은 collections 내장 모듈로 부터 임포트해서 사용합니다.

>>> from collections import namedtuple

namedtuple() 함수는 첫번째 인자로 타입명 두번째 인자로 필드 목록을 받으며, namedtuple 객체를 생성해주는 팩토리 함수를 리턴합니다.

>>> User = namedtuple("User", ["name", "mail"])

dictionary 객체 앞에 **를 붙여서 인자로 넘기면 손쉽게 namedtuple 객체로 변환할 수 있습니다.

>>> immutable_user = User(**user)
>>> type(immutable_user)
<class '__main__.User'>
>>> immutable_user
User(name='Ana Holloway', mail='johnsonjames@yahoo.com')

아예 새로운 namedtuple 객체를 생성하려면, 각 필드의 값을 인자로 넘겨주면 됩니다.

>>> immutable_user = User(name=fake.name(), mail=fake.email())
>>> immutable_user
User(name='William Baker', mail='bentleyjessica@hotmail.com')

데이터를 변경하려고 하면, 다음과 같이 예외가 발생하게 됩니다.

>>> immutable_user.name = fake.name()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    immutable_user.name = fake.name()
AttributeError: can't set attribute
>>> immutable_user.address = fake.address()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    immutable_user.address = fake.address()
AttributeError: 'User' object has no attribute 'address'

주의 사항

tuple, frozenset, namedtuple와 같은 불변 데이터 타입을 다룰 때 흔히 발생하는 실수가 있습니다. 바로, 단지 이러한 데이터 타입을 사용한다고 해서 무조건 데이터의 불변성이 보장되는 것은 아니라는 것입니다.

예를 들어, list 형태의 좌표 여러 개를 담고 있는 다음 tuple은 특정 list를 통째로 변경하는 것은 불가능할지 몰라도…

>>> points = ([1, 2], [3, 4])
>>> type(points)
<class 'tuple'>
>>> points[0] = [10, 2]
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    points[0] = [10, 2]
TypeError: 'tuple' object does not support item assignment

tuple이 담고 있는 각각의 list 자체는 가변 데이터 타입이기 때문에 해당 list 내부의 데이터 변경은 얼마든지 가능합니다.

type(points[0])
<class 'list'>
>>> points[0][0] = 10
>>> points
([10, 2], [3, 4])

이러한 문제를 해결하려면, 외부의 데이터 타입 뿐만 아니라 내부의 데이터 타입까지도 모두 불변 데이터 타입을 사용해야 합니다.

>>> immutable_points = ((1, 2), (3, 4))
>>> type(immutable_points)
<class 'tuple'>
>>> type(immutable_points[0])
<class 'tuple'>
>>> immutable_points[0][0] = 10
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    immutable_points[0][0] = 10
TypeError: 'tuple' object does not support item assignment

마찬가지 원리로 여러 개의 dictionary를 담는 tuple 대신에 여러 개의 namedtuple를 담는 tuple을 사용했을 때 불변성이 높아지게 됩니다. 제가 불변성 보장이라고 하지 않는 이유는 namedtuple의 각 필드가 불변 데이터 타입이라는 보장이 없기 때문입니다. 😂

마치면서

이상으로 파이썬 내장되어 있는 대표적인 불변 데이터 타입인 tuple, namedtuple, frozenset에 대해서 알아보았습니다. 유연함이 장점인 프로그래밍 언어인 파이썬에서 완벽한 불변성을 달성하는 것은 이외로 쉽지 않을 수 있는 일입니다.

사실 이러한 불변 데이터 타입은 함수형 프로그래밍에서 특히 더 빛을 발합니다. 이 부분에 대해서는 추후 기회가 되면 포스팅해보도록 하겠습니다.