본문 바로가기

Python study

객체 참조, 가변성, 재활용

파이썬 변수

파이썬에서 변수는 데이터를 저장하는데 사용되는 이름이다. 변수를 사용함으로써 프로그램에서 데이터를 더 쉽게 관리하고 이해할 수 있다.

  1. 변수의 정의와 할당: 파이썬에서 변수를 생성하고 값을 할당할 때는 = 연산자를 사용한다. 예를 들어,*x = 10은 x라는 이름의 변수에 10이라는 값을 할당한다.
  2. 동적 타이핑: 파이썬은 동적 타입 언어이다. 이는 변수에 특정한 데이터 타입을 사전에 지정할 필요 없이, 값을 할당하는 순간 해당 변수의 데이터 타입이 결정된다는 의미이다. 예를 들어, x = 10 후에 x = "hello"로 재할당할 수 있으며, 이 경우 x의 타입이 정수에서 문자열로 변경된다.
  3. 변수명 규칙: 변수명은 문자, 숫자, 밑줄 문자(_)를 포함할 수 있지만, 숫자로 시작할 수는 없다. 또한 파이썬의 키워드(예: if, for, class 등)와 같은 이름을 사용할 수 없다.
  4. 변수의 스코프: 변수의 스코프는 해당 변수가 프로그램 내에서 접근 가능한 범위를 말한다. 예를 들어, 함수 내에서 생성된 변수는 일반적으로 해당 함수 내에서만 접근 가능한 로컬 변수이다. 반면에 함수 밖에서 생성된 변수는 프로그램의 다른 부분에서도 접근 가능한 전역 변수가 된다.
  5. 불변성과 가변성: 파이썬에서 일부 데이터 타입은 불변(immutable)이고 일부는 가변(mutable)이다. 예를 들어, 숫자, 문자열, 튜플은 불변 타입으로, 이들의 값을 직접 변경할 수 없다. 반면에 리스트, 딕셔너리와 같은 타입은 가변이므로, 생성된 후에도 내용을 변경할 수 있다.

 

정체성, 동질성, 별명

파이썬에서 정체성(identity), 동질성(equality), 그리고 별명(aliasing)은 변수와 데이터에 관련된 중요한 개념들이다. 이 개념들을 이해하는 것은 파이썬 프로그래밍의 깊이 있는 이해를 위해 필수적이다.

1. 정체성 (Identity)

정체성은 객체의 고유성을 나타낸다. 파이썬에서 모든 객체는 메모리 상에 위치를 가지며, 이 메모리 주소를 통해 각 객체는 구별된다. 두 객체가 동일한 메모리 주소를 가리킨다면, 그 두 객체는 정체성이 같다고 한다. 파이썬에서는 is 연산자를 사용하여 두 변수가 동일한 객체를 참조하는지 검사할 수 있다.

a = [1, 2, 3]
b = a
print(a is b)  # True, a와 b는 같은 리스트 객체를 참조

2. 동질성 (Equality)

동질성은 두 변수가 참조하는 값의 동등성을 나타낸다. 이는 == 연산자를 통해 검사할 수 있다. 두 변수가 다른 객체를 참조하더라도, 그 내용이 같다면 동등하다고 볼 수 있다.

a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)  # True, a와 b는 내용이 동일

3. 별명 (Aliasing)

별명은 두 개 이상의 변수가 메모리 상의 같은 객체를 참조할 때 발생한다. 이는 특히 가변 객체를 다룰 때 주의해야 할 점이다. 한 변수를 통해 객체를 변경하면, 동일한 객체를 참조하는 다른 모든 변수에도 그 변경이 반영된다.

a = [1, 2, 3]
b = a  # b는 a의 별명
b.append(4)
print(a)  # [1, 2, 3, 4], a도 변경됨

이 세 개념은 파이썬에서 데이터를 처리하고 관리할 때 중요한 역할을 한다. 각각을 정확히 이해하고 구분할 줄 알아야, 데이터의 흐름과 변경을 예측하고 효율적으로 관리할 수 있다.

 

파이썬에서 모든 것은 객체

파이썬에서 모든 것은 객체이다. 이 객체들은 세 가지 중요한 특성을 가지고 있다: 정체성(identity), 자료형(type), 그리고 값(value). 이 중에서 정체성은 객체의 가장 근본적인 특성 중 하나로, 객체가 생성된 후에는 결코 변경될 수 없다.

1. 정체성 (Identity)

객체의 정체성은 객체가 생성될 때 한번 설정되고 그 이후에는 절대 변경되지 않는다. 이 정체성은 메모리 내에서 객체의 주소로 생각할 수 있으며, 파이썬 내부적으로 각 객체를 구분하는 데 사용된다. 이는 각 객체가 독립적인 메모리 공간에 위치함을 의미하고, 이 위치를 통해 객체를 식별할 수 있다.

id() 함수는 객체의 정체성을 나타내는 유일한 정수를 반환한다. 이 정수는 객체의 메모리 주소를 기반으로 하지만, 플랫폼이나 파이썬의 구현에 따라 달라질 수 있다.

