- 프롤로그
- 개발 첫걸음
- 파이썬 기초
- 파이썬 중급
- 파이썬 고급
- 내장 함수 톺아보기
- 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
- 변수의 범위 – 이름 검색의 범위
- 파이썬 심화
- 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
- 데코레이터 – 함수의 기능을 강화하기
- 프로퍼티
- 제너레이터
- async와 await
- 객체로서의 클래스 – 클래스를 동적으로 정의하기
- 파이썬 프로젝트 실습
- 원카드 게임 만들기 (1)
- 원카드 게임 만들기 (2)
- 원카드 게임 만들기 (3) (작성중)
- 턴제 자동 전투 게임 만들기 (작성중)
- 실전 (파이썬 외적인 것들)
- 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
- 유니코드 – 컴퓨터에서 문자를 표기하는 방법
- html, css, 인터넷 – 자동화 첫 걸음 내딛기
- 네트워크 – 인터넷으로 통신하는 방법
- 문서 – 문맥을 읽어보기
가변 객체(Mutable Object)와 불변 객체(Immutable Object)
객체는 가변 객체와 불변 객체로 구분할 수 있습니다. 말 한마디로 요약하면 가변 객체는 내용을 수정할 수 있고 불변 객체는 내용을 수정할 수 없습니다. 말 한 마디로 당연히 이해가 되지 않을 겁니다. 일단 어떤 것이 가변 객체이고 불변 객체인지는 이미 확정되어져 있으므로 그것부터 살펴본 뒤 가변 객체와 불변 객체가 어떻게 동작하는지 알아보도록 합시다.
종류
객체는 실제로 데이터로서 메모리에 존재하는 녀석이고, 이 객체를 만들기 위해 클래스를 이용하곤 합니다. 사실 가변 객체와 불변 객체를 나누는 불변성이라는 특징은 객체 하나하나에 대응되는 것이 아니라 클래스 그 자체에 대응됩니다. 그러니까 클래스를 불변성으로 구분할 수 있다는 것이 사실 정확한 표현이라는 겁니다.
사용자 정의 클래스, list
, dict
, 등등이 가변 객체이고 숫자형, tuple
, str
등이 불변 객체입니다. 보기 좋게 표로 나누면 다음과 같습니다.
가변형 | 불변형 |
---|---|
사용자 정의 클래스, list , dict 등 |
숫자형, tuple , namedtuple , str 등 |
차이점
불변 객체와 가변 객체를 나누는 불변성이 대상을 무엇으로 하는지 분명히 합시다. 변수를 배울 때 우리는 이름과 데이터로 나눌 수 있다고 했습니다. 우리가 새로운 변수를 정의하고 그 이름을 부를 때 즉각적으로 메모리의 특정 위치에서 파이썬 인터프리터가 값을 뽑아옵니다. 불변 객체는 내용을 수정할 수 없다는 말은 그 메모리에 위치하는 불변 객체의 값을 수정할 수 없다는 뜻입니다. 즉 불변형 객체의 모든 데이터는 객체가 최초로 생성될 때 결정된다라고도 말할 수 있습니다.
a = (3, 4)
a[1] = 2
print(a)
Traceback (most recent call last):
File "c:/Users/tooth/Desktop/test2.py", line 2, in <module>
a[1] = 2
TypeError: 'tuple' object does not support item assignment
튜플은 불변형 객체이므로 내용을 수정할 수 없습니다.
반면 리스트는 가변 객체이므로 항목을 수정할 수 있습니다.
a = [3, 4]
a[1] = 2
print(a)
[3, 2]
불변 객체든 가변 객체든 가리키는 대상을 다른 대상으로 변경하는 것 대해서는 아무런 제한이 없습니다. 그러니까 어떤 변수를 재정의하는 문제와는 전혀 다른 이야기라는 것입니다. 변수는 언제든지 재정의할 수 있습니다. 불변성은 변수에 있는 것이 아니라 클래스에 있습니다. 아래 예제는 정상적으로 작동됩니다.
a = (3, 4)
a = (5, 6)
print(a)
(5, 6)
아래 예제는 언뜻 튜플 내부를 수정하는 것처럼 보이지만, 그렇지 않습니다.
a = (3, 4)
a += (5, 7)
print(a)
(3, 4, 5, 7)
튜플의 +=
연산에서는 새로운 튜플이 만들어져 변수에 새로 대입됩니다. 기존에 a
가 가리키던 (3, 4)
라는 튜플은 이제 아무도 가리켜주지 않으므로 누구도 접근할 수 없어 곧 메모리에서 삭제될 것입니다. 기존에 가리키던 (3, 4)
나 새로 가리키는 (3, 4, 5, 7)
이나 어느 것 하나 수정은 이루어지지 않았으므로 아무런 문제가 없습니다.
만약 튜플 안에 리스트가 있다면 어떻게 될까요?
a = (3, 4, [5, 6])
a[2].append(3)
print(a)
(3, 4, [5, 6, 3])
리스트의 내용은 수정할 수 있습니다. 튜플이 불변형이라 그 내용이 수정되지 않아야 하는데, 수정이 된다면 논리적으로 틀린 것일까요? 다행히도 그렇지는 않습니다. 튜플이 내용으로서 가지고 있는 것은 리스트에 대한 참조, 즉 이름입니다. 즉 가리키고 있는 대상이 어느 메모리에 위치한 특정 리스트임이 가장 중요한 포인트입니다. 그 리스트가 처음에 5, 6
이라는 항목을 가지고 있었지만 append
하여 내용이 바뀐다 하더라도 그 리스트가 다른 리스트로 바뀌는 것이 아니니, 튜플 입장에서는 내용이 변경되는 게 아니지요.
정체성
가변 객체는 정체성이라는 개념이 추가됩니다. 정체성이란 무엇일까요?
철수라는 동명이인이 있는데, 나이도 14살로 똑같다고 가정해봅시다. 이 두 사람이 같다
라고 말할 수 있을까요? 아니지요. 심지어 이름도 같고 생김새도 같고 말버릇도 같고 모든 게 다 같은 사람이라도 따로 존재하고 있다면 그 두 사람은 다르다 라고 말하는 것이 마땅합니다. 아무리 겉모습이 같더라도, (뭐, 생각하는 것도 같다 하더라도) 둘은 독립적으로 존재하기 때문에 같다고 할 수 없습니다. 이를 정체성이라고 하며 모든 가변 객체는 정체성이 부여됩니다.
a = {"name": '철수', "age": 14}
b = {"name": '철수', "age": 14}
print(a == b)
print(a is b)
True
False
우리는 이제껏 두 변수가 같은지 아닌지 비교할 때 ==
를 줄곧 사용해 왔습니다. ==
연산자는 값이 같은지를 비교합니다. 딕셔너리나 리스트 등에 ==
연산을 한다면, 내부에 있는 모든 요소를 하나하나 비교하며 다른게 있는지 없는지 검사합니다. 이것과 관련된 성질을 동질성이라고 하겠습니다.
그리고, is
가 새로 등장했습니다. is
는 정체성을 비교하는 역할입니다. is
는 id
함수의 결과를 비교합니다.(쉽게 얘기하자면) id
함수의 예제는 아래에서 확인할 수 있습니다.
거의 쓰이지는 않지만 id
함수를 이용하여 그 대상이 어떤 정체성인지 확인할 수 있습니다. id
함수를 실행하게 되면 알 수 없는 정수 값이 튀어나옵니다. 그 객체의 고유 번호인 셈이지요. 이 값이 같다면 정말 같은 메모리에 위치하는, 이름만 여러 개인 객체인 셈이고, 그렇지 않다면 메모리에 독립적으로 존재하는 두 개의 객체라는 뜻입니다.
a = {"name": '철수', "age": 14}
b = {"name": '철수', "age": 14}
print(id(a), id(b), a is b)
a = b
print(id(a), id(b), a is b)
2672326162752 2672326162816 False
2672326162816 2672326162816 True
파이썬 내장 가변 객체를 만들 때 쓰이는 기호는 새로운 객체를 만듭니다. 즉 [...]
를 쓸 때마다 새로운 리스트가 만들어지는 것이고, {...}
를 쓸 때마다 새로운 set
혹은 dict
가 만들어지는 셈이죠. list(...)
와 같이 클래스 이름을 통해 생성하는 것도 당연히 새로운 객체를 만드는 과정이구요. a
와 b
가 처음에 정의될 때, 각자 리터럴로 생성했으니 각자 다른 객체를 가리키게 되며, id
값도 다르게 됩니다.
반면 가변 객체를 대입하는 것은 새로운 객체를 만들지 않고 여러 개의 이름을 붙여주는 것에 불과합니다. 위 예제와 같이 a = b
를 하게 되면 a
는 b가 가리키던 객체를 같이 가리키게 되며, 즉 a
와 b
가 같은 객체를 가리킨다는 것이고, 만약 a
를 수정하게 되면 바로 b
영향이 갑니다.
None
None
은 가변 객체이기는 하지만 불변성을 논할 필요는 없습니다. None
이라는 객체는 NoneType
의 유일한 객체입니다. 파이썬 프로그램이 실행된 이후 NoneType
으로 만들어지는 객체는 None
이 유일무이합니다. 이를 좀 더 전문 용어로 싱글톤이라고 이야기하지만, 중요한 건 아닙니다. 쉽게 설명하면 코드 상에서 보이는 None
은 전부 같은 객체(id
값이 같은 객체)라는 것입니다.
print(id(None))
a = None
print(id(a))
def hi(p = None):
print(id(p))
hi()
140718916847744
140718916847744
140718916847744
그러므로 None
과 비교할 일이 있을 때에는 정체성 검사인 is
를 이용합니다. 값 비교인 ==
를 써도 제대로 동작하기는 하지만, None
에는 is
를 사용하는 것이 논리적으로도 맞고 파이썬 고인물들의 국룰입니다.
함수 인수의 기본값을 가변 객체로 두지 않아야 하는 이유
대개 클래스를 설계할 때 함수의 기본값으로 비어있거나 없는 값을 설정하곤 합니다. 지금 클래스를 설계할 것은 아니고, 단순히 예시로 보기 위해서 함수 하나를 상상하여 만들어봅시다.
add_5_in_list
함수는 리스트 하나를 인수 하나를 받습니다. 만약 인수가 아무것도 주어지지 않는다면 빈 리스트에5
하나를 추가하여 리턴합니다. 리스트가 주어진다면 그 리스트에5
를 추가하고 그 리스트를 리턴합니다.
add_5_in_list
함수를 정의한 후 이어서 테스트 코드를 작성해봅시다.
def add_5_in_list(ls = []):
ls.append(5)
return ls
print(add_5_in_list())
ls = [3]
add_5_in_list(ls)
print(ls)
print(add_5_in_list())
print(add_5_in_list())
[5]
[3, 5]
[5, 5]
[5, 5, 5]
잘 동작하는 것 같다가도, add_5_in_list
가 인수 없이 여러번 호출이 되니 [5, 5, 5]와 같이 5가 여러 개로 늘어나는 것을 확인할 수 있습니다. 이게 무슨 일일까요. add_5_in_list
가 호출될 때마다 우리는 단지 [5]
만을 받아야 하는 말이지요!!
비밀은 함수를 정의할 때 파이썬 인터프리터가 하는 동작에 있습니다. 인수 부분에서 기본 값을 설정하는 ls = []
부분은 사실 함수가 호출할 때마다 동작하는 것이 아니라, 함수를 정의하는 순간 해당 함수의 기본값을 모아두는 __defaults__
라는 특별 속성에 저장됩니다. 한번 직접 이 __defaults__
라는 아이를 직접 출력해보도록 하겠습니다.
def add_5_in_list(ls = []):
ls.append(5)
return ls
print(add_5_in_list.__defaults__)
add_5_in_list()
add_5_in_list()
add_5_in_list()
print(add_5_in_list.__defaults__)
([],)
([5, 5, 5],)
위 코드에서는 add_5_in_list
함수를 정의한 직후 이 리스트 내부에 있는 __defaults__
속성을 print
해보니 하나의 빈 리스트가 담겨있는 튜플 하나를 보실 수 있습니다. 아무런 인수 없이 함수를 호출하기만 하는데도 __defaults__
는 계속해서 변형됩니다!
아까 가변 객체는 대입될 때 새로운 객체가 생기는 것이 아니라 별도의 별명으로 취급된다고 하였죠? 그러므로 매 호출 때마다 ls
인수의 기본값은 모두 같은 객체를 가리키게 되어 같은 리스트에 연이어 세 번 append
를 실행하게 되었던 것입니다. 이는 결단코 우리의 의도가 아닙니다. 위와 같은 상황을 그림으로 표현하면 다음과 같습니다.
이를 수정하기 위해서는, 아래와 같이 다소 보수적인 방법을 취해야 합니다.
def add_5_in_list(ls = None):
if ls is None:
ls = []
ls.append(5)
return ls
print(add_5_in_list())
ls = [3]
add_5_in_list(ls)
print(ls)
print(add_5_in_list())
print(add_5_in_list())
[5]
[3, 5]
[5]
[5]
바로 기본 값으로 None
을 두고, 함수의 내용에서 이 인자가 None
인지 아닌지 정체성을 검사하는 과정을 넣는 것입니다. 첨언하자면 이 예제가 가장 흔하게 None
과의 정체성을 직접적으로 비교하는 형태입니다. None
을 함수 인자의 기본값으로 두는 구현은 유명한 파이썬 라이브러리에서도 쉽게 찾아볼 수 있습니다.
불변형 객체에 대한 정체성 검사?
다음 코드의 결과는 사실 True
가 나올지 False
가 나올지 모릅니다. 확률적이라는 말인가요? 그게 아닙니다.
a = 123
print(a is 123)
c:/Users/tooth/Desktop/test2.py:2: SyntaxWarning: "is" with a literal. Did you mean "=="?
print(a is 123)
True
일단 True
가 나오긴 하는 군요. 생전 보지 못한 SyntaxWarning
까지 보게 됩니다. is
가 리터럴이랑 같이 쓰이고 있다. ==
를 하려고 했던 것이냐?고 파이썬 인터프리터가 물어보니까 일단 is
를 안쓰고 ==
를 써야 할 것만 같습니다. 근데 왜 is
를 쓰면 안 될까요?
is
의 역할을 다시 생각해봅시다. is
의 역할은 객체의 정체성을 검사하는 것이며 내부적으로는 id
값을 비교합니다. 숫자에 정체성을 검사한다는 말은 4라는 숫자에도 정직한 4, 못생긴 4, 러시아에 사는 4, 어찌되었든 다른 4가 존재하기 때문에 그를 구별하겠다는 뜻입니다. 이는 말이 안 됩니다! 4가 다 같은 4지, 달리 존재할 수 있는 4가 존재할 수 있나요? 그렇기 때문에 모든 불변형 객체에 대해서 is
연산을 먹이는 것은 어불성설인 셈이죠.
어쨌거나 is
를 쓰면 연산은 하게 됩니다. 본래 동작대로 id
값을 비교하긴 하겠지요. 그러나 파이썬 구현체가 어떻게 구현되어 있느냐에 따라 효율을 위해 같은 4라도 id
를 다르게 두었을 수도 있습니다. (어쨌거나 대개 같게 두긴 하겠지만) 즉 위 예제에서 id(a)
와 id(123)
의 값이 같다는 보장이 없습니다.
그러므로 불변형 객체에 대해 같은지를 비교할 때에는 is
가 아니라 ==
를 사용하여야 합니다. 아래 예제가 올바른 예제입니다.
a = 123
print(a == 123)
True
연습 문제
- 정체성이란 무엇인가?
- 정체성을 비교하려면 어떻게 해야 하는가?
- 가변 객체와 불변 객체의 차이는 무엇인가?
프로그래밍 문제
추가 예정
프로그래밍 문제 정답
추가 예정
- 프롤로그
- 개발 첫걸음
- 파이썬 기초
- 파이썬 중급
- 파이썬 고급
- 내장 함수 톺아보기
- 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
- 변수의 범위 – 이름 검색의 범위
- 파이썬 심화
- 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
- 데코레이터 – 함수의 기능을 강화하기
- 프로퍼티
- 제너레이터
- async와 await
- 객체로서의 클래스 – 클래스를 동적으로 정의하기
- 파이썬 프로젝트 실습
- 원카드 게임 만들기 (1)
- 원카드 게임 만들기 (2)
- 원카드 게임 만들기 (3) (작성중)
- 턴제 자동 전투 게임 만들기 (작성중)
- 실전 (파이썬 외적인 것들)
- 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
- 유니코드 – 컴퓨터에서 문자를 표기하는 방법
- html, css, 인터넷 – 자동화 첫 걸음 내딛기
- 네트워크 – 인터넷으로 통신하는 방법
- 문서 – 문맥을 읽어보기
One thought on “파이썬 강좌 – 정체성, 동질성 ~ 객체의 성질”