파이썬의 객체 참조, 가변성, 재사용

휴먼스케이프

안녕하세요. 휴먼스케이프의 개발자 bruno입니다.

이 포스트에서는 파이썬 객체의 참조, 가변성, 재사용에 대해서 얘기해보겠습니다.

객체 참조

파이썬의 변수는 객체에 붙은 라벨이다.

>>> a = [1, 2, 3]
>>> b = a     # a가 참조하는 객체를 b도 참조하게 된다
>>> a.append(4)
>>> b
[1, 2, 3, 4]

b = a 와 같은 코드를 실행하면 b 변수에 a 변수가 참조하던 객체 값을 할당하게 된다.

객체의 id, 동질성, alias

모든 객체는 객체의 정체성(identity)을 나타내는 id를 가진다. id() 연산자로 확인할 수 있다.

>>> bruno = {'name': 'Bruno', 'born': 1988}
>>> bhkim = bruno
>>> bhkim is bruno      # 참조하는 객체의 id를 비교한다.
True
>>> id(bruno), id(bhkim)
(4457716224, 4457716224)
>>> bruno['balance'] = 1000000000
>>> bhkim
{'name': 'Bruno', 'born': 1988, 'balance': 1000000000}
>>> bruno2 = {'name': 'Bruno', 'born': 1988, 'balance': 1000000000}
>>> bhkim == bruno2   # 값 비교
True
>>> bhkim is bruno2    # id 비교 (정체성 비교)
False

is 연산자는 참조하는 객체의 identity, 즉 id를 비교한다.

== 연산자는 참조하는 객체의 값을 비교한다.

위 예에서 bhkim은 bruno가 가리키는 객체를 참조하는 alias(별명)가 된다.

튜플의 상대적 불변성

튜플은 불변형(frozenset)이지만 참조된 항목은 변할 수 있다. 튜플 안에 있는 변수는 객체를 참조하기 때문이다. 튜플 안에서 변경되지 않는 것은 튜플이 담고 있는 항목들의 identity(정체성)이다.

>>> t1 = (1, 2, [30, 40])
>>> t2 = (1, 2, [30, 40])
>>> t1 == t2
True
>>> id(t1[-1])
4458658880
>>> t1[-1].append(99)    # 튜플은 불변형이지만 가변형을 멤버로 가질 수 있다.
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1])
4458658880
>>> t1 == t2
False

객체의 복사

기본 복사는 얕은 복사(shallow copy)를 생성한다.

>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1)    # 기본 복사(얕은 복사 생성)
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1    # 값은 같다.
True
>>> l2 is l1    # 새로운 객체가 생성되었음을 알 수 있다.
False

얕은 복사는 최상위 컨테이너는 복제하지만 사본은 원래 컨테이너에 들어 있던 동일 객체에 대한 참조로 채워진다.

>>> l1.append(100)    # 튜플이 가진 가변형 객체는 변경 가능
>>> l1[1].remove(55)    # 항목 제거도 가능
>>> l1
[3, [44], (7, 8, 9), 100]
>>> l2    # l2에는 100이 추가되지 않았다.
[3, [44], (7, 8, 9)]
>>> l2[1] += [33, 22]
>>> l2[2] += (10, 11)    # 새로운 튜플을 만들어서 l2[2]에 다시 바인딩
>>> l1[2] is l2[2]    # 더 이상 같은 튜플이 아니다.
False
>>> l1
[3, [44, 33, 22], (7, 8, 9), 100]
>>> l2
[3, [44, 33, 22], (7, 8, 9, 10, 11)]

깊은 복사와 얕은 복사

참조가 아닌 내부 객체의 값을 복사할 필요가 있을 때가 있다. 이를 깊은 복사라고 한다.

deepcopy()는 깊은 복사, 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)

Bus 클래스는 내부에 passengers라는 리스트 컨테이너를 멤버로 가지는 클래스이다.

>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)    # 얕은 복사
>>> bus3 = copy.deepcopy(bus1)    # 깊은 복사
>>> bus1.drop('Bill')    # 원래 변수에서 객체를 변경한다.
>>> bus2.passengers    # 얕은 복사에선 내부 객체의 참조를 복사해서 변경이 반영됨
['Alice', 'Claire', 'David']
>>> bus3.passengers    # 깊은 복사의 내부 컨테이너는 새로운 정체성(identity)를 가짐
['Alice', 'Bill', 'Claire', 'David']
>>> id(bus1), id(bus2), id(bus3)    # 얕은 복사, 깊은 복사 모두 변수가 참조하는 객체의 정체성은 다르다.
(4457596336, 4458588624, 4458589392)