a = 42
print(id(a))  # 예: 140703285216896

2. 자료형 (Type)

객체의 자료형은 객체가 저장할 수 있는 데이터의 종류와 해당 데이터를 처리할 수 있는 연산의 종류를 결정한다. 예를 들어, 정수형(int)은 숫자 관련 연산을, 문자열(str)은 문자열 조작 연산을 지원한다. 파이썬에서는 type() 함수를 사용하여 객체의 자료형을 확인할 수 있다.

a = 42
print(type(a))  # <class 'int'>

3. 값 (Value)

객체의 값은 객체가 저장하고 있는 데이터이다. 객체의 자료형에 따라 값의 종류와 특성이 달라진다. 값은 가변 객체의 경우 변경될 수 있지만, 불변 객체의 경우 생성 후 변경할 수 없다.

a = 42   # a는 정수 42를 값으로 가짐
b = "hello"  # b는 문자열 "hello"를 값으로 가짐

정체성 비교: is 연산자

is 연산자는 두 객체의 정체성이 같은지 즉, 같은 메모리 주소를 참조하는지 확인한다. 이는 두 변수가 실제로 동일한 객체를 가리키고 있는지를 검사할 때 사용된다.

a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)  # True, a와 b는 동일한 객체
print(a is c)  # False, a와 c는 내용은 같지만 다른 객체

이러한 세 가지 특성을 이해하는 것은 파이썬 프로그래밍에서 객체를 효과적으로 다루고, 데이터의 흐름을 정확히 파악하는 데 매우 중요하다.

 

==연산자와 is 연산자

== 연산자와 is 연산자는 파이썬에서 종종 혼동될 수 있는데, 각각은 매우 구체적인 목적을 가지고 사용된다. 이 두 연산자의 차이를 이해하는 것은 코드의 정확성과 의도에 맞게 데이터를 비교하는데 필수적이다.

1. == 연산자: 값의 동등성 비교

== 연산자는 두 객체의 값이 동등한지 비교한다. 이는 객체의 내용이나 값이 서로 같은지를 확인하는데 사용된다. 객체의 타입이 서로 다르더라도, 값의 동등성을 판단할 수 있는 경우가 많다. 예를 들어, 리스트의 내용이 같거나, 두 문자열이 동일한 문자를 갖고 있을 때 ==는 True를 반환한다.

a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)  # True, 내용이 동일하기 때문

x = 10
y = 10.0
print(x == y)  # True, 값이 같기 때문 (정수 10과 부동소수점 10.0)

2. is 연산자: 객체의 정체성 비교

is 연산자는 두 객체가 동일한 객체(즉, 메모리 상에서 같은 위치를 차지하는 객체)인지를 확인한다. 이 연산자는 주로 객체가 같은 인스턴스를 참조하는지 확인할 때 사용된다. 값이 같더라도, 두 객체가 메모리 상의 서로 다른 주소에 위치하고 있다면 is는 False를 반환한다.

a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)  # True, b는 a와 동일한 객체
print(a is c)  # False, c는 a와 같은 값을 가지지만, 다른 객체

선택 기준:

  • 값의 동등성을 비교할 때 (==): 두 변수의 값이나 내용이 같은지 확인하려면 ==를 사용한다. 이는 대부분의 일반적인 비교 상황에서 적합하다.
  • 객체의 정체성을 비교할 때 (is): 두 변수가 동일한 객체를 참조하는지 (즉, 정확히 같은 메모리 주소를 가리키는지) 확인하고 싶을 때 is를 사용한다. 이는 특정 객체에 대한 참조가 유일한지 확인할 때 유용다.

일반적으로, is는 싱글턴 객체 (예: None) 또는 상태가 매우 중요한 객체의 정체성을 확인할 때 사용된다. 반면, ==는 대부분의 값을 비교할 때 사용되어 값이 같은지를 판단한다. 따라서 상황에 따라 적절한 연산자를 선택해 사용하는 것이 중요하다.

 

튜플의 상대적 불변성

파이썬에서 튜플(tuple)은 일반적으로 불변(immutable) 자료형으로 알려져 있다. 이는 튜플의 요소를 한 번 생성하고 나면 변경할 수 없다는 의미이다. 그러나 튜플의 "불변성"이 항상 절대적인 것은 아니며, 이를 "상대적 불변성"이라고 한다. 이 개념을 이해하기 위해선 튜플이 가변 객체를 요소로 포함할 수 있음을 알아야 한다.

튜플의 불변성

튜플이 불변이라는 것은 튜플 자체의 직접적인 요소를 변경할 수 없다는 것을 의미한다. 예를 들어, 튜플에 저장된 요소를 다른 값으로 바꾸거나 튜플에서 요소를 추가하거나 삭제하는 것은 불가능하다.

t = (1, 2, 3)
# t[0] = 10  # 이 코드는 TypeError를 발생시킨다.

상대적 불변성

그러나 튜플의 요소가 리스트와 같은 가변 객체일 경우, 튜플의 구조는 변경할 수 없지만, 가변 객체 내의 내용은 변경할 수 있다. 이러한 경우 튜플을 "상대적으로 불변"이라고 한다. 튜플 자체는 같은 객체를 참조하고 있으나, 그 객체의 내부 상태는 변할 수 있다.

