1. 불변과 가변 객체의 개념
파이썬에서 모든 데이터는 객체로 취급된다. 이 객체들은 크게 두 가지 범주로 나눌 수 있다: 불변(Immutable) 객체와 가변(Mutable) 객체. 이 두 종류의 객체는 메모리 관리와 데이터 조작 방식에서 근본적인 차이를 보인다.
2. 불변(Immutable) 객체
불변 객체는 한 번 생성된 후에는 그 내용을 변경할 수 없는 객체를 말한다. 파이썬에서 대표적인 불변 객체로는 정수(int), 부동소수점(float), 문자열(str), 튜플(tuple), 불리언(bool) 등이 있다. 이러한 객체들은 한 번 생성되면 그 값을 직접적으로 수정할 수 없다.
예를 들어, 문자열을 살펴보자.
s = "Hello"
s[0] = "h" # TypeError: 'str' object does not support item assignment
위 코드에서 문자열 's'의 첫 번째 문자를 변경하려고 하면 TypeError가 발생한다. 이는 문자열이 불변 객체이기 때문이다.
3. 가변(Mutable) 객체
반면, 가변 객체는 생성된 후에도 그 내용을 변경할 수 있는 객체를 말한다. 파이썬에서 대표적인 가변 객체로는 리스트(list), 딕셔너리(dict), 집합(set) 등이 있다. 이러한 객체들은 생성 후에도 요소를 추가, 삭제, 수정할 수 있다.
리스트를 예로 들어보자.
lst = [1, 2, 3]
lst[0] = 10 # 가능
lst.append(4) # 가능
위 코드에서 리스트 'lst'의 요소를 변경하고 새로운 요소를 추가하는 것이 가능하다. 이는 리스트가 가변 객체이기 때문이다.
4. 메모리 관리와 성능
불변 객체와 가변 객체는 메모리 관리 방식에서 큰 차이를 보인다. 이는 파이썬 프로그램의 성능과 효율성에 직접적인 영향을 미친다.
4-1. 불변 객체의 메모리 관리
불변 객체는 값이 변경될 때마다 새로운 객체가 생성된다. 이는 메모리 사용량을 증가시킬 수 있지만, 파이썬은 이를 최적화하기 위해 특별한 메커니즘을 사용한다.
예를 들어, 정수나 작은 문자열의 경우 파이썬은 객체 인터닝(interning)이라는 기법을 사용한다. 이는 자주 사용되는 값들을 미리 생성해 놓고 재사용하는 방식이다.
a = 'hello'
b = 'hello'
print(a is b) # True
위 코드에서 'a'와 'b'는 같은 객체를 참조하고 있다. 이는 메모리 사용을 효율적으로 만들어준다.
4-2. 가변 객체의 메모리 관리
가변 객체는 내용이 변경될 때 새로운 객체를 생성하지 않고 기존 객체를 수정한다. 이는 메모리 사용량을 줄일 수 있지만, 여러 변수가 같은 객체를 참조할 때 예상치 못한 부작용을 일으킬 수 있다.
list1 = [1, 2, 3]
list2 = list1
list2.append(4)
print(list1) # [1, 2, 3, 4]
위 코드에서 'list2'를 수정했지만 'list1'도 함께 변경되었다. 이는 두 변수가 같은 객체를 참조하고 있기 때문이다.
5. 함수 인자 전달 방식
불변 객체와 가변 객체는 함수에 인자로 전달될 때 다르게 동작한다. 이 차이를 이해하는 것은 효과적인 함수 설계와 버그 방지에 중요하다.
5-1. 불변 객체의 전달
불변 객체가 함수에 전달될 때, 값에 의한 전달(pass by value)처럼 동작한다. 함수 내에서 인자를 변경해도 원본 객체에는 영향을 미치지 않는다.
def modify_string(s):
s += " World"
print("Inside function:", s)
text = "Hello"
modify_string(text)
print("Outside function:", text)
# 출력:
# Inside function: Hello World
# Outside function: Hello
위 예제에서 'modify_string' 함수 내부에서 문자열을 수정했지만, 원본 'text' 변수는 변경되지 않았다.
5-2. 가변 객체의 전달
가변 객체가 함수에 전달될 때, 참조에 의한 전달(pass by reference)처럼 동작한다. 함수 내에서 인자를 수정하면 원본 객체도 함께 변경된다.
def modify_list(lst):
lst.append(4)
print("Inside function:", lst)
numbers = [1, 2, 3]
modify_list(numbers)
print("Outside function:", numbers)
# 출력:
# Inside function: [1, 2, 3, 4]
# Outside function: [1, 2, 3, 4]
이 예제에서는 'modify_list' 함수 내부에서 리스트를 수정했고, 원본 'numbers' 리스트도 함께 변경되었다.
6. 불변성의 이점
불변 객체는 여러 가지 이점을 제공한다. 이러한 이점들은 특히 대규모 시스템이나 복잡한 애플리케이션에서 더욱 두드러진다.
a. 스레드 안전성
불변 객체는 여러 스레드에서 동시에 접근해도 안전하다. 값이 변경되지 않기 때문에 동기화 문제가 발생하지 않는다.
b. 캐싱
불변 객체는 그 값이 변하지 않기 때문에 안전하게 캐시할 수 있다. 이는 성능 향상에 도움이 된다.
c. 예측 가능성
불변 객체는 상태가 변하지 않기 때문에 프로그램의 동작을 예측하기 쉽다. 이는 디버깅과 유지보수를 용이하게 만든다.
d. 해시 테이블 키로 사용
불변 객체는 그 값이 변하지 않기 때문에 딕셔너리의 키나 집합의 요소로 사용하기에 적합하다.
# 튜플(불변)을 딕셔너리 키로 사용
coord = {(0, 0): 'origin', (1, 0): 'right', (0, 1): 'up'}
# 리스트(가변)를 키로 사용하려고 하면 오류 발생
# coord_error = {[0, 0]: 'origin'} # TypeError
7. 가변성의 이점
가변 객체 역시 특정 상황에서 유용하게 사용될 수 있다. 가변 객체의 주요 이점은 다음과 같다.
a. 메모리 효율성
큰 데이터 구조를 다룰 때, 가변 객체를 사용하면 새로운 객체를 생성하지 않고도 내용을 수정할 수 있어 메모리를 절약할 수 있다.
b. 성능
대량의 데이터를 자주 수정해야 하는 경우, 가변 객체를 사용하면 새 객체를 생성하는 오버헤드를 줄일 수 있다.
c. 유연성
가변 객체는 동적으로 내용을 변경할 수 있어, 실시간으로 데이터를 추가하거나 수정해야 하는 상황에 적합하다.
# 리스트를 사용한 효율적인 데이터 수집
data = []
for i in range(1000000):
data.append(i) # 기존 리스트에 추가, 새 객체 생성 없음
8. 불변 객체와 가변 객체의 활용
8-1. 불변 객체의 활용
8-1-1. 상수 정의
불변 객체는 프로그램 전체에서 변경되지 않아야 하는 값을 정의할 때 유용하다.
PI = 3.14159
MAX_USERS = 100
8-1-2. 함수형 프로그래밍
불변 객체는 함수형 프로그래밍 패러다임에서 중요한 역할을 한다. 부작용 없는 순수 함수를 작성할 때 불변 객체를 사용하면 예측 가능성과 테스트 용이성이 향상된다.
def add_to_tuple(t, element):
return t + (element,)
original = (1, 2, 3)
new = add_to_tuple(original, 4)
print(original) # (1, 2, 3)
print(new) # (1, 2, 3, 4)
8-1-3. 멀티스레딩
불변 객체는 여러 스레드에서 안전하게 공유될 수 있다. 동시성 문제를 피하고 싶을 때 불변 객체를 사용하면 좋다.
import threading
shared_data = "This is shared among threads"
def thread_function(data):
print(f"Thread {threading.current_thread().name}: {data}")
threads = []
for i in range(5):
t = threading.Thread(target=thread_function, args=(shared_data,))
threads.append(t)
t.start()
for t in threads:
t.join()
8-1-4. 디폴트 인자
함수의 디폴트 인자로 불변 객체를 사용하면 예상치 못한 부작용을 방지할 수 있다.
def add_to_list(element, target=[]): # 잘못된 방식
target.append(element)
return target
def add_to_list_safe(element, target=None): # 안전한 방식
if target is None:
target = []
target.append(element)
return target
8-2. 가변 객체의 활용
8-2-1. 데이터 구조 구현
가변 객체는 동적 데이터 구조를 구현할 때 유용하다. 예를 들어, 그래프, 트리, 캐시 등을 구현할 때 가변 객체를 사용할 수 있다.
class Graph:
def __init__(self):
self.nodes = {}
def add_edge(self, node1, node2):
if node1 not in self.nodes:
self.nodes[node1] = set()
if node2 not in self.nodes:
self.nodes[node2] = set()
self.nodes[node1].add(node2)
self.nodes[node2].add(node1)
g = Graph()
g.add_edge('A', 'B')
g.add_edge('B', 'C')
print(g.nodes) # {'A': {'B'}, 'B': {'A', 'C'}, 'C': {'B'}}
8-2-2. 성능 최적화
대량의 데이터를 처리할 때, 가변 객체를 사용하면 메모리 사용량과 처리 시간을 줄일 수 있다.
def process_large_data(data):
result = []
for item in data:
# 복잡한 처리 로직
processed = item * 2
result.append(processed)
return result
large_data = list(range(1000000))
processed_data = process_large_data(large_data)
8-2-3. 객체 상태 관리
객체 지향 프로그래밍에서 객체의 상태를 동적으로 변경해야 할 때 가변 객체를 사용한다.
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
return True
return False
account = BankAccount(1000)
account.deposit(500)
print(account.balance) # 1500
account.withdraw(200)
print(account.balance) # 1300
'프로그래밍 > 파이썬' 카테고리의 다른 글
파이썬 - 정규표현식 (0) | 2024.11.30 |
---|---|
파이썬 문자열 포매팅: f-string, str.format(), % 연산자 비교 (0) | 2024.11.30 |
파이썬 - Set (0) | 2024.11.29 |
파이썬 - 딕셔너리 (0) | 2024.11.29 |
파이썬 - 제너레이터 (0) | 2024.11.28 |