얕은 복사와 깊은 복사 모두 객체를 새로 만들어서 각 변수가 참조하는 객체의 정체성(identity)는 다르다. 하지만 얕은 복사에서는 객체 내부의 컨테이너가 참조하는 객체는 기존 변수의 객체 내부의 컨테이너 참조 값을 그대로 가져오므로, 같은 정체성의 컨테이너를 참조하게 된다.

copy()와 deepcopy() 메소드는 각각 __copy__(), __deepcopy__() 던더 메소드를 구현해서 동작을 제어할 수 있다.

참조로서의 함수 매개변수

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

call by sharing은 ‘함수의 각 매개변수가 인수로 전달받은 각 참조의 사본을 받는다.’는 의미다.

즉 함수 안의 매개변수는 실제 인수의 별명이 된다. 때문에 인수로 전달받은 모든 가변 객체를 변경할 수 있지만, 객체의 identity(정체성)은 변경할 수 없다.

>>> def f(a, b):
...     a += b
...     return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)    # 숫자 x는 변경되지 않는다.
3
>>> x, y
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)    # 가변형인 리스트 a는 변경된다.
[1, 2, 3, 4]
>>> a, b
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u    # 불변형인 튜플 t는 변경되지 않는다.
((10, 20), (30, 40))

가변형을 매개변수 기본값으로 사용하지 말자

가변형을 매개변수 기본값으로 사용하게 되면, 초기화 되지 않은 가변형 객체를 공유하게 되는 문제가 발생한다.

class HauntedBus:
    '''A bus model haunted by ghost passengers'''
    def __init__(self, passengers=[]):
        self.passengers = passengers
    def pick(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.remove(name)
>>> bus1 = HauntedBus(['David', 'Henry'])
>>> bus1.passengers
['David', 'Henry']
>>> bus1.pick('Ted')
>>> bus1.drop('David')
>>> bus1.passengers
['Henry', 'Ted']
>>> bus2 = HauntedBus()    # 초기화 되지 않은 가변형 할당
>>> bus2.pick('loowin')
>>> bus2.passengers
['loowin']
>>> bus3 = HauntedBus()    # 초기화 되지 않은 가변형 할당
>>> bus3.passengers
['loowin']
>>> bus3.pick('jake')
>>> bus2.passengers
['loowin', 'jake']
>>> bus2.passengers is bus3.passengers    # 다른 객체가 컨테이너 공유
True
>>> bus1.passengers
['Henry', 'Ted']

위 예에서 self.passengers 가 passengers 매개변수 기본값의 별명이 되기 때문에, 초기화되지 않은 매개변수를 전달하면 컨테이너를 공유하게 된다.

해결 방법은 __init__() 메서드가 passengers 인수를 받을 때, 인수의 사본으로 self.passengers를 초기화하는 것이다.

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

del과 가비지 컬렉션

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

파이썬은 각 객체를 참조하고 있는 변수가 몇 개인지를 reference count(참조 카운트)에 관리한다. 각 객체를 얼마나 많은 참조가 가리키는지 그 개수 refcount를 세고 있다.

이 refcount가 0이 될 때, 객체는 제거된다, 즉 메모리에서 해제된다.

>>> import weakref    # 사라지는 객체를 보기 위해 약한 참조 사용
>>> s1 = {1, 2, 3}
>>> s2 = s1    # s2에 s1의 참조 할당
>>> def bye():
...     print('Gone with the wind...')
...
>>> ender = weakref.finalize(s1, bye)    # 약한 참조로 객체 소멸 콜백 등록
>>> ender.alive    # alive 속성은 객체가 살아있는지 알려준다.
True
>>> del s1    # 변수만 제거된다.
>>> ender.alive
True
>>> s2 = 'spam'    # 참조가 모두 제거되면서, 객체가 소멸된다.
Gone with the wind...
>>> ender.alive
False

약한 참조

약한 참조는 refcount를 증가시키지 않는 참조다. 즉 약한 참조는 참조 대상이 가비지 컬렉트되는 것을 방지하지 않는다.

정리

변수가 객체의 참조값을 가진다는 것, 불변형 내부에도 가변형 컨테이너의 참조를 가질 수 있다는 것 등, 참조로 인해서 발생할 수 있는 실수들이 많습니다. 실수가 잦을 수 있는 주제이므로 한 번씩 확인해보면 좋을 것 같습니다.

감사합니다.

이 포스트는 루시아누 하말류의 책 [Fluent Python]을 참고해서 작성했습니다.

Get to know us better! Join our official channels below.

Telegram(EN) : t.me/Humanscape KakaoTalk(KR) : open.kakao.com/o/gqbUQEM Website : humanscape.io Medium : medium.com/humanscape-ico Facebook : www.facebook.com/humanscape Twitter : twitter.com/Humanscape_io Reddit : https://www.reddit.com/r/Humanscape_official Bitcointalk announcement : https://bit.ly/2rVsP4T Email : support@humanscape.io

기업문화 엿볼 때, 더팀스

로그인

/