t = ([1, 2, 3], 'a', 'b')
t[0].append(4)  # 튜플 내부의 리스트는 가변 객체이므로 변경할 수 있다.
print(t)  # ([1, 2, 3, 4], 'a', 'b')
# t[1] = 'new'  # 이 코드는 여전히 TypeError를 발생시킨다.

위 예에서 볼 수 있듯이, 튜플 t의 첫 번째 요소는 리스트이며, 리스트의 내용을 변경할 수 있다. 하지만 튜플의 요소 자체(예: 리스트를 다른 값이나 객체로 교체)를 변경하는 것은 불가능하다.

# 두 튜플 생성
t1 = ([1, 2, 3], 'a', 'b')
t2 = ([1, 2, 3], 'a', 'b')

# 튜플 t1과 t2의 전체 비교
print(t1 == t2)  # True, 튜플 내용이 동일하기 때문

# 튜플 내 리스트의 id 비교
print(id(t1[0]), id(t2[0]))  # 두 리스트의 메모리 주소가 다름을 확인

# 튜플 내 리스트의 내용 변경
t1[0].append(4)
print(t1)  # ([1, 2, 3, 4], 'a', 'b')
print(t2)  # ([1, 2, 3], 'a', 'b')

# 변경 후 튜플 t1과 t2의 전체 비교
print(t1 == t2)  # False, 리스트 내용이 변경되었기 때문

따라서 튜플의 불변성은 그 요소가 가리키는 객체에 대한 참조가 변경되지 않는다는 것을 의미하지만, 그 요소가 가변 객체인 경우 내부 상태는 변할 수 있다. 이러한 성질 때문에 튜플을 사용할 때는 그 요소가 가변 객체인지 여부를 고려해야 하며, 프로그램에서 의도치 않은 데이터 변경을 방지하기 위해 주의 깊게 사용해야 한다.

 

[:]을 이용한 복사

[:]을 사용한 복사는 파이썬의 리스트를 얕은 복사하는 방법 중 하나이다. 이 방법은 리스트의 모든 요소를 새로운 리스트로 복사하지만, 단순한 얕은 복사에 해당된다. 이 방법을 사용하면 원본 리스트와 복사본 리스트는 독립적인 최상위 객체가 되지만, 리스트 내부의 가변 객체들은 여전히 공유한다.

[:]을 이용한 복사의 특징

  • 새로운 리스트 객체 생성: 원본 리스트의 모든 요소를 포함하는 새로운 리스트 객체가 생성된다.
  • 최상위 리스트는 독립적: 원본 리스트와 복사본 리스트는 서로 다른 메모리 주소를 가지며, 최상위 수준에서는 독립적이다.
  • 내부 가변 객체는 공유: 리스트 내부의 가변 객체(예를 들어, 다른 리스트나 딕셔너리 등)는 복사되지 않고 참조만 복사된다. 따라서 내부 객체가 변경되면 원본과 복사본 모두에 영향을 미친다.
original = [1, 2, [3, 4]]
copied = original[:]

print(original == copied)  # True, 내용이 같음
print(original is copied)  # False, 다른 객체임

# 내부 리스트 변경
original[2].append(5)
print(copied)  # [1, 2, [3, 4, 5]], 내부 리스트 변경이 복사본에도 반영됨

위 예에서 copied는 original의 얕은 복사본이다. [:]은 리스트의 모든 요소를 포함하는 새 리스트를 만들기 때문에 original과 copied는 서로 다른 객체이다(is 연산자 결과가 False). 하지만, 리스트 내의 가변 객체인 [3, 4] 리스트는 단순히 참조만 복사되기 때문에, original에서 이 리스트를 변경하면 copied에도 영향을 미친다.

[:]을 사용한 복사는 간단한 경우나 복사할 리스트가 가변 객체를 포함하지 않을 때 유용하다. 하지만 리스트가 다른 가변 객체를 내포하고 있을 경우, 이들 객체의 독립성이 필요하다면 더 신중한 접근이 필요하며, 깊은 복사를 고려해야 할 수도 있다.

 

객체의 깊은 복사와 얕은 복사

객체를 복사할 때, 파이썬에서는 일반적으로 두 가지 방법을 사용한다: 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy). 이 두 방법의 차이를 이해하는 것은 프로그래밍에서 데이터 구조를 다룰 때 매우 중요하다.

얕은 복사 (Shallow Copy)

얕은 복사는 객체의 최상위 수준만 복사하는 것입니다. 즉, 복사된 객체와 원본 객체는 서로 다른 객체지만, 내부에 있는 가변 객체들(예: 리스트 내의 리스트)에 대해서는 참조만 복사됩니다. 결과적으로, 내부의 가변 객체는 원본과 복사본 사이에 공유된다.

특징

  • 최상위 객체는 새롭게 생성되어 독립적이다.
  • 내부에 포함된 가변 객체의 참조는 공유되므로, 하나의 객체에서 이를 변경하면 다른 객체에도 영향을 미친다.
