Python

[전문가를 위한 파이썬] 8장 객체 참조, 가변성, 재활용 내용 정리

나쁜도비 2024. 6. 18. 14:55

8.1. 변수는 상자가 아니다

- 파이썬 변수는 객체에 붙은 레이블이라고 생각하는 것이 좋다. '상자로서의 변수' 개념이 설명할 수 없는 예:

a = [1, 2, 3]
b = a
a.append(4)
print(b)
# [1, 2, 3, 4]

- 파이썬에서는 객체의 오른쪽에서 객체를 생성하거나 가져온다. 그 후 레이블을 붙이듯이 할당문 왼족에 있는 변수가 객체에 바인딩된다.

- 객체에 여러 레이블을 붙이는 것을 별명(alias)이라고 한다.

 

 

8.2. 정체성, 동질성, 별명

- 서로 다른 두 변수가 동일한 객체에 바인딩되어 있을 경우 '별명'이라고 한다.

- 서로 다른 두 변수가 바인딩된 객체가 동일한 값을 가지고 있더라도 메모리 주소(정체성)는 다르다면, 두 변수는 서로의 별명이 아니다.

 

8.2.1. == 연산자와 is 연산자 간의 선택

- == 는 객체의 값을 비교하는 반면, is는 객체의 정체성을 비교한다.

- 변수를 싱글턴(singleton)과 비교할 때는 is 연산자를 사용해야 한다. 대표적으로 x is None 과 같이 비교한다.

 

 

8.2.2. 튜플의 상대적 불변성

- 튜플의 불변성은 tuple 데이터 구조의 물리적인 내용(즉, 참조 자체)만을 말하는 것이며, 참조된 객체까지 불변성을 가지는 것은 아니다. 튜플 안에서 결코 변경되지 않는 것은 튜플이 담고 있는 항목들의 정체성뿐이다.

 

 

8.3. 기본 복사는 얕은 복사

- 내장 가변 컬렉션을 복사하는 방법 중 하나는 그 자료형 자체의 내장 생성자를 이용하는 것이다.

l1 = [3, [1,2]]
l2 = list(l1)

l2 == l1
# True

# 서로 다른 두 객체를 참조한다.
l2 is l1
# False

 

- [:]를 사용하면 얕은 사본(shallow copy)을 생성한다. 즉, 최상위 컨테이너는 복제하지만 사본은 원래 컨테이너에 들어 있던 동일 객체에 대한 참조로 채워진다.

 

# 얕은 복사, 깊은 복사
l1 = [1, [1,2,3], (4,5,6)]
l2 = list(l1)
l1.append(100) # l1에 100을 추가해도 l2에는 영향을 미치지 않는다.
l1[1].remove(2) # l2[1]이 l1[1]과 동일한 리스트에 바인딩되어 있으므로 이 코드는 l2에 영향을 미친다.

print('l1: ', l1)
# l1:  [1, [1, 3], (4, 5, 6), 100]

print('l2: ', l2)
# l2:  [1, [1, 3], (4, 5, 6)]

l2[1] += [33, 22] # 가변객체의 경우 += 연산자가 리스트를 그 자리에서 변경한다. 이 변경은 l2[1]의 별명인 l1[1]에도 반영된다.
l2[2] += (10, 11) # += 연산자는 새로운 튜플을 만들어서 l2[2]에 다시 바인딩한다. 이제 l1과 l2의 마지막에 있는 튜플은 더이상 동일 객체가 아니다.

print('l1: ', l1)
print('l2: ', l2)

 

 

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

- 내포된 객체의 참조를 공유하지 않도록 깊게 복사할 필요가 종종 있다.

- copy.deepopcy() 함수는 깊은 복사를, copy.copy() 함수는 얕은 복사를 지원한다.

 

class Bus:
   def __init__(self, passengers=None):
       if passengers is None:
           self.passengers = []
       else:
           self.passengers = list(passengers)
       
   def pick(self, name):
       self.passengers.append(name)
   
   def drop(self, name):
       self.passengers.remove(name)

 

import copy
bus1 = Bus(["Alice", "Bill", "Claire", "David"])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
print(bus1 is bus2)
print(bus2 is bus3)
print(bus1 is bus3)

# False
# False
# False

print(id(bus1.passengers))
print(id(bus2.passengers))
print(id(bus3.passengers))

# 132332697046912
# 132332697046912
# 132332448786880

bus1.drop('Bill')
print(bus2.passengers) # bus1에서 bill을 내리면 bus2에도 bill이 사라진다. bus1과 bus2가 동일 리스트를 공유하고 있기 때문이다.
print(bus3.passengers) # bus3은 bus1의 깊은 사본이므로, passengers 속성이 다른 리스트를 가리킨다.

# ['Alice', 'Claire', 'David']
# ['Alice', 'Bill', 'Claire', 'David']

 

 

8.4. 참조로서의 함수 매개변수

- 파이썬은 공유로 호출(call by sharing)하는 매개변수 전달 방식만 지원한다. 

- 공유로 호출한다는 말은 함수의 각 매개변수가 인수로 전달받은 각 참조의 사본을 받는다는 의미다.

- 즉, 함수 안의 매개변수는 실제 인수의 별명이 된다.

 

 

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

