제공 :
한빛 네트워크저자 : Luciano Ramalho
역자 : 권정민
원문 :
Python tuples: immutable but potentially changing파이썬 튜플에는 놀라운 특징이 있다. 튜플 자체는 수정되지 않지만, 튜플의 값은 바뀔 수 있다. 이런 현상은 리스트 같이 튜플이 변경 가능한 객체를 참조할 때 발생한다. 만약 파이썬을 처음 사용하는 동료에게 이 내용에 대해서 설명해야 한다면, 첫 번째 단계로는 변수는 데이터를 저장하는 상자 같다는 통념을 뒤집어주는 것이 좋을 것이다.
1997년에 MIT에서 자바 여름 학기 수업을 들었다. Lynn Andrea Stein 교수-전산 교육 수상자-는 일반적인 "변수는 상자다"라는 일반적인 비유가 실제로 객체형 언어의 참조 변수에 대한 이해를 방해한다는 점을 지적했다. 파이썬 변수는 자바의 참조 변수와 비슷해서, 이를 객체에 붙이는 이름표처럼 생각하는 것이 더 나을 것이다.
다음은 루이스 캐럴의 [거울나라의 앨리스]에서 가져온 예제다.
트위들덤과 트위들디는 쌍둥이다. 책에는 "한 명의 옷깃에는 "DUM", 다른 사람의 옷깃에는 "DEE"라고 수놓여 있기 때문에 앨리스는 한 눈에 누가 누군지 구분할 수 있었다."라고 되어 있다.
이를 사용해서 다음과 같이 생일과 능력이 기록된 튜플로 나타낼 수 있다.
>>> dum = ('1861-10-23', ['poetry', 'pretend-fight'])
>>> dee = ('1861-10-23', ['poetry', 'pretend-fight'])
>>> dum == dee
True
>>> dum is dee
False
>>> id(dum), id(dee)
(4313018120, 4312991048)
dum과 dee는 동일한 객체를 참조하지만, 둘이 같은 객체가 아니라는 것은 명확하다. 둘은 서로 다른 정체성을 가진다.
앨리스가 옷깃을 본 이후, 트위들덤은 T-Doom이라는 예명을 사용하는 래퍼가 되기로 했다. 이를 파이썬에서는 다음과 같이 나타낼 수 있다.
>>> t_doom = dum
>>> t_doom
('1861-10-23', ['poetry', 'pretend-fight'])
>>> t_doom == dum
True
>>> t_doom is dum
True
그러므로 t_doom과 dum은 같다. 하지만 t_doom과 dum은 동일한 사람을 참조해서 t_doom is dum 이므로 앨리스는 이렇게 말하는 것 자체가 바보같다고 불평할 지도 모른다
t_doom과 dum이란 이름은 알리아스(alias)다. 공식 파이썬 문서에서 종종 "이름"처럼 변수를 참조하는 것이 좋다. 변수는 객체에 부여하는 이름이고, 다른 이름은 가명이다. 이렇게 생각하면 변수가 상자와 같다는 개념에서 자유로워질 수 있다. 변수가 상자와 같다고 생각하는 사람은 이 이후에 어떻게 진행될 지 감을 잡을 수 없을 것이다.
>>> skills = t_doom[1]
>>> skills.append('rap')
>>> t_doom
('1861-10-23', ['poetry', 'pretend-fight', 'rap'])
>>> dum
('1861-10-23', ['poetry', 'pretend-fight', 'rap'])
T-Doom이 'rap' 기술을 익혔고, 이는 트위들덤도 마찬가지다 - 물론, 이 둘은 한 사람으로, 동일인이다. 만약 t_doom이 str과 list를 포함하는 상자라면, t_doom 내에 있는 리스트에 값을 추가했을 경우 dum의 상자에 있는 리스트도 변한다는 것을 어떻게 설명할 것인가? 하지만 변수를 이름표라고 본다면 이 내용은 완벽하게 맞아떨어진다.
이름표로 비유하는 것은 한 객체에 두 개 이상의 이름을 부여하는 알리아스에 대해 간단히 설명한다는 면에서도 좋다. 이 예제에서, t_doom[1]과 skills는 하나의 리스트 객체에 두 이름을 부여한 것으로, dum과 t_doom이 동일한 튜플 객체에 대해 부여된 두 가지 이름인 것과 일맥상통하다.
다음은 Tweedledum을 나타내는 객체에 대해 다르게 나타낸 내용이다. 이 그림은 튜플이 객체에 대한 참조를 가지고 있다는 것을 강조한다.
튜플에는 무조건적으로 객체 참조 값만으로 이루어진 물리적 내용이 들어간다. dum[1]이 참조하는 리스트의 값이 변해도, 참조하는 객체의 ID는 여전히 동일하다. 객체는 독립적으로 튜플 밖에서 존재하고 앞에서 사용한 skills처럼 이름은 이를 참조할 뿐이므로, 튜플에는 해당 아이템의 값이 변하는 것을 막을 방도가 없다. 튜플 내의 리스트와 다른 변경 가능한 객체는 얼마든지 변할 수 있지만, 이에 대한 ID는 항상 동일하다.Python Language Reference의
Data Model 장에서는 다음과 같이 ID와 값의 개념적 차이에 대해 강조하고 있다.
모든 객체는 ID와 유형, 값을 가진다. 객체의 ID는 한 번 생겨나면 절대 변하지 않는다. 이는 메모리상의 객체 주소라고 생각하면 된다. "is" 연산자는 두 객체의 ID를 비교한다. id() 함수는 이 id를 나타내는 숫자를 반환한다.
dum이 래퍼가 된 후의 쌍둥이 형제는 더 이상 같지 않다.
>>> dum == dee
False
이 두 튜플은 처음에는 동일하게 만들어졌으나 지금은 다르다.
파이썬의 수정 불가한 형태의 내장 컬렉션 타입인 frozenset은 수정 불가하지만 값이 변할수 있다는 속성이 문제가 되지 않는다. frozenset(혹은 일반 set)은 해시 가능한 객체의 참조값만 가지고 있고, 해시 가능한 객체는 정의된 바에 따르면, 절대 바뀌지 않는다.
튜플은 일반적으로 사전의 키처럼 쓰이고, 이는 set 처럼 해시 가능하다. 따라서, 튜플은 해시가 가능할까? 정확한 답은, 어떤 튜플은 해시가 가능하다는 것이다. 변형이 가능한 객체를 가진 튜플 값은 바뀔 수 있고, 이런 튜플은 해시를 할 수 없다. 딕셔너리의 키나 셋의 원소로 사용되는 튜플은 해시 가능한 객체만으로 구성되어 있다. 여기서 dum과 dee로 명명된 튜플은 각각이 리스트 참조를 가지고 있고, 리스트는 해시를 할 수 없으므로 이 둘도 해시가 불가능하다.
그럼 이 전체 예제의 핵심인 할당문을 들여다 보자.
파이썬에서의 할당은 값을 복사하지 않는다. 할당에서는 참조만을 복사한다. 그래서 skills = t_doom[1]라고 썼을 때 t_doom[1]을 복사하지 않고 이에 대한 참조만을 복사하고, skills.append("rap")를 실행해서 리스트를 바꿨다.
MIT 이야기로 돌아가면, Stein 교수는 할당에 대해 매우 신중하게 설명했다. 예를 들어, 시소 시뮬레이션에 대해 이야기하면서, 이렇게 말했다. " 변수 s를 시소에 할당했다" 라고 하지, "시소를 변수 s 에 할당했다"고 하지 않았다. 참조 변수의 경우는 변수를 객체에 할당했다고 하는 쪽이 훨씬 합리적이고, 그 반대는 정말 틀리다. 무엇보다, 객체는 할당 전에 만들어지지 않는가.
y = x * 10 같은 할당문에서, 오른쪽 부분이 먼저 계산된다. 이를 통해 새로운 객체가 생기거나 기존의 객체를 가져온다. 객체를 만들거나 가져온 후, 이름이 여기에 할당된다.
다음은 이를 코드에 구현한 것이다. 우선 Gizmo 클래스와 이 클래스의 인스턴스를 만든다.
>>> class Gizmo:
... def __init__(self):
... print('Gizmo id: %d' % id(self))
...
>>> x = Gizmo()
Gizmo id: 4328764080
__init__ 메서드는 만들어진 객체의 id를 보여준다. 이는 다음 예시에서 중요하게 사용될 것이다. 그럼 다른 Gizmo 인스턴스를 초기화하고 결과에 이름을 부여하기 전에 바로 사용해 보도록 하자.
>>> y = Gizmo() * 10
Gizmo id: 4328764360
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>> 'y' in globals()
False
이 스니펫을 보면 새로운 객체가 초기화되고(ID는 4328764360이다) y라는 이름을 만들기 전에, TypeError가 발생해서 할당이 되지 않는 것을 알 수 있다. globals()의 'y'는 다른 광역 변수명 중 y가 없는 지를 확인한다.
정리해 보자. 항상 할당문의 오른쪽을 먼저 읽는다. 이 부분이 객체를 만들거나 가져오는 부분이다. 이후 이름표를 붙이듯이, 왼쪽의 이름을 객체에 할당한다. 상자에 대해서는 그냥 잊어버려라.
튜플에서, 객체를 딕셔너리의 키처럼 사용하거나 셋에 넣지 않는 한 수정 불가한 객체의 참조값만을 가진다는 사실을 기억해 두자. 이 글은 내
Fluent Python 책의 8장을 기반으로 한다. "객체 참조, 수정가능성, 재활용"이라고 명명된 이 장에서는 여러 주제 중 함수 내 매개변수 전달 문법, 수정 가능한 매개변수 처리 예제, 얕은 복사와 깊은 복사, 약한 참조 개념 등을 다룬다. 이 책은 파이썬 3을 주로 다루지만 대부분의 내용은 이 글처럼 파이썬 2.7에도 적용 가능하다.