import copy

original = [1, 2, [3, 4]]
shallow = copy.copy(original)
shallow[2].append(5)

print(original)  # [1, 2, [3, 4, 5]]
print(shallow)   # [1, 2, [3, 4, 5]]

여기서 original의 내부 리스트는 **shallow**와 공유되므로, 하나를 변경하면 다른 하나도 영향을 받는다.

깊은 복사 (Deep Copy)

깊은 복사는 객체에 포함된 모든 수준의 항목을 복사하여 완전히 새로운 복제본을 생성한다. 이 방법을 사용하면, 원본 객체와 복사된 객체 사이에 어떤 참조도 공유되지 않는다. 깊은 복사는 내부의 가변 객체까지도 새롭게 복사하기 때문에, 원본과 복사본은 완전히 독립적이다.

특징

  • 모든 수준에서 새로운 객체가 생성되며, 어떤 내부 객체도 원본과 공유되지 않는다.
  • 원본 객체를 수정하더라도 복사본에는 아무런 영향을 미치지 않는다.
import copy

original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)
deep[2].append(5)

print(original)  # [1, 2, [3, 4]]
print(deep)       # [1, 2, [3, 4, 5]]

여기서 original의 내부 리스트는 deep과 독립적이므로, 하나를 변경해도 다른 하나에는 영향을 미치지 않는다.

사용 시 고려 사항

  • 얕은 복사는 객체의 내부 구조가 간단하거나 내부 객체의 변경이 필요 없을 때 유용하다.
  • 깊은 복사는 복잡한 객체, 특히 내부에 변경 가능한 여러 객체를 포함하는 경우에 적합하다. 하지만 더 많은 메모리와 시간이 소요된다.

각 복사 방법의 선택은 프로그램의 요구 사항과 객체의 구조에 따라 달라질 수 있으며, 데이터의 안정성과 효율성을 고려하여 적절히 결정해야 한다.

 

copy(), deepcopy()

파이썬에서 __copy__()와 __deepcopy__()는 객체의 복사 방식을 사용자 정의할 수 있는 특별 메소드이다. 이 메소드들을 구현함으로써, 객체의 얕은 복사와 깊은 복사 동작을 직접 제어할 수 있다. 이를 통해 **copy.copy()**나 copy.deepcopy() 함수가 호출될 때, 객체의 복사 방식을 정확히 지정할 수 있다.

__copy__() 메소드

__copy__() 메소드는 객체의 얕은 복사를 구현할 때 사용된다. 이 메소드는 객체의 얕은 복사본을 생성하고 반환해야 한다. copy.copy() 함수가 해당 객체에 대해 호출될 때, 객체 내에서 __copy__()가 정의되어 있다면 이 메소드가 사용된다.

import copy

class MyClass:
    def __init__(self, value):
        self.value = value

    def __copy__(self):
        # 새로운 객체를 생성하고 초기화
        cls = self.__class__
        result = cls.__new__(cls)
        result.value = self.value
        return result

obj = MyClass(10)
copied_obj = copy.copy(obj)

print(copied_obj.value)  # 10

__deepcopy__() 메소드

__deepcopy__() 메소드는 객체의 깊은 복사를 구현할 때 사용된다. 이 메소드는 깊은 복사본을 생성하고 반환해야 하며, memo 딕셔너리를 인자로 받아야 한다. memo는 이미 복사된 객체의 참조를 저장하는데 사용되어 재귀적 복사에서 무한 루프를 방지하고 효율성을 높인다. copy.deepcopy() 함수가 호출될 때 이 메소드가 사용된다.

import copy

class MyClass:
    def __init__(self, value):
        self.value = value

    def __deepcopy__(self, memo):
        print("deepcopy 호출됨")
        # 새로운 객체를 생성하고 초기화
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        result.value = copy.deepcopy(self.value, memo)
        return result

obj = MyClass([1, 2, 3])
deep_copied_obj = copy.deepcopy(obj)

print(deep_copied_obj.value)  # [1, 2, 3]

이 예시에서, __deepcopy__() 메소드는 객체의 깊은 복사본을 생성하고, memo 딕셔너리를 사용하여 이미 복사된 객체의 참조를 저장한다. 이 방법은 특히 복잡한 객체 그래프가 있을 때 중요하며, 복사 과정의 효율성을 크게 향상시킨다.

이 두 메소드를 구현함으로써, 파이썬의 표준 라이브러리 copy 모듈이 제공하는 기능을 확장하고, 객체의 복사 동작을 정밀하게 제어할 수 있다.

 

참조로서의 함수 매개변수

파이썬에서 함수 매개변수는 "참조에 의한 전달" (pass-by-reference) 방식으로 작동한다. 이 말은 함수에 어떤 값을 전달할 때, 값의 복사본이 아닌 값이 저장된 메모리 위치의 참조가 전달된다는 의미이다. 하지만 이 개념을 이해할 때 주의할 점이 있다. 파이썬의 경우, 함수 매개변수의 동작은 실제로 "객체 참조에 의한 전달" (pass-by-object-reference)로 더 정확하게 표현된다.

