1. 변수와 메모리 참조(Variables and memory reference)
Python에서는 모든 것이 객체로 취급된다. 객체는 힙(heap) 메모리에 저장되며, 변수는 이 객체들에 대한 참조를 보관한다. 또한, 파이썬 메모리 매니저가 객체의 생성 및 삭제, 그리고 메모리 할당과 반환을 관리하므로 개발자는 메모리 관리에 대해 신경 쓸 필요가 거의 없다.
변수와 객체 사이의 연결을 설명하는 좋은 방법은 id() 함수를 사용하는 것이다. 이 함수는 파이썬 내부에서 객체의 고유한 ID를 반환하는데, 이 ID는 실제로는 객체가 저장된 메모리 주소이다. id() 함수에 hex() 함수를 적용하면 이 주소를 16진수로 변환하여 볼 수 있다.
my_var = 10
print(id(my_var)) # 고유 ID, 즉 메모리 주소 출력
print(hex(id(my_var))) # 메모리 주소를 16진수로 변환하여 출력
2. 참조 카운트 (Reference Counting)
Python 메모리 매니저는 "참조 카운트"라는 메커니즘을 사용하여 객체의 생명 주기를 관리한다. 참조 카운트는 간단히 말하면 특정 객체를 참조하는 변수의 수를 의미한다. 만약 참조 카운트가 0이 되면, 해당 객체는 더 이상 필요하지 않다고 판단하고 메모리에서 해제된다.
Python의 sys 모듈에는 getrefcount() 함수가 있어 객체의 참조 카운트를 알아낼 수 있다. 그런데 이 함수를 사용하면 함수 호출에 의해 참조 카운트가 일시적으로 1 증가하므로, 원래 참조 카운트보다 1 큰 값을 반환한다.
import sys
my_var = 10
print(sys.getrefcount(my_var)) # 참조 카운트 출력
3. 가비지 컬렉션 (Garbage Collection)
Python은 가비지 컬렉션 기능을 제공하여 메모리를 자동으로 관리한다. 이 기능은 참조 카운트가 0이 된 객체를 메모리에서 자동으로 해제한다.
그런데 이런 방식으로는 "순환 참조(circular reference)"와 같은 문제를 해결할 수 없다. 순환 참조는 서로 다른 객체가 서로를 참조하면서 참조 카운트가 0이 되지 않는 상태를 의미한다. 이 경우, 두 객체는 더 이상 사용되지 않지만 참조 카운트가 0이 아니므로 메모리에서 해제되지 않아 메모리 누수를 일으킬 수 있다.
Python의 가비지 컬렉션 모듈인 gc는 이러한 순환 참조 문제를 해결하기 위해 설계되었다. gc는 순환 참조를 감지하고 이를 해제하여 메모리 누수를 방지한다. Python 3.4 이후의 버전에서는 이러한 가비지 컬렉션 기능이 기본적으로 활성화되어 있다.
import gc
# 순환 참조 생성
a = {}
b = {}
a['b'] = b
b['a'] = a
# 순환 참조 확인
print(gc.get_referents(a)) # a가 참조하는 객체 출력
print(gc.get_referents(b)) # b가 참조하는 객체 출력
# 순환 참조 해제
del a
del b
gc.collect() # 가비지 컬렉션 실행
마지막으로, 객체가 메모리에서 해제될 때 __del__() 메소드가 호출된다. 이 메소드는 파이썬의 빌트인 메소드로, 객체의 소멸자 역할을 한다. __del__() 메소드를 재정의하면 객체가 소멸될 때 원하는 작업을 수행하도록 할 수 있다.
class MyClass:
def __del__(self):
print("MyClass instance will be deleted.")
mc = MyClass()
del mc # "MyClass instance will be deleted." 출력
위의 내용들은 Python의 메모리 관리와 관련된 중요한 개념이다. Python에서는 대부분의 메모리 관리 작업이 자동으로 이루어지지만, 이런 내부 동작 원리를 이해하는 것이 Python 프로그래밍에 있어 중요한 역량이다.
4. Dynamic Typing vs Static Typing
정적 타이핑(static typing) 언어는 변수의 타입이 선언 시점에 결정되며, 이후 변경할 수 없다. 즉, 데이터 타입과 변수명은 서로 관련이 있다. C++나 Java와 같은 언어가 이에 해당된다.
Python은 동적 타이핑(dynamic typing) 언어로, 변수의 타입이 실행 시점에 결정된다. 즉, 변수를 선언할 때 타입을 지정하지 않아도 되며, 이후 어떤 타입의 객체든 변수에 할당할 수 있다. 이는 코드를 더 간결하게 만들어 준다. Python에서 변수의 현재 타입을 확인하려면 type() 함수를 사용하면 된다.
var = 10 # 초기에는 정수를 가리킵니다.
print(type(var)) # <class 'int'>
var = "Hello, Python!" # 이후에는 문자열을 가리킵니다.
print(type(var)) # <class 'str'>
5. Object Mutability
Python의 객체는 불변(immutable) 객체와 가변(mutable) 객체로 나눌 수 있다.
불변 객체는 생성 후 그 상태를 변경할 수 없는 객체로, 숫자, 문자열, 튜플, 프로즌 셋 등이 있다. 불변 객체를 다른 변수에 할당하면, 새로운 메모리 공간이 생성되고, 새로운 객체가 생성된다.
x = 10
y = x
print(id(x) == id(y)) # True
x = x + 1
print(id(x) == id(y)) # False
가변 객체는 리스트, 딕셔너리, 셋, 사용자 정의 클래스 등으로, 생성 후에도 내부 상태를 변경할 수 있다. 가변 객체를 다른 변수에 할당하면 동일한 메모리 공간을 가리키게 된다.
x = [1, 2, 3]
y = x
print(id(x) == id(y)) # True
x.append(4)
print(id(x) == id(y)) # True
이 불변성과 가변성은 함수에서 파라미터를 전달할 때의 동작에도 영향을 미친다. 불변 객체는 사이드 이펙트(side-effects)에서 안전하며, 이는 함수 내에서 객체의 상태를 예상치 못하게 변경하는 것을 막아준다.
6. Shared Reference and Mutability
Python에서는 불변 객체에 대한 변수 할당이 동일한 메모리 주소를 가리키도록 만든다. 이를 "공유 참조(shared reference)"라고 한다. 그러나 이러한 특징은 가변 객체에는 적용되지 않는다.
a = 10 # 불변 객체
b = a
print(id(a) == id(b)) # True
c = [1, 2, 3] # 가변 객체
d = c
print(id(c) == id(d)) # True
c.append(4)
print(id(c) == id(d)) # 여전히 True, 상태 변경이 메모리 주소에 영향을 미치지 않음
7. Variable Equality
Python에서는 두 가지 방법으로 변수의 동등성을 비교할 수 있다. is는 두 변수가 동일한 메모리 주소를 가리키는지를 확인하고, ==는 두 변수의 값을 비교한다.
# Memory Address
print(var_1 is var_2) # 메모리 주소 비교
# Object State (data)
print(var_1 == var_2) # 값 비교
이 두 방법은 결과가 다를 수 있다. 예를 들어, 두 리스트가 다른 메모리 주소를 가리키더라도 같은 요소를 가지고 있다면 ==의 결과는 True가 된다.
list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 is list2) # False
print(list1 == list2) # True
8. Everything is an Object
Python에서는 모든 것이 객체다. 이는 함수, 클래스, 타입도 포함한다. 따라서 함수는 변수에 할당할 수 있고, 다른 함수의 인자로 전달할 수도 있으며, 함수에서 반환할 수도 있다.
def shout(text):
return text.upper()
# 함수 할당
yell = shout
# 함수 호출
print(yell('hello')) # 'HELLO'
이러한 특성은 고차 함수(high-order function)를 작성하거나, 함수형 프로그래밍 패러다임을 따르는데 유용합니다.
6. Python Optimizations: Interning
Python은 작은 정수나 일부 문자열에 대해 interning이라는 최적화 방법을 사용한다. 이는 -5에서 256 사이의 정수와 불변 문자열에 대해 동일한 객체를 재사용함으로써 메모리를 효율적으로 사용한다. 이는 Python의 메모리 관리 전략 중 하나로, 다른 프로그래밍 언어에서도 유사한 전략을 볼 수 있다.
a = 256
b = 256
print(a is b) # True가 출력됨
c = 257
d = 257
print(c is d) # False가 출력됨
7. Python Optimizations: String Interning
String interning은 동일한 불변 문자열이 여러 번 생성될 경우 메모리에 한 번만 저장하는 기법이다. 이는 메모리와 속도 최적화에 도움이 다.
import sys
# sys.intern을 사용하여 문자열 인터닝 강제
s1 = sys.intern('hello world')
s2 = sys.intern('hello world')
s3 = 'hello world'
# s1과 s2는 동일한 메모리 주소를 가리킵니다.
print(s1 is s2) # True
# s3는 다른 메모리 주소를 가리킵니다.
print(s1 is s3) # False
sys.intern() 함수를 사용하여 명시적으로 인터닝되므로, 객체를 참조한다. 이런 식으로 문자열 인터닝을 사용하면, 특히 문자열 비교가 빈번하게 발생하는 경우 성능 향상을 이룰 수 있다. 이런 기법은 큰 데이터셋에서 또는 문자열을 자주 비교하는 경우에 효율적이다.
8. Python Optimizations: Peephole
Python은 peephole 최적화를 통해 상수 표현식을 미리 계산하거나 가변 객체를 불변 객체로 대체하는 등의 작업을 수행한다.
# Before Optimization
if e in [1,2,3]:
pass
# After Optimization
if e in (1,2,3): # tuple로 대체
pass
# More Optimization
if e in {1,2,3}: # set로 대체
pass
'Python study' 카테고리의 다른 글
텍스트(text)와 바이트(byte) (0) | 2024.05.04 |
---|---|
딕셔너리(dictionary)와 집합(set) (0) | 2024.04.29 |
시퀀스(sequence) - 슬라이싱(slicing), bisect, 다른 시퀀스 (2) | 2024.04.25 |
시퀀스(sequence) - 지능형 리스트(List Comprehension), 튜플(tuple) (0) | 2024.03.28 |
Python Special method (magic method) (0) | 2024.03.18 |