본문 바로가기

Computer/Python

파이썬과 객체

반응형

파이썬의 설계 철학은 간단함입니다. C/C++ 에서와 같이 포인터라는 복잡한 개념을 명시적으로 사용하지 않고 메모리 관리를 별도로 할 필요없이 속도 대신 범용성을 추구하는 usability가 설계의 근본으로 자리잡고 있습니다. 이는 Zen of Python 에서도 잘 드러납니다. 언제나 Zen of Python을 읽을 때마다 내 자신이 파이썬 철학에 맞게 구현하고 있는지 반성하게 됩니다...


Beautiful is better than ungly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!


이 철학의 모든 것은 객체로부터 출발합니다. 파이썬에서 모든 것은 객체입니다. 그렇다면 객체는 어떻게 구성되어 있을까요?

>>> isinstance(1, object)
True
>>> isinstance(list(), object)
True
>>> isinstance(True, object)
True
>>> def foo():
...     pass
...
>>> isinstance(foo, object)
True

파이썬의 모든 것은 객체이고 각 객체는 무조건 1) 메모리 관리를 위한 reference count, 2) CPython의 런타임을 위한 타입, 3) 객체의 실체인 value를 포함하는 구조체로 구성되어 있습니다. 하지만 모든 객체가 같지는 않습니다. 대표적으로 리스트, 딕셔너리, 세트 등은 값이 변할 수 있는 mutable objects 입니다. 객체가 담긴 메모리 주소를 알 수 있는 id() 내장함수와 두 객체가 같은 메모리 주소를 가지는지 판단하는 is 키워드로 객체에 대해 간단히 살펴보겠습니다.


먼저 5라는 정수를 x라는 변수에 대입하고 이를 6으로 수정하면 x에 담긴 객체가 달라지므로 id 값이 달라집니다.

>>> x = 5
>>> id(x)
94529957049376

>>> x += 1
>>> x
6
>>> id(x)
94529957049408

불변 객체인 문자열에 대해서 보면 "+=" 오퍼레이션으로 s에 담긴 메모리 주소가 변한 것을 확인할 수 있습니다. 또한, 문자열은 불변 객체이므로 값을 수정하려하면 에러가 발생합니다.

>>> s = "real_python"
>>> id(s)
140637819584048
>>> s += "_rocks"
>>> s
'real_python_rocks'
>>> id(s)
140637819609424

>>> s[0] = "R"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

가변 객체인 리스트는 값이 바뀔 수 있으므로 값을 추가하거나 변경해도 id 값이 같습니다.

>>> my_list = [1, 2, 3]
>>> id(my_list)
140637819575368
>>> my_list.append(4)
>>> my_list
[1, 2, 3, 4]
>>> id(my_list)
140637819575368
>>> my_list[0] = 0
>>> my_list
[0, 2, 3, 4]
>>> id(my_list)
140637819575368

 

Variables in python

파이썬에서의 변수 (variable)은 C/C++ 에서의 변수와 아예 다릅니다. 실제로는 C/C++ 에서와 같이 변수에 값을 할당한다는 개념이 아닌, 객체의 이름을 뜻하며 C/C++ 에서의 변수가 아닙니다.

Variables in C

"int x = 2337" 같이 변수에 값을 할당하면 C에서는 1) 정수를 담기 위한 메모리 공간을 할당하고, 2) 할당된 공간에 값을 부여하고, 3) x로 하여금 그 값을 가리키게 합니다. 그림으로 Figure 1과 같이 표현할 수 있습니다.

Figure 1

이 상황에서 "x = 2338" 코드를 수행한다면 2338이라는 새로운 value가 기존 값을 덮어쓰게 됩니다. 즉, 변수 x가 mutable 하므로 Figure 2와 같이 x에 할당된 주소는 그대로 유지한 채 value만 바뀌게 됩니다. 여기서 중요한 점은 x는 단순한 변수의 이름을 넘어서 특정한 메모리 공간을 점유한다는 것이죠. 

Figure 2

또한, "int y = x"를 수행한다면 Figure 3과 같이 변수 y를 위한 새로운 메모리 공간을 할당합니다.

Figure 3

Names in python

보통 편의상 변수라고 하지만 파이썬에서는 실제로는 객체에 부여된 이름에 불과합니다. 위와 마찬가지로 파이썬에서 "x = 2337" 이라하면 1) CPython 에서 사용되는 모든 파이썬 객체의 기본 C 구조체인 PyObject 생성하고, 2) PyObject의 typecode를 정수로 설정하고, 3) PyObject의 value를 2337로 설정하고, 4) x라는 이름을 생성하고 이것이 PyObject를 가리키게 하면서, 5) PyObject의 refcount를 1 증가시킵니다. 이를 그림으로 표현하면 Figure 4와 같습니다.

Figure 4