객체 참조에 의한 전달

파이썬에서 모든 것은 객체이다. 변수들은 객체를 참조하는 레이블이며, 변수에 할당된 데이터는 객체의 주소(메모리 위치)를 가리키는 참조이다. 함수로 변수를 전달할 때, 실제로 전달되는 것은 객체 참조이다. 따라서 함수 내부에서 매개변수를 통해 객체를 변경하면, 원본 객체도 영향을 받는다. 그러나 매개변수에 새로운 객체를 할당하면, 원본 객체는 변경되지 않는다.

가변 객체와 불변 객체

함수 매개변수로 전달되는 객체가 가변 객체인 경우(예: 리스트, 딕셔너리 등), 함수 내에서 객체를 변경하면 호출자의 객체도 변경된다. 하지만 불변 객체(예: 정수, 튜플, 문자열 등)의 경우, 객체의 값을 변경할 수 없으므로, 함수 내에서 이러한 객체를 변경하려고 하면 새로운 객체가 생성되고, 매개변수는 새 객체를 참조하게 된다.

1. 가변 객체 전달:

def modify_list(lst):
    lst.append(4)  # 기존 리스트 객체 변경

my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # 출력: [1, 2, 3, 4]

2. 불변 객체 전달:

def modify_number(x):
    x += 10  # x는 새로운 객체를 참조하게 됨

num = 5
modify_number(num)
print(num)  # 출력: 5

함수 매개변수는 파이썬에서 객체의 참조를 전달하므로, 전달된 객체가 가변인지 불변인지에 따라 함수 내에서의 동작이 결정된다. 이로 인해 파이썬에서 데이터를 효율적으로 다루면서도, 실수로 원본 데이터를 변경하지 않도록 주의해야 한다.

 

가변형을 매개변수 기본값으로 사용하기 : 좋지 않은 생각

파이썬에서 함수를 정의할 때 가변형(mutable) 객체를 매개변수의 기본값으로 사용하는 것은 일반적으로 피해야 하는 패턴이다. 이런 방식으로 코드를 작성하면, 예상치 못한 부작용과 버그가 발생할 수 있다.

왜 좋지 않은 생각인가?

파이썬에서 함수의 매개변수 기본값은 함수가 정의될 때 한 번만 생성되고, 그 이후의 모든 함수 호출에서 재사용된다. 만약 이 기본값이 가변형 객체라면, 함수를 호출할 때마다 이 객체가 변경될 수 있고, 이러한 변경은 다음 함수 호출 때까지 유지된다. 이는 함수가 예상치 못한 상태를 가지게 하고, 함수의 동작이 호출마다 달라지게 할 수 있다.

def append_to(element, to=[]):
    to.append(element)
    return to

print(append_to(12))  # [12]
print(append_to(34))  # [12, 34]

이 예제에서, append_to 함수는 리스트에 요소를 추가하는 간단한 기능을 수행한다. 기본값으로 빈 리스트를 사용했다. 첫 번째 호출에서 12가 리스트에 추가되고, 두 번째 호출에서는 이전에 수정된 리스트([12])에 34가 추가된다. 사용자가 새로운 리스트를 기대했던 경우, 이 결과는 의도하지 않은 결과를 초래한다.

올바른 접근 방법

가변형 객체를 사용해야 할 경우, 기본값으로 None을 설정하고 함수 내에서 이를 확인하여 새 객체를 할당하는 것이 좋다. 이 방식은 함수가 예상대로 동작하도록 보장한다:

def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

print(append_to(12))  # [12]
print(append_to(34))  # [34]

이 방식을 사용하면 각 함수 호출마다 to 매개변수가 None으로 초기화되고, 따라서 새로운 빈 리스트가 생성된다. 이는 함수 호출 간에 상태를 공유하지 않게 하므로, 각 호출이 독립적이고 예측 가능한 결과를 반환하게 한다.

다음은 가변형 객체를 클래스의 매개변수 기본값으로 사용할 때 발생할 수 있는 문제를 보여주는 클래스이다:

class DataCollector:
    def __init__(self, data=[]):
        self.data = data

    def add_data(self, value):
        self.data.append(value)

# 객체 생성
collector1 = DataCollector()
collector1.add_data(1)
collector1.add_data(2)

# 또 다른 객체 생성
collector2 = DataCollector()
collector2.add_data(3)

# 각 객체의 데이터 출력
print("Collector 1 data:", collector1.data)  # [1, 2, 3] - 의도치 않은 결과!
print("Collector 2 data:", collector2.data)  # [1, 2, 3] - 의도치 않은 결과!

위 예시에서, collector1과 collector2는 둘 다 기본 매개변수로 설정된 같은 리스트를 참조하게 된다. 결과적으로 하나의 객체에 데이터를 추가하면, 다른 객체의 데이터도 변경된다.

올바른 클래스 설계

이 문제를 해결하기 위해, 매개변수 기본값으로 None을 사용하고 생성자 내부에서 적절히 초기화하는 방법을 사용한다:

class DataCollector:
    def __init__(self, data=None):
        if data is None:
            data = []
        self.data = data

    def add_data(self, value):
        self.data.append(value)