class HauntedBus:
  def __init__(self, passengers=[]): # passengers 인수를 전달하지 않는 경우 이 매개변수는 기본값인 빈 리스트에 바인딩된다.
    self.passengers = passengers # 이 할당문은 self.passengers를 passengers에 대한 별명으로 만드므로, passengers 인수를 전달하지 않는 경우 self.passengers를 기본값인 빈 리스트에 대한 별명으로 설정한다.

  def pick(self, name):
    self.passengers.append(name) # self.passengers에 remove()와 append() 메서드를 사용할 때, 실제로는 함수 객체의 속성인 가변형 기본 리스트를 변경하는 것이다.

  def drop(self, name):
    self.passengers.remove(name)
    

bus1 = HauntedBus(['Alice', 'Bill'])
bus1.passengers
# ['Alice', 'Bill']

bus1.pick('Charlie')
bus1.drop('Alice')
bus1.passengers 
# ['Bill', 'Charlie']

bus2 = HauntedBus() # bus2를 빈 리스트로 시작하므로, 기본값인 빈 리스트가 self.passengers에 할당된다.
bus2.pick('Carrie')
bus2.passengers
# ['Carrie']

bus3 = HauntedBus() # bus3도 빈 리스트로 시작하므로, 기본값인 빈 리스트가 self.passengers에 할당된다.
bus3.passengers # 기본값이 비어 있지 않다.
# ['Carrie']

bus3.pick('Dave')
bus2.passengers # bus3에 승차한 Dave가 bus2에 나타난다.
# ['Carrie', 'Dave']

bus2.passengers is bus3.passengers # bus2.passengers와 bus3.passengers가 동일한 리스트를 참조한다.
# True

bus1.passengers # bus1.passengers는 별개의 리스트다.
# ['Bill', 'Charlie']

 

- 결국 명시적인 승객 리스트로 초기화되지 않은 Bus 객체들이 승객 리스트를 공유하게 되는 문제가 발생한다. self.passengers가 passengers 매개변수 기본값의 별명이 되기 때문이다. 

- 문제는 각 기본값이 함수가 정의될 때(즉, 일반적으로 모듈이 로딩될 때) 평가되고, 기본값은 함수 객체의 속성이 된다는 것이다. 따라서 기본값이 가변 객체고, 이 객체를 변경하면 변경 내용이 향후에 이 함수의 호출에 영향을 미친다.

- 가변 기본값에 대한 이러한 문제 때문에, 가변 값을 받는 매개변수의 기본값으로 None을 주로 사용한다.

 

 

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

- 아래의 TwilightBus 클래스는 '최소 놀람의 법칙(Principle of least astonishment'를 어긴다. 

class TwilightBus:
  """승객이 사라지게 만드는 버스 모델"""
  def __init__(self, passengers=None):
    if passengers is None:
      self.passengers = [] # passengers가 None일 때 빈 리스트를 새로 생
    else:
      self.passengers = passengers # 이 할당문에 의해 self.passengers는 passengers에 대한 별명이 된다.

  def pick(self, name):
    self.passengers.append(name)

  def drop(self, name):
    self.passengers.remove(name) # self.passengers의 remove()나 append() 메서드를 사용하면, 생성자에 인수로 전달된 원래 리스트를 변경하게 된다.

 

- 아래와 같이 __init__() 메서드에서 passengers 인수를 받을 때 인수의 사본으로 self.passengers를 초기화하면 된다.

def __init__(self, passengers=None):
  if passengers is None:
    self.passengers = []
  else:
    self.passengers = list(passengers)

 

 

8.5. del과 가비지 컬렉션

- del 명령은 이름을 제거하는 것이지 객체를 제거하는 것이 아니다.

 

 

8.6. 약한 참조

- 객체가 메모리에 유지되거나 유지되지 않도록 만드는 것은 참조의 존재 여부이다.

- 객체 참조 카운트가 0이 되면 가비지 컬렉터는 해당 객체를 제거한다.

- 약한 참조는 참조 카운트를 증가시키지 않고 객체를 참조한다. 따라서 약한 참조는 참조 대상이 가비지 컬렉트되는 것을 방지하지 않는다.

- 약한 참조는 캐시 어플리케이션에서 유용하게 사용된다. 캐시가 참조하고 있다고 해서 캐시된 객체가 계속 남아 있기 원치 않기 때문이다.

 

 

8.6.1. WeakValueDictionary 촌극

- 임시 변수가 객체를 참조함으로써 예상보다 객체의 수명이 늘어날 수 있다. 지역 변수는 함수가 반환되면서 사라지므로 일반적으로 문제가 되지 않지만, 전역변수는 명시적으로 제거하기 전에는 사라지지 않는다.

 

 

8.6.2. 약한 참조의 한계

- 모든 파이썬 객체가 약한 참조의 대상이 될 수 있는 것은 아니다. int, tuple 객체는 클래스를 상속해도 약한 참조의 대상이 될 수 없다.

 


Pytorch 관련 예시

https://colab.research.google.com/drive/1ghC9zEvoV9oDw7PeFiDSepq20JBsksik#scrollTo=y9gidag_9DiO

728x90