C에서와의 차이점은 x가 특정 메모리 공간을 점유하는 것이 아니라 새로 생성된 PyObject가 메모리 공간을 점유하고 x는 그것을 가리키는 이름인 것입니다. 즉, C에서처럼 변수 x가 특정 메모리 공간을 차지하지 않습니다. 이 상황에서 "x = 2338" 로 값을 수정하면 어떻게 될까요? 새로운 값 2338을 담은 PyObject를 생성하면서 x가 새로운 PyObject의 이름이 되면서 refcount가 예전 PyObject에서는 1 감소하고 새로운 PyObject에서 1 증가합니다. 즉, x가 메모리 공간을 점유하고 있지 않으니 엄밀히 말하면 할당 (assignment)가 아니게 됩니다. 추가적으로 기존의 PyObject의 refcount가 0이 되었으므로 garbage collector에 의해 삭제됩니다. 

Figure 5

여기서 "y=x" 구문을 수행한다고 해도 C에서처럼 y를 위한 새로운 메모리 공간이 생성되는 것이 아니라 Figure 6과 같이 기존의 PyObject를 가리키는 새로운 이름이 되면서 refcount가 2로 증가하게 됩니다. 따라서 "y is x" 구문을 수행하면 True가 출력됩니다. 

Figure 6

여기서 "y += 1"을 수행한다면 2339가 담긴 새로운 PyObject가 생성되므로 "y is x"가 False가 되며, refcount가 조정됩니다. 정리하면 파이썬에서의 할당 (=) 오퍼레이션은 변수에 할당하는 것이 아니라 객체에 레퍼런스할 이름을 부여하는 것이라 볼 수 있습니다. 

Figure 7

 

Tricky part

그렇다면 같은 객체를 가리키는 이름 2개가 있다면 그 둘의 id값은 같을까요? 놀랍게도 다음 코드와 같이 더하기를 통해서 1000을 구성할 경우 id값이 달라지게 됩니다. 여기서는 이름 x에 붙은 1000이라는 객체가 있고, 499, 501 두 객체가 합쳐질 때 1000이라는 객체를 새롭게 만들기 때문입니다. 

>>> x = 1000
>>> y = 1000
>>> x is y
True

>>> x = 1000
>>> y = 499 + 501
>>> x is y
False

하지만 다음과 같이 20에 대해서는 위와 결과가 다릅니다.

>>> x = 20
>>> y = 19 + 1
>>> x is y
True

이는 20이 파이썬의 interned 객체이기 때문입니다. 파이썬은 매우 자주 쓰이는 특정 객체들을 메모리에 미리 생성하는데 이를 interned 객체라 하고 파이썬 3.7 기준으로 1) -5에서 256사이의 정수, 2) 아스키문자, 숫자, 언더바가 해당됩니다. 이렇게 하는 이유는 자주 쓰이는 객체에 대해 메모리 할당 콜을 방지하여 속도를 어느정도 높이기 위함이고 20글자 이하의 문자열은 자동으로 interned 됩니다. (특히 interned 객체가 사전의 키로 사용될 경우 문자열 비교가 아닌 주소 비교 (포인터 비교)로 수행하기 때문에 속도측면에서 향상됩니다.)

>>> s1 = "realpython"
>>> id(s1)
140696485006960
>>> s2 = "realpython"
>>> id(s2)
140696485006960
>>> s1 is s2
True

하지만 아스키 문자가 아닌 느낌표가 들어가 있을 경우 id값이 달라지게 됩니다. 이를 sys 모듈의 getrefcount 함수를 통해 확인해보면 getrefcount("Real Python!") 을 라인 별로 수행하더라도 매번 새로운 객체가 생성되기 때문에 일정하게 유지됩니다. 

>>> s1 = "Real Python!"
>>> s2 = "Real Python!"
>>> s1 is s2
False

위의 s1과 s2를 같은 interned 객체를 가리키게 하려면 sys 모듈의 intern 함수에 해당 객체를 등록하면 됩니다. 예를 들어 다음과 같이 'why do pangolins dream of quiche' 라는 문자열을 intern 함수에 전달하면 이 문자열이 interned 객체로 등록됩니다. 하지만 intern 함수에 전달하지 않으면 같은 값의 문자열이라도 다른 객체가 됩니다. 따라서 sys.intern 함수를 이용해 같은 값을 가지는 두 개의 객체를 생성하지 않고 하나의 같은 객체를 가리키게 함으로써 메모리를 절약할 수 있습니다.

>>> import sys
>>> a = sys.intern('why do pangolins dream of quiche')
>>> a
'why do pangolins dream of quiche'

>>> b = sys.intern('why do pangolins dream of quiche')
>>> b
'why do pangolins dream of quiche'

>>> b is a
True

>>> c = 'why do pangolins dream of quiche'
>>> c is a
False
>>> c is b
False
반응형

'Computer > Python' 카테고리의 다른 글

namedtuple 인스턴스 확인  (0) 2021.07.19
파이썬의 memory management  (3) 2021.07.14
파이썬의 Namespace와 Scope  (0) 2021.07.13
파이썬의 매개변수 전달 방식  (0) 2021.07.13
Property, Setter, Getter  (0) 2021.06.28