8.1 변수는 상자가 아니다.
흔히들 변수를 상자로 비유한다. 어떠한 값을 변수에 할당하면 변수라는 상자에 그 값이 들어간다고 표현한다.
하지만 이는 잘못된 비유다.
상자보다는 포스트잇에 빗대는 것이 더 명확한 비유라고 볼 수 있다.
단지 어떠한 값(객체) 에 이 값의 별명이 무엇이라고 포스트잇을 붙여 놓는 것과 같다.
a = [1, 2, 3]
b = a
a.append(4)
print(b)
> [1, 2, 3, 4]
변수를 어떤 값을 담고 있는 상자라고 생각하면 위의 코드를 설명할 수 없다. a와 b는 서로 다른 상자임에도 a 에 추가한 값이 b 에도 들어가 있기 때문이다.
비슷한 맥락에서 할당에 관하여 어떠한 객체에 대해서 얘기할 때, '변수 s 가 객체에 할당되었다' 라고 하는 것이 '객체가 변수 s에 할당되었다' 라고 하는 것보다 타당하다.
객체는 변수에 할당되기 전에 생성되기 때문이다.
class TestClass:
def __init__(self):
print('TestClass id: %d' % id(self))
x = TestClass()
>TestClass id: 4378866160
y = TestClass() * 10
> TestClass id: 4379137072
Traceback (most recent call last):
File "chapter_08.py", line 11, in <module>
y = TestClass() * 10
TypeError: unsupported operand type(s) for *: 'TestClass' and 'int'
TestClass 의 인스턴스를 생성할 때마다 자동으로 해당 객체의 id 가 출력되도록 했다. y 변수에 할당되기 전에 이미 객체는 생성되었다. 다만 TypeError 로 인하여 y 변수는 생성되지 않았다.
print(dir())
['TestClass', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'x']
파이썬에서 할당문을 볼 때는 반드시 오른쪽을 먼저 읽어야 한다. 객체를 생성한 후에 변수가 객체에 바인딩되기 때문이다.
변수는 객체에 별명을 적어 놓은 포스트잇을 붙이는 것과 같으며, 동일한 객체에 여러 별명을 지을 수 있다.
charles = {"name": "Charles L. Dodgson", "born": 1832}
lewis = charles
print(lewis is charles)
> True
print(id(charles), id(lewis))
> 4491907008 4491907008
lewis["balance"] = 950
print(charles)
> {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
lewis 와 charles 두 변수는 동일한 객체를 가리키고 있다는 사실을 알 수 있다.
alex = {"name": "Charles L. Dodgson", "born": 1832, "balance": 950}
print(alex == charles)
> True
print(alex is charles)
> False
위와 동일한 dict 에 alex 라는 변수를 할당했다. dict 클래스의 __eq__() 를 통해서 두 값은 같다고 판단되어 "==" 연산자의 결과는 True 이다. 그러나 두 객체는 서로 다른 별개의 객체이다.
8.2.1 == 연산자와 is 연산자 간의 선택
== 연산자가 객체의 값을 비교하는 반면, is 연산자는 객체의 정체성을 비교한다.
is 연산자는 오버로딩 할 수 없다. 즉 객체에 연산자 관련 매직 메서드를 재정의하여 객체 간의 연산이 가능하도록 할 때, is 와 관련하여 정의할 수 있는 게 없다. is 연산자는 단지 각 객체의 is 값을 비교할 뿐이며, 매직 메서드를 호출하지도 않으므로 == 연산자보다 빠르다.
반면, a==b 는 a.__eq__(b) 의 편리 구문이다. object 객체에서 상속받은 __eq__() 메서드는 객체의 ID를 비교하므로 is 연산자와 같다. 그러나 대부분의 내장 자료형은 __eq__() 메서드를 오버라이드하여 객체의 값을 비교한다. 대형 컬렉션이나 깊이 내포된 구조체를 비교하는 경우 == 연산은 상당한 처리를 요구한다.
8.2.2 튜플의 상대적 불변성
튜플도 리스트, 집합 등과 마찬가지로 객체에 대한 참조를 담는다. 따라서 참조된 항목이 가변형이면 튜플 자체는 불변형이지만 참조된 항목은 변할 수 있다. 즉, 튜플의 불변성은 참조 자체만 말하는 것이며 참조된 객체까지 불변성을 가지는 것은 아니다.
t1 = (1, 2, [3, 4])
t2 = (1, 2, [3, 4])
print(t1 == t2)
> True
print(id(t1[-1]))
> 4557472384
t1[-1].append(99)
print(t1)
> (1, 2, [3, 4, 99])
print(id(t1[-1]))
> 4557472384
print(t1 == t2)
> False
최초 할당된 t1, t2 는 서로 같은 값을 갖는다. 이 때, t1 내부의 리스트에 값을 추가할 경우, 리스트 객체의 id 값을 그 전과 비교해도 동일하다. 튜플이 참조하고 있는 리스트 객체의 정체성에는 변화가 없다. 다만 값이 바뀌었을 뿐이다.
이러한 이유로 일부 튜플은 해시 불가능하다. (3장 딕셔너리와 집합 : 해시 가능하다는 말의 의미는? 참조)
동질성과 정체성 간의 차이는 객체를 복사할 때 더 큰 영향을 미친다.
8.3 기본 복사는 얕은 복사
리스트나 대부분의 내장 가변 컬렉션을 복사하는 손쉬운 방법은 그 자료형 자체의 내장 생성자를 사용하는 것이다.
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1)
print(l2)
> [3, [55, 44], (7, 8, 9)]
print(l1 == l2)
> True
print(l1 is l2)
> False
리스트 및 가변형 시퀀스의 경우 l2 = l1[:] 코드로 사본을 생성할 수 있다.
그러나 생성자나 [:] 를 사용하면 얕은 사본(shallow copy)을 생성한다. 즉, 최상위 컨테이너는 복제하지만 사본은 원래 컨테이너에 들어 있던 동일 객체에 대한 참조로 채워진다.
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)
l1.append(100)
l1[1].remove(55)
print("l1:", l1)
> l1: [3, [66, 44], (7, 8, 9), 100]
print("l2:", l2)
> l2: [3, [66, 44], (7, 8, 9)]
l2[1] += [33, 22]
l2[2] += (10, 11)
print("l1:", l1)
> l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
print("l2:", l2)
> l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
- l2 는 l1 의 얕은 복사본이다.
- l1 에 100을 추가해도 l2 에는 영향을 미치지 않는다. 최상위 컨테이너(가장 바깥의 리스트)는 복제했기 때문이다.
- l1 의 내부 리스트 값 중 55를 제거한다. 이 때, l1[1] 과 l2[1] 는 동일한 리스트에 바인딩 되어 있다. 따라서 l2 에도 영향을 준다.
- l2[1] 과 같은 가변 객체에 복합할당 += 연산자를 사용하면 바로 리스트를 변경한다. 이 때, l1[1] 에도 영향을 준다.
- 튜플에 복합할당 += 연산자를 사용하면 새로운 튜플을 만든다. 즉, l2[2] = l2[2] + (10, 11) 과 같다. l1과 l2의 튜플은 더 이상 동일 객체가 아니게 된다.
8.3.1 객체의 깊은 복사와 얕은 복사
copy 모듈의 copy(), deepcopy() 두 함수가 각각 얕은 복사와 깊은 복사를 지원한다.
다음과 같이 버스 클래스가 있다고 가정해본다.
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(id(bus1), id(bus2), id(bus3))
> 4478821904 4478823584 4478856496
bus1.drop("Bill")
print(bus2.passengers)
> ['Alice', 'Claire', 'David']
print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
> 4478913024 4478913024 4478336000
print(bus3.passengers)
> ['Alice', 'Bill', 'Claire', 'David']
- bus2 는 bus1 의 얕은 복사본이므로 Bill 이 내리면 둘 다 영향을 받는다.
- passengers 의 id 를 보면 bus1, bus2 는 같은 객체를 참조하고 있다.
- bus3 은 깊은 복사본이므로 passengers 가 다른 리스트 객체를 참조한다.
deepcopy() 함수는 객체 안에 순환 참조가 있는 경우 무한 루프에 빠지는 것을 방지하기 위해 이미 복사한 객체에 대한 참조를 기억하고 있다.
__copy__() 와 __deepcopy__() 특별 메서드를 구현해서 copy(), deepcopy() 의 동작을 제어할 수 있다.
8.4 참조로서의 함수 매개변수
파이썬은 call by sharing으로 매개변수를 전달한다.
- call by value : 값을 복사하여 매개변수로 넘겨줌. 함수 내부에서 값을 변경할 수 없음(매개 변수가 지역 변수 기능)
- call by reference : 값의 참조를 매개변수로 넘겨줌. 함수 내부에서 값이 바뀔 수 있음
파이썬은 객체의 참조 사본을 매개변수로 넘겨준다. 이게 무슨소리냐면 파이썬은 모든 것이 객체다. 변수는 객체의 인스턴스에 대한 별명과 같다. 함수에 넘겨주는 매개변수는 결국 객체에 대한 참조를 넘겨주는 것과 같다. 따라서 객체가 불변형이냐 가변형이냐에 따라서 객체가 수정될 수 있다. 하지만 객체의 정체성 자체는 변경할 수 없다.
def f(a, b):
a += b
return a
x = 1
y = 2
print(f(x, y))
> 3
print(x, y)
> 1 2
a = [1, 2]
b = [3, 4]
print(id(a))
> 4564388800
print(f(a, b), id(f(a, b)))
> [1, 2, 3, 4] 4564388800
print(a, b)
> [1, 2, 3, 4] [3, 4]
t = (10, 20)
u = (30, 40)
print(f(t, u), id(f(t, u)))
> (10, 20, 30, 40) 4482340720
print(t, u, id(t))
> (10, 20) (30, 40) 4481578688
- 불변형 객체인 숫자 x 는 변하지 않는다.
- 가변형 객체인 리스트 a 는 변한다. 그러나 id 값(객체의 정체성)은 변하지 않는다.
- 불변형 객체인 튜플 t 는 변하지 않는다. 함수의 결과물은 튜플 t 와는 다른 객체이다. 함수에서 내부적으로 불변 객체가 매개 변수일 때는 새로운 객체를 생성하기 때문이다. 단지 값만 복사하여 마치 같은 객체처럼 보이지만 둘은 서로 다르다.
8.4.1 가변형을 매개변수 기본값으로 사용하기 : 좋지 않은 생각
선택적 인수가 기본값을 갖는 것은 파이썬 함수 정의에서 좋은 기능으로, 하위 호환성을 유지할 수 있게 해준다. 그러나 매개변수의 기본값으로 가변 객체를 사용하는 것은 피해야 한다.
class HauntedBus:
"""유령 승객이 출몰하는 버스 모델"""
def __init__(self, passengers=[]):
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
passensgers 의 기본값을 빈 리스트로 한다.
bus1 = HauntedBus(["Alice", "Bill"])
print(bus1.passengers)
> ['Alice', 'Bill']
bus1.pick("Charlie")
bus1.drop("Alice")
print(bus1.passengers)
> ['Bill', 'Charlie']
bus2 = HauntedBus()
bus2.pick("Carrie")
print(bus2.passengers)
> ['Carrie']
bus3 = HauntedBus()
print(bus3.passengers)
> ['Carrie']
bus3.pick("Dave")
print(bus2.passengers)
> ['Carrie', 'Dave']
print(bus2.passengers is bus3.passengers)
> True
print(bus1.passengers)
> ['Bill', 'Charlie']
- bus2 는 기본값인 빈 리스트가 self.passengers 에 할당된다.
- bus3 도 기본값인 빈 리스트가 self.passengers 에 할당된다. 그러나 리스트는 비어있지 않다.
- bus3 에서 탄 승객이 bus2 에도 있다.
- bus2 와 bus3 은 같은 리스트 객체를 참조한다.
- bus1 은 별개의 리스트를 참조한다.
인스턴스를 생성할 때, 리스트를 매개변수로 전달하여 초기화하는 경우(bus1) 에는 문제가 없다. 빈 리스트로 인스턴스가 생성됐을 때만 문제가 된다. 각 기본값이 함수가 정의될 때 평가되고 기본값은 함수 객체의 속성이 된다. 따라서 기본값이 변경되면 그 내용이 함수의 호출에 영향을 미친다.
print(HauntedBus.__init__.__defaults__)
> (['Carrie', 'Dave'],)
bus2 와 bus3 의 passengers 속성은 defaults 속성에 바인딩되어 있다.
print(bus2.passengers is HauntedBus.__init__.__defaults__[0])
> True
이러한 문제 때문에 가변값을 받는 매개변수의 기본값으로 None 을 주로 사용한다. __init__() 메서드에서 passengers 인수가 None 인지 확인하고 새로 만든 빈 리스트를 self.passengers 에 할당한다.
8.4.2 가변 매개변수에 대한 방어적 프로그래밍
가변 매개변수를 받는 함수를 구현할 때는, 전달된 인수가 변경될 것이라는 것을 호출자가 예상할 수 있는지 없는지 신중하게 고려해야 한다.
class TwilightBus:
"""승객이 사라지게 만드는 버스 모델"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
- 이번에는 매개변수의 기본값을 None 으로 하고, 조건문으로 빈 리스트를 self.passengers 에 할당했다.
basketball_team = ["Sue", "Tina", "Maya", "Diana", "Pat"]
bus = TwilightBus(basketball_team)
bus.drop("Tina")
bus.drop("Pat")
print(basketball_team)
> ['Sue', 'Maya', 'Diana']
이번에는 basketball_team 이 탄 버스에서 두 명이 내렸는데, 아예 팀에서도 빠져버렸다.
TwilightBus 에서 self.passengers = passengers 에 의해 self.passengers 는 passengers 의 별명이 된다. 이 때 passengers는 인수로 전달된 basketball_team 의 또다른 별명이 된다.
이를 해결하기 위해서는 간단하다. 복사본을 만드는 것이다.
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
리스트 생성자를 통해서 사본을 만들면 동일한 객체를 참조하는 것이 아니기 때문에 문제가 해결된다. 또한 융통성도 향상된다. list 생성자는 모든 반복 가능한 객체를 받으므로, 튜플이나 집합 등 반복 가능한 객체는 모두 passengers 매개변수에 사용할 수 있게 된다.
8.5 del 과 가비지 컬렉션
del 명령은 이름을 제거하는 것이지, 객체를 제거하는 것이 아니다. del 명령의 결과로 객체가 가비지 컬렉트될 수 있지만, 제거된 변수가 객체를 참조하는 마지막 변수이거나 객체에 도달할 수 없을 때만 가비지 컬렉트된다. 변수를 다시 바인딩해도 객체에 대한 참조 카운트를 0으로 만들어 객체가 제거될 수 있다.
CPython 의 가비지 컬렉션은 주로 참조 카운트에 기반한다. 각 객체는 얼마나 많은 참조가 자신을 가리키는지 개수를 세고 있다. refcount 가 0이 되자마자 CPython이 객체의 __del__() 메서드를 호출하고 객체에 할당되어 있는 메모리를 해제함으로써 객체가 제거된다.
객체가 소멸되는 때를 보기 위해서 weakref.finalize() 를 사용하여 객체가 소멸될 때 호출되는 콜백 함수를 등록한다.
import weakref
s1 = {1, 2, 3}
s2 = s1
def bye():
print("Gone with the wind...")
ender = weakref.finalize(s1, bye)
print(ender.alive)
> True
del s1
print(ender.alive)
> True
s2 = "spam"
> Gone with the wind...
print(ender.alive)
> False
- s1, s2 는 동일한 집합을 참조한다.
- s1 이 가리키는 객체에 대해 bye() 콜백을 등록한다.
- finalize 객체가 호출되기 전의 alive 속성은 참이다.
- del 로 집합의 참조인 s1 을 제거한다.
- 또다른 참조인 s2 에 다른 객체를 바인딩하면 {1, 2, 3} 집합에 도달할 수 없게 된다. 집합이 제거되고, bye() 콜백이 호출된다.
위의 예제를 통해 del 이 객체를 제거하는 것이 아니라, 객체에 도달할 수 없게 되었을 때(참조 카운트가 0일 때) 제거됨을 알 수 있다.
8.6 약한 참조
객체가 메모리에 유지되거나 유지되지 않도록 만드는 것은 참조의 존재 여부다. 앞서 보았듯 객체 참조 카운트가 0이 되면 가비지 컬렉터는 해당 객체를 제거한다. 그러나 불필요하게 객체를 유지시키지 않으면서 객체를 참조할 수 있으면 도움이 되는 경우가 종종 있다. 캐시가 대표적이다.
약한 참조는 참조 카운트를 증가시키지 않고 객체를 참조한다. 참조의 대상인 객체를 참조 대상(referent) 이라고 한다. 따라서 약한 참조는 참조 대상이 가비지 컬렉트되는 것을 방지하지 않는다고 말할 수 있다.
import weakref
a_set = {0, 1}
wref = weakref.ref(a_set)
print(wref)
> <weakref at 0x10dfc4ae0; to 'set' at 0x10dfcf040>
print(wref())
> {0, 1}
a_set = {2, 3, 4}
print(wref())
> None
print(wref() is None)
> True
- 약한 참조 객체 wref 를 생성하고 이를 호출하면 참조된 객체 {0, 1} 을 반환한다.
- a_set 이 더 이상 {0, 1} 을 참조하지 않으므로 참조 카운트는 0이 되고 wref() 를 호출하면 None 이 출력된다.
- wref 는 약한 참조이므로 참조 카운트에 포함되지 않는다.
8.6.1 WeakValueDictionary
WeakValueDictionary 클래스는 객체에 대한 약한 참조를 값으로 가지는 가변 매핑을 구현한다. 참조된 객체가 프로그램 다른 곳에서 가비지 컬렉트되면 해당 키도 WeakValueDictionary 에서 자동으로 제거된다. 이 클래스는 일반적으로 캐시를 구현하기 위해 사용한다.
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return "Cheese(%r)" % self.kind
import weakref
stock = weakref.WeakValueDictionary()
catalog = [Cheese("Red Leicester"), Cheese("Tilsit"), Cheese("Brie"), Cheese("Parmesan")]
for cheese in catalog:
stock[cheese.kind] = cheese
print(globals())
> { ... , 'cheese': Cheese('Parmesan')}
print(sorted(stock.keys()))
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']
del catalog
print(sorted(stock.keys()))
> ['Parmesan']
del cheese
print(sorted(stock.keys()))
> []
del catelog 이후에 Parmesan 이 남은 이유는 무엇인가?
반복문이 실행된 이후에 전역변수로 cheese 에 Parmesan 이 할당되어 있다. 따라서 명시적으로 제거하기 전까지는 남아있다.
8.6.2 약한 참조의 한계
파이썬의 모든 객체가 약한 참조의 대상이 될 수 있는 것은 아니다. list, dict 객체의 서브 클래스는 약한 참조의 대상이 될 수 있다.
class MyList(list):
"""약한 참조의 대상이 될 수 있는 list 서브클래스"""
a_list = MyList(range(10))
wref_to_a_list = weakref.ref(a_list)
set 객체는 참조 대상이 될 수 있다. 그러나 int, tuple 객체는 클래스를 상속해도 약한 참조 대상이 될 수 없다.
8.7 파이썬의 특이한 불변형 처리법
t1 = (1, 2, 3)
t2 = tuple(t1)
print(t2 is t1)
> True
t3 = t1[:]
print(t3 is t1)
> True
튜플 t 에 대해 t[:] 는 사본을 생성하지 않고, 해당 객체에 대한 참조를 반환하다. tuple(t) 도 마찬가지다.
str, bytes, fronzenset 객체에서도 이와 동일한 동작을 볼 수 있다.
8.8 요약
모든 파이썬 객체는 정체성, 자료형, 값을 가지고 있다. 코드가 실행되는 동안 객체는 값만 바뀔 뿐이다.
변수 두 개가 동일한 값을 가진 불변 객체를 가리키고 있다면 변수가 각각의 사본을 가리키고 있는지 아니면 동일한 객체에 대한 별명인지는 중요하지 않다. 어쨋든 불편 객체는 변하지 않기 때문이다.
파이썬은 객체의 참조 사본을 매개변수로 넘겨준다. 따라서 객체에 따라서 함수 내에서 값이 바뀔 수도, 바뀌지 않을 수도 있다.
변수가 참조를 담고 있다는 사실은 아래와 같은 영향을 미친다.
- 단순 할당문은 사본을 생성하지 않는다.
- += 나 *= 같은 복합 할당 연산자는 왼쪽 변수가 불변 객체에 바인딩되어 있을 때는 객체를 새로 생성하고, 가변 객체에 바인딩되어 있을 때는 기존 객체를 변경한다.
- 기존 변수에 새로운 값을 할당하면 기존에 바인딩되어 있던 객체를 변경하지 않는다. 이것을 rebinding 이라고 하며, 변수가 새로운 객체에 바인동되도록 만든다. 그 변수가 기존 객체를 참조하는 마지막 참조였다면, 기존 객체는 가비지 컬렉트된다.
- 함수 매개변수는 별명을 전달하므로, 함수는 인수로 전달받은 가변 객체를 모두 변경할 수 있다. 가변 객체의 변경을 막으려면 함수 안에서 사본을 생성하거나, 리스트 대신 튜플을 전달하는 등 불변 객체를 사용해야 한다.
- 함수 매개변수의 기본값으로 가변 객체를 사용하는 것은 위험하다. 매개변수를 변경하면 기본값이 변경되어 이 기본값을 사용하는 함수가 나중에 호출될 때 영향을 받기 때문이다.
'python' 카테고리의 다른 글
Fluent Python (챕터 9) - 파이썬스러운 객체 (0) | 2021.12.09 |
---|---|
Architecture Patterns with Python(8장) (0) | 2021.12.04 |
Architecture Patterns with Python(6장) (0) | 2021.11.12 |
Architecture Patterns with Python(5장) (0) | 2021.11.06 |
Fluent Python (챕터 7) (0) | 2021.11.06 |