파이썬 객체 참조, 가변성을 통해 본 파이썬 객체 성질 알아보기
최근 참여하고 있는 OpenStack 프로젝트도 그렇고, 구글 머신러닝 부트캠프에서의 활동도 그렇고 언어를 파이썬으로 통일해 2가지 프로젝트를 함께 진행하면서 파이썬을 깊게 이해해야만 하는 상황이 많이 발생하고 있다.
‘클린 코드(Clean Code)’와 같은 책에서는 함수를 정의할 때, 한 개의 함수에서는 하나의 기능만을 구현해야하는 언급하는 한 편, 오픈 스택과 같은 오픈 소스 프로젝트에서는 기능이 모여 동작하는 단위를 클래스를 사용해 구현하고 있다.
(굳이 파이썬만이 아니라도 C++이나 Java, JS도 객체 지향 원칙을 준수하기 때문에 다 그럴 것이다)
파이썬에서 변수를 설명할 때, 처음 코딩을 배우는 사람들이 변수가 언급될 때 객체가 같이 언급되면 둘의 차이가 뭔지 많이 헷갈려하는 상황이 많이 만들어지고 나도 베트남에서 파이썬 강의를 진행할 때 ‘변수’가 뭐냐고 정의하는 시간에 마치 ‘상자’처럼 생각하면 된다고 하며 변수 개념이 잘 안와닿으면 거의 비슷한 개념이라고 생각하면 된다고 얘기했었다.
근데 이 설명이 부족하다고 항상 느끼고 있었던 게, 클래스를 작성하고 그 클래스를 변수에 할당하면, 우리는 ‘객체로부터 비롯된 변수’이기 때문에 이러한 변수를 ‘인스턴스’라고 불렀다.
이 개념들을 명확히 구분하는 것이 뭘까에 대해 막연히 생각해보고 있었고, 그 답에 근접하게나마 찾은 내용이 있어 정리하게 되었다.
1. 변수의 객체 참조
1997년 MIT에서 진행된 Java Summer Course에서 컴퓨터 교육자인 Lynn Andrea Stein 교수가 언급하길 ‘상자로써의 변수’ 개념이 실제로 객체 지향 언어에서 참조 변수를 이해하는데 오히려 방해가 된다는 언급을 했다.
되려 파이썬에서 ‘변수(variables)’는 객체에 붙은 레이블(label)이라고 생각하는 게 좋다고 한다.
왜 여기서 Label로써의 변수라고 생각하면 좋은지는 아래 코드를 보면 한 눈에 알 수 있다.
아마 파이썬 기초 과정을 수강하면 위의 내용을 항상 한 번씩은 거치고 간다.
코드를 한 줄씩 해석하자면, a라는 리스트 자료형 변수를 선언하고, 그 변수를 b라는 변수의 참조 변수로 할당하게 되었다.
이 경우, a 변수에 새로운 요소 값을 추가하게 되면, b 변수가 참조하고 있는 변수와 서로 메모리 상의 주소 값이 동일하므로 b 변수도 할당된 값이 수정되게 된다.
그래서 Lynn Andrea Stein 교수님은 포스트잇으로써의 변수라는 개념을 강조한다.
포스트잇에 따라 화이트보드에 작성된 내용에 대한 태그만 달라질 뿐, 화이트 보드에 작성된 값은 변화하지 않기 때문이다.
위의 코드에서 변수가 실행되는 순서는
- 부등호 오른편의 객체를 읽고, 실행한다 (x 변수 오른편은 객체 실행 성공, y는 정수형 값과 곱셈 연산을 실행할 수 없어 에러 발생)
- 변수가 생성된다.
인데 이를 통해 입증할 수 있는 것은 객체의 레이블은 생성 이후에 붙는 것이며 객체를 생성하는 것은 마치 별명(alias)를 붙이는 것과 같다고 한다.
위의 x, y 두 변수는 하나의 동일한 객체를 복제해서 사용하기 때문에 하나의 객체에 여러 label을 붙이는 것과 같은 상황을 만들기 때문이다.
2. == 연산자와 is 연산자 간의 선택 (값과 객체 주소 사이의 선택)
나도 보통 구분 안하고 사용하는데 ==는 동치 연산, 그리고 is 연산은 객체의 정체성을 비교한다.
객체의 정체성이라는 것이 잘 못봤던 용어라 헷갈릴 수 있는데 쉽게 말해 메모리 내의 객체 주소라고 생각하면 된다.
is 연산은 비교하고자 하는 두 객체의 정체성을 비교하는 것이다.
(즉 객체 주소를 비교하는 것)
그래서 보통 파이썬 코드를 짤 때, 정체성을 비교하는 상황보다 값을 비교하는 상황이 일반적이므로 ==를 더 자주 볼 수 있고, is 연산자를 사용하는 상황은 주로 None과 비교하는 상황이다.
참고로 ==를 사용하는 것은 a.__eq__(b)를 사용하는 것과 동일한데 더 간편하게 표현한 것이다.
3. Tuple 자료형의 상대적 불변성 (Tuple 자료형도 가변적일 수 있다)
이건 JS의 Const 변수 선언형과 비슷한데, Tuple은 일반적으로 자료형 내부에 값을 수정이 불가능한 자료형으로 알려져 있지만, 내부에 가변형 자료형이 들어가면, 참조하고 있는 이 항목에 대해서는 변경이 가능하다.
예를 들어 t1 = (1, 2, [30, 40]) 이라는 튜플 변수가 있다면, t[-1] 요소에 한 해서 t[-1].append(99)를 실행하면 자료형의 수정이 가능해진다.
그런데 여기서 의문이 생긴다.
만약 이러한 가변적인 자료형이 담겨 있는 튜플을 복사하면, 현재 튜플처럼 객체가 다른 객체를 담고 있을 때 내부 객체도 함께 복사해야 하는가? 아니면 내부 객체는 공유하는 형태 (주소 값만 참조하는)로 해도 될까? 정답은 없지만 파이썬의 리스트 또는 내장 가변 컬렉션은 기본적으로 얕은 복사를 (shallow copy)를 지원하고 있다.
아마 [:] 나 .copy() 메소드를 사용해 본 파이썬 개발자라면 이러한 객체 복사 방법이 얕은 복사라는 것을 알고 있을 것이다.
즉, 최상위 컨테이너는 복제하지만 사본은 원래 컨테이너에 들어있던 동일 객체에 대한 참조로 채워진다.
(예를 들어 cp = (1,2,3,[4,5,6]) 이라면 튜플 자료형은 복사하고 내부의 요소 값들은 cp 변수에 생성된 객체의 참조 값을 따른다는 뜻)
그래서 객체의 내부 값들을 참조하는 형태로 공유할 경우, 아주 가끔씩 문제가 있을 수 있어 deepcopy() 메소드를 지원하는 것이다.
(deepcopy의 경우 내부 참조하는 요소 값의 주소가 원본 변수의 주소 값과 다르다. 다시 말해, 새로운 값을 주소에 할당해 만드는 것이다)
우리가 변수를 사용하는 것은 “별명(alias)을 통한 객체 공유 방식”이다.
즉, 공유로 객체를 호출하는 (call by sharing) 전달 방식이라 함수 내부의 매개변수는 실제 인수의 별명으로써 동작한다.
(쉽게 풀어 설명하면 매개 변수가 변수처럼 활용된다.)
아래 코드를 보면서 좀 더 쉽게 설명할 수 있을 것 같은데 HauntedBus라는 클래스를 생성하면서 초기화할 때 매개 변수 passengers를 인수를 전달하지 않는 상황에는 기본 값인 빈 리스트를 바인딩 해놨다.
근데 이 경우, 객체를 각각 다른 변수에 생성하더라도 Bus 객체들이 승객 리스트를 공유하는 잘못된 상황이 발생하며, 이러한 종류의 객체는 정말 찾기 힘들다.
(내가 봐도 실제 프로젝트에서 이런 상황이 발생하면, 봐야하는 코드가 엄청 많을텐데 고생 꽤나 하게 될 것이다)
때문에 명시적으로 클래스 생성시, 아래와 같이 작업할 필요가 있다.
생성된 인스턴스들이 상호 독립적으로 동작하기 위해서는 위와 같은 형태가 클래스를 사용하는 프로그래머들의 행복도를 높일 것이다.
4. 후기
오픈 스택을 읽으면서 파이썬 컨벤션 규칙에 대해 찾아볼 일이 종종 있다.
대표적으로는 PEP8이 있지만 다 읽어볼 시간은 없고, 필요한 부분을 찾아서 읽는 형태로 보고 있다.
파이썬을 깊게 배우면 배울수록 느끼지만 C언어에서 포인터와 주소 값을 직접 개발자가 할당하는 방식에 대해 더 깊게 배울 필요성이 있다고 느낀다.
결국 파이썬 코어로 접근할 수록 “참조”라는 단어를 더 많이 보게 되고, 위에 작성한 클래스 예시처럼 초기화를 잘못했을 때 발생하는 에러 등을 빠르게 캐치하려면 프로그래밍 언어의 가장 본질적인 개념을 잘 이해하고 있는지, 못하는지가 시간을 많이 아껴주기 때문이다.
파이썬을 현업에서 어느 정도 레벨 이상 사용하는 단계가 오면 C++을 배우기 시작해서 이 2개를 내 메인 언어로 잡고 가야겠다.
(어차피 C++을 하면 C는 알고 있어야하기 때문에…)
5. Reference
전문가를 위한 파이썬 8장
Ryan