# 객체 생성
collector1 = DataCollector()
collector1.add_data(1)
collector1.add_data(2)

# 또 다른 객체 생성
collector2 = DataCollector()
collector2.add_data(3)

# 각 객체의 데이터 출력
print("Collector 1 data:", collector1.data)  # [1, 2] - 올바른 결과!
print("Collector 2 data:", collector2.data)  # [3] - 올바른 결과!

이 설계에서는 collector1과 collector2가 독립적인 data 리스트를 가지게 된다. 따라서 한 객체의 상태가 다른 객체에 영향을 주지 않으며, 각 객체는 자신의 데이터만을 관리하게 된다.

이 예시를 통해 클래스를 설계할 때 가변형 객체를 기본 매개변수 값으로 사용하는 것의 위험을 이해하고, None을 사용해 이를 초기화하는 방법이 어떻게 문제를 예방하는지 보여준다.

함수에서 가변형 객체를 기본 매개변수 값으로 사용하는 것은 여러 호출에 걸쳐 상태가 유지되기 때문에 버그를 발생시킬 수 있다. 대신, 기본값으로 None을 사용하고 필요할 때 객체를 생성하는 방법을 사용하는 것이 안전하고 권장되는 방법이다. 이런 패턴은 함수의 동작을 명확하고 일관성 있게 유지하는 데 도움을 준다.

 

가변 매개변수에 대한 방어적 프로그래밍

가변 매개변수를 다룰 때 방어적 프로그래밍은 특히 중요하다. 가변 매개변수는 내부 상태가 외부의 영향으로부터 쉽게 변경될 수 있으며, 이는 버그 발생의 원인이 될 수 있다. 방어적 프로그래밍은 예기치 않은 문제를 미리 예방하고, 코드의 견고성을 높이기 위한 전략이다.

가변 매개변수와 방어적 프로그래밍

1. 데이터 복사하기

  • 가변 객체를 함수나 클래스의 매개변수로 전달할 때는, 객체의 복사본을 생성하여 작업을 수행하는 것이 좋다. 이렇게 함으로써 원본 데이터의 무결성을 유지할 수 있다.
  • 예: 리스트나 딕셔너리 같은 가변 객체를 매개변수로 받을 때, 얕은 복사(copy())나 깊은 복사(deepcopy())를 사용하여 원본을 보호한다.
import copy

def process_data(data):
    # 데이터의 깊은 복사본을 만들어 작업 수행
    data_copy = copy.deepcopy(data)
    # 데이터 처리 로직
    data_copy.append("new data")
    return data_copy

original_data = [1, 2, 3]
new_data = process_data(original_data)
print(original_data)  # [1, 2, 3] - 원본 데이터 유지
print(new_data)       # [1, 2, 3, "new data"] - 변경된 데이터

2. 불변 객체 사용하기

  • 가능하면 불변 객체를 사용하여 데이터의 안정성을 확보한다. 예를 들어, 리스트 대신 튜플을 사용하거나, 딕셔너리 대신 frozenset 등을 사용할 수 있다.
  • 불변 객체를 사용하면 함수나 메소드 내에서 데이터를 변경하려 할 때 자연스럽게 오류가 발생하여, 코드의 안정성을 높일 수 있다.

3. 매개변수 유효성 검사 수행

  • 함수나 메소드에 데이터가 전달되기 전에 입력 값을 검증한다. 이는 특히 외부에서 오는 데이터를 처리할 때 중요하다.
  • 데이터 타입, 데이터 포맷, 데이터 범위 등을 검사하여 예상치 못한 값이 함수 내부 로직에 전달되는 것을 방지한다.
def add_item(items, item):
    if not isinstance(items, list):
        raise ValueError("items must be a list.")
    if item in items:
        raise ValueError("item already exists.")
    items.append(item)

try:
    current_items = [1, 2, 3]
    add_item(current_items, 2)
except ValueError as e:
    print(e)  # item already exists.

4. 사이드 이펙트 문서화

  • 함수나 메소드가 외부 상태를 변경할 가능성이 있는 경우, 이를 명확히 문서화하여 사용자가 이해할 수 있도록 한다. 예상할 수 있는 모든 사이드 이펙트를 문서화하는 것이 좋다.

5. 최소 권한 원칙 적용

  • 객체나 함수는 필요한 최소한의 데이터에만 접근할 수 있도록 한다. 이는 데이터의 무결성을 유지하는 데 도움이 된다.

방어적 프로그래밍 기법을 통해 코드의 안정성과 유지보수성을 높일 수 있으며, 예기치 않은 에러로부터 시스템을 보호할 수 있다.

 

del과 가비지 컬렉션

파이썬에서 del 키워드와 가비지 컬렉션은 메모리 관리와 관련된 중요한 개념이다. 이들은 파이썬이 메모리를 효율적으로 사용하고, 불필요한 메모리를 해제하여 프로그램의 성능을 유지하는 데 도움을 준다.

del 키워드

del 키워드는 파이썬에서 객체에 대한 참조를 제거하는 데 사용된다. 이 키워드는 변수를 삭제하거나, 리스트에서 특정 요소를 제거하는 등 다양하게 사용될 수 있다. del은 단순히 이름과 객체 간의 연결을 끊는다. 즉, 해당 이름이 더 이상 객체를 참조하지 않게 한다. 하지만 del을 사용한다고 해서 객체가 즉시 메모리에서 제거되는 것은 아니다. 객체가 실제로 메모리에서 해제되는 것은 가비지 컬렉터가 그 객체에 대한 모든 참조가 사라졌다고 판단했을 때이다.

x = [1, 2, 3]
y = x
del x  # x에 대한 참조를 제거하지만, y가 여전히 리스트 [1, 2, 3]을 참조하고 있으므로 리스트는 메모리에 남아 있다.

가비지 컬렉션 (Garbage Collection)

가비지 컬렉션은 프로그램이 동적으로 할당한 메모리 영역 중에서 더 이상 사용되지 않는 영역을 자동으로 찾아서 해제하는 프로세스이다. 파이썬은 주로 참조 카운팅 방식을 사용하여 객체가 더 이상 필요 없을 때 메모리를 해제한다. 객체의 참조 카운트가 0이 되면, 즉, 어떤 변수도 그 객체를 참조하지 않게 되면, 파이썬의 가비지 컬렉터가 그 객체를 메모리에서 해제한다.

파이썬은 또한 순환 참조를 감지하고 처리할 수 있는 가비지 컬렉터를 내장하고 있다. 순환 참조는 두 객체가 서로를 참조하는 경우 발생하며, 이러한 경우 참조 카운트가 절대 0이 되지 않기 때문에, 별도의 메커니즘이 필요하다.

import gc

class A:
    def __init__(self, b_instance):
        self.b = b_instance

class B:
    def __init__(self, a_instance):
        self.a = a_instance

gc.enable()  # 가비지 컬렉션 활성화

a = A(None)
b = B(a)
a.b = b

del a
del b

gc.collect()  # 가비지 컬렉터를 강제로 호출하여 순환 참조를 제거

이 예제에서, A 인스턴스와 B 인스턴스가 서로를 참조하고 있어서 간단한 참조 카운팅만으로는 메모리 해제가 어렵다. 따라서 가비지 컬렉터를 명시적으로 호출하여 순환 참조를 제거한다.

del 키워드와 가비지 컬렉션은 파이썬에서 메모리 관리를 위해 중요한 도구이다. del은 객체의 참조를 제거하는 데 사용되며, 가비지 컬렉션은 더 이상 사용되지 않는 메모리를 자동으로 정리한다. 이러한 메커니즘은 효율적인 메모리 사용을 보장하고 메모리 누수를 방지하는 데 중요한 역할을 한다.

 

약한 참조(weak reference)

weakref 모듈은 파이썬에서 약한 참조(weak reference)를 지원하기 위한 방법을 제공한다. 약한 참조는 객체를 참조하지만, 이 참조만으로는 객체가 가비지 컬렉션되는 것을 막지 않는다. 즉, 약한 참조는 객체의 생명주기에 영향을 미치지 않는다. 이러한 특성 때문에 weakref는 메모리 관리와 관련된 다양한 상황에서 유용하게 활용된다.

약한 참조의 필요성

약한 참조는 주로 순환 참조가 발생하는 문제를 해결하거나, 큰 데이터 구조를 메모리에 유지하지 않고도 참조할 필요가 있을 때 사용된다. 예를 들어, 캐시 시스템이나 콜백 구조에서 객체를 참조할 때, 그 객체가 더 이상 필요하지 않으면 자동으로 메모리에서 제거되도록 하기 위해 약한 참조를 사용할 수 있다.

weakref 모듈의 주요 기능

1. 약한 참조 생성

weakref.ref 함수는 주어진 객체에 대한 약한 참조를 생성한다. 이 약한 참조는 원본 객체에 대한 참조를 유지하지만, 가비지 컬렉션의 대상이 되지 않는다. 원본 객체가 프로그램의 다른 부분에서 더 이상 사용되지 않을 때, 약한 참조는 객체가 메모리에서 해제되는 것을 방지하지 않는다.

import weakref

class MyClass:
    def __init__(self, value):
        self.value = value

obj = MyClass(10)
r = weakref.ref(obj)

print(r())  # MyClass 인스턴스 출력
del obj
print(r())  # None, obj가 가비지 컬렉션된 후

2. 약한 사전 (WeakKeyDictionary와 WeakValueDictionary)

약한 사전은 키 또는 값이 약한 참조를 통해 참조되는 사전이다. 이 구조를 사용하면 키나 값 객체가 더 이상 필요 없을 때 자동으로 사전에서 제거된다. 이는 주로 캐싱과 같은 상황에서 유용하게 사용된다.

import weakref

class MyClass:
    pass

obj = MyClass()
weak_dict = weakref.WeakValueDictionary()
weak_dict['primary'] = obj

print(weak_dict['primary'])  # MyClass 인스턴스 출력
del obj
print(weak_dict.get('primary'))  # None, obj가 가비지 컬렉션된 후

3. 약한 집합 (WeakSet)

약한 집합은 집합의 요소들이 약한 참조로 관리되는 집합이다. 약한 집합의 요소로 사용된 객체가 더 이상 다른 곳에서 사용되지 않으면, 그 요소는 자동으로 집합에서 제거된다.

import weakref

class MyClass:
    pass

obj = MyClass()
weak_set = weakref.WeakSet()
weak_set.add(obj)

print(obj in weak_set)  # True
del obj
print(obj in weak_set)  # False, obj가 가비지 컬렉션된 후

약한 참조의 한계

약한 참조는 내장 타입의 인스턴스(예: 리스트, 사전, 집합 등)에 직접 사용할 수 없다. 이러한 제한은 파이썬의 구현 세부 사항 때문에 발생한다. 대신, 사용자 정의 클래스 인스턴스 또는 특정 내장 클래스(예: functools.lru_cache()에서 반환되는 객체)에 사용될 수 있다.

weakref 모듈은 파이썬에서 메모리 관리를 더 세밀하게 제어하고자 할 때 중요한 도구이다. 약한 참조를 적절히 사용하면 메모리 사용을 최적화하고, 순환 참조로 인한 메모리 누수 문제를 방지할 수 있다. 하지만, 약한 참조를 사용할 때는 객체의 생명주기와 가비지 컬렉션의 동작 방식을 잘 이해하고 있어야 한다.

 

파이썬의 특이한 불변형 처리법

파이썬에서 불변형(immutable) 객체의 처리 방식은 언어의 메모리 관리와 성능 최적화 전략에 중요한 역할을 한다. 불변형 객체는 한 번 생성된 후 그 상태가 변경될 수 없다는 특성을 가진다. 파이썬의 주요 불변형 객체에는 정수, 실수, 문자열, 튜플 등이 있다. 이들 객체의 처리 방식에는 몇 가지 특이한 점이 있다:

1. 인터닝(interning)

파이썬은 특히 문자열과 작은 정수에 대해 인터닝이라는 최적화 기법을 사용한다. 인터닝은 동일한 불변 객체가 여러 번 생성될 때, 메모리에 중복해서 저장하는 대신에 하나의 객체만을 생성하고, 필요할 때마다 이 객체에 대한 참조를 공유하는 방식이다.

정수 인터닝

파이썬은 일반적으로 -5에서 256 사이의 정수에 대해 인터닝을 적용한다. 이 범위 내의 정수는 자주 사용되기 때문에, 파이썬 인터프리터가 시작할 때 미리 생성해두고, 이후에 같은 정수가 필요할 때마다 이미 생성된 객체를 재사용한다.

a = 256
b = 256
print(a is b)  # True - 같은 객체

a = 257
b = 257
print(a is b)  # 가능성에 따라 True 또는 False

문자열 인터닝

파이썬은 불변인 문자열도 인터닝한다. 특히 컴파일 시점에 알려진 문자열 리터럴은 자동으로 인턴되어 동일한 문자열에 대한 중복 저장을 방지한다. 이는 메모리 사용을 줄이고, 문자열 비교를 빠르게 수행할 수 있게 한다.

a = "hello"
b = "hello"
print(a is b)  # True

2. 불변 컨테이너의 불변성

파이썬에서 튜플은 불변 컨테이너이다. 하지만 튜플이 불변이라고 해서 튜플이 담고 있는 요소까지 불변인 것은 아니다. 예를 들어, 튜플 안에 리스트와 같은 가변 객체가 포함된 경우, 튜플의 구조는 변경할 수 없지만 포함된 가변 객체는 변경할 수 있다.

t = (1, 2, [3, 4])
t[2].append(5)
print(t)  # (1, 2, [3, 4, 5])

3. 메모리 절약 및 성능 최적화

이러한 불변 객체의 처리 방식은 메모리 사용을 최적화하고, 프로그램의 성능을 향상시킨다. 인터닝을 통해 객체의 생성과 소멸에 소요되는 비용을 줄이며, 참조를 통한 접근은 객체 생성 비용보다 훨씬 저렴하다. 또한, 불변 객체의 특성 덕분에 멀티 스레딩 환경에서의 동기화 문제 없이 객체를 공유할 수 있다.

파이썬에서 불변형 객체의 이러한 처리 방법은 프로그래밍 시 여러 면에서 효율적이고, 안전한 코드 작성을 가능하게 한다. 하지만 불변형의 특성과 메모리 관리 방식을 이해하지 못하면, 때때로 예상치 못한 동작이 발생할 수 있으므로 주의가 필요다.

 

모든 코드는 github에 저장되어 있습니다.

https://github.com/SeongUk18/python

728x90

'Python study' 카테고리의 다른 글

시퀀스 해킹, 해시, 슬라이스  (0) 2024.05.22
파이썬스러운 객체(Pythonic object)  (0) 2024.05.20
함수 데커레이터(Decorator) & 클로저(Closure)  (0) 2024.05.10
일급 함수 디자인 패턴  (0) 2024.05.08
일급 함수  (0) 2024.05.06