객체 참조, 가변성, 재활용 - (1)

Sanghun Moon·2022년 1월 3일
0

python

목록 보기
4/4

이번 장에서 다루는 내용

  • 파이썬 변수를 은유적으로 표현 - 변수는 이름표지, 상자가 아니다 - (1)
  • 객체의 정체성, 동질성, 별명의 개념 - (1)
  • 얕은 복사와 깊은 복사 - (1)
  • 참조 및 함수 매개변수 - (1) 가변 매개변수가 기본이 될 때의 문제 및 함수 호출자가 전달한 가변 인수의 안전한 처리
  • 가비지 컬렉션(gc) - (2)
  • del 명령 및 객체를 보존하지 않으면서 객체를 기억하기 위해 약한 참조를 사용하는 방법 - (2)

변수는 상자가 아니다

상자로서의 변수 개념이 실제로는 객체지향 언어에서 참조 변수를 이해하는 데 방해가 된다 - 린 안드레아 스타인

→ 파이썬 변수는 자바에서의 참조 변수와 같으므로 변수는 객체에 붙은 레이블이라고 생각하는 것이 좋다

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

참조 변수

→ 변수가 객체에 할당 (O), 객체를 변수에 할당 (X)

결국 객체는 변수가 할당되기 전에 생성

변수는 단지 레이블일 뿐이므로 객체에 여러 레이블을 붙이지 못할 이유가 없다

여기서 여러 레이블을 붙이는 것을 별명이라고 한다

정체성, 동질성, 별명

모든 객체는 정체성, 자료형 값을 가지고 있다.

객체의 정체성은 일단 생성한 후에는 결코 변경되지 않는다.

  • 정체성은 메모리 내의 객체 주소라고 생각할 수 있다.
  • is 연산자는 두 객체의 정체성을 비교한다.
  • id() 함수는 정체성을 나타내는 정수를 변환한다.

객체의 정체성 - id

객체의 id는 실제 구현마다 다르다. (CPython 의 경우 객체의 메모리 주소 반환)

id는 객체마다 고유한 레이블이라는 것을 보장

객체가 소멸할 때까지 결코 변하지 않는다

정체성 검사는 주로 is 연산자를 수행

객체의 별명 - alias

charles = {'name': 'Charles L', 'born': 1832}

lewis = charles

print(lewis is charles)
print(id(charles), id(lewis))

>> True
>> 140682057366192 140682057366192 

alex = {'name': 'Charles L', 'born': 1832}
print(alex == charles)
print(alex is charles)

>> True
>> False

lewis와 charles 는 별명이다

alex는 charles 에 대한 별명이 아니다

  • 두 변수는 서로 다른 객체에 바인딩
  • 각각의 바인딩된 객체가 동일한 값을 갖고 있으므로 == (동치 연산자)에 의해 동일하다고 판단되지만, 정체성은 다름

Untitled

기본 복사는 얕은 복사

리스트나 대부분의 내장 가변 컬렉션을 복사하는 가장 간단한 방법은

그 자료형 자체의 내장 생성자를 사용하는 것 - 리스트는 list(n)

l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1)
print(l2)
print(id(l2), id(l1))
print(l2 == l1)

>> [3, [55, 44], (7, 8, 9)]
>> 140674810171912 140674810267976
>> True

생성자나 [:] 를 사용하면 얕은 사본을 생성

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

모든 항목이 불변형 → 이 방식은 메모리를 절약하며 아무런 문제를 일으키지 않음

가변 항목이 들어 있을 때는 불쾌한 문제를 야기할 수 있음

l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)

l1.append(100)
l1[1].remove(55)

print("l1: ", l1)
print("l2: ", l2)

l2[1] += [33, 22]
**l2[2] += (10, 11)**
print("l1: ", l1)
print("l2: ", l2)

>> l1:  [3, [66, 44], (7, 8, 9), 100]
>> l2:  [3, [66, 44], (7, 8, 9)]
>> l1:  [3, [66, 44, 33, 22], (7, 8, 9), 100]
>> l2:  [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

l2[2] += (10, 11) 에서 += 연산자는 새로운 튜플을 만들어서 해당 리스트에 바인딩을 한다

그렇게 때문에 이제 l1 과 l2에 있는 튜플은 더 이상 동일 객체가 아니다

참고 - 불변형 객체에서의 += 연산자

a = (1, 2, 3)
print(id(a))

a += (2, 3)
print(id(a))

>> 140233635521664
>> 140234440909224

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

얕은 복사는 객체 내부에 불변형 객체가 있을 경우에 의도하지 않은 결과를 나타낼 수 있다

얕게 복사한다고 해서 늘 문제가 생기지는 않지만, 내포된 객체의 참조를 공유하지 않도록

깊게 복사할 필요가 있기도 하다

파이썬의 copy 모듈에서는 다음과 같은 복사를 지원

  • deepcopy() 함수는 깊은 복사
  • copy() 함수는 얕은 복사
class Bus:

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

    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))

bus1.drop('Bill')
print(bus2.passengers)
print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
print(bus3.passengers)

>> 140378189219880 140378189239744 140378189274528
>> ['Alice', 'Claire', 'David']
>> 140378189307656 140378189307656 140378189269704
>> ['Alice', 'Bill', 'Claire', 'David']

깊은 사본을 만들 경우

객체 안에 순환 참조가 있으면 단순한 알고리즘은 무한 루프에 빠질 수 있다

deepcopy() 함수는 순환 참조를 제대로 처리하기 위해 이미 복사한 객체에 대한 참조를 기억하고 있다

깊은 복사가 너무 깊이 복사하는 경우도 있다

이 경우에는 복사하면 안되는 외부 리소스나 싱글턴(singleton)을 객체가 참조할 수가 있다

copy 모듈 문서에서는

__copy__(), __deepcopy__() 특별 메소드를 구현해서

copy() 와 deepcopy() 의 동작을 제어할 수 있다

특별 메소드 구현으로 copy, deepcopy 동작 제어

from copy import copy, deepcopy

class A(object):
    def __init__(self):
        print 'init'
        self.v = 10
        self.z = [2,3,4]

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result

a = A()
a.v = 11
b1, b2 = copy(a), deepcopy(a)
a.v = 12
a.z.append(5)
print b1.v, b1.z
print b2.v, b2.z

참조로서의 함수 매개변수

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

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

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

함수는 인수로 전달받은 모든 가변 객체를 변경할 수 있지만,

객체의 정체성 자체는 변경할 수 없다 (어떤 객체를 다른 객체로 바꿀 수는 없다)

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

기본값을 가진 선택적 인수는 함수 정의에서 유용하며

하위 호환성을 유지하며 API 를 개선할 수 있게 한다

그러나 매개변수 기본값으로 가변 객체를 사용하는 것은 피해야 한다

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)

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 is bus2.passengers) # False
class Bus:
    def __init__(self, **passengers=[]**):
        self.passengers = passengers

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

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

bus1 = Bus([1, 2])
bus2 = Bus([1, 2])
bus3 = Bus([3])
**bus4 = Bus()
bus5 = Bus()**

print(id(bus1.passengers)) # 140659642647240
print(id(bus2.passengers)) # 140659642648904
print(id(bus3.passengers)) # 140659642632904
**print(id(bus4.passengers)) # 140659642542152
print(id(bus5.passengers)) # 140659642542152**

IDE 에서 매개변수의 default 를 가변형으로 넣었을 때의 경고 (이 경우 None 으로 넣어야 한다)

Untitled

결국 명시적인 승객 리스트로 초기화되지 않은 Bus 객체들이 승객 리스트를 공유하는 문제가 발생한다

self.passengers 가 passengers 매개변수 기본값의 별명이 되기 때문이다

문제는 각 기본값이 함수가 정의될 때 평가되고 기본값은 함수 객체의 속성이 된다는 것이다

따라서 기본값이 가변 객체고, 이 객체를 변경하면 변경 내용이 향후에 이 함수의 호출에 영향을 미친다

가변 기본값에 (mutable default argument) 대한 이러한 문제 때문에

가변 값을 받는 매개변수의 기본값으로 None을 주로 사용한다

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

가변 매개변수를 받는 함수를 구현시에는

전달된 인수가 변경될 것이라는 것을 호출자가 예상할 수 있는지 없는지 신중하게 고려해야 한다

중요한 것은 함수 구현자와 함수 호출자가 예상하는 것을 일치하는 것이다

bus.passengers 가 생성자에 전달된 리스트의 별명이 되어 의도치 않은 결과가 발생하는 경우

from scratch_25 import Bus

basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = Bus(basketball_team)
bus.drop('Tina')
bus.drop('Pat')

print(basketball_team)

['Sue', 'Maya', 'Diana']

의도대로 제대로 구현하는 경우

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

list() 생성자는 모든 반복 가능한 객체 (iterable object) 를 받기 때문에

튜플은 물론 집합이나 데이터베이스 결과 등의 반복가능한 객체는 저렇게 처리할 수 있다

인수로 받은 객체를 메서드가 변경할 것이라는 의도가 명백하지 않는 한
클래스 안에서 인수를 변수에 할당함으로써 인수 객체에 별명을 붙이는 것에 대해 주의할 필요가 있다
불명확한 경우에는 사본을 만들어라
여러분이 만든 클래스를 사용하는 프로그래머들의 행복도가 향상될 것이다


출처

전문가를 위한 파이썬

https://engkimbs.tistory.com/667

https://stackoverflow.com/questions/1500718/how-to-override-the-copy-deepcopy-operations-for-a-python-object

해당 글은 전문가를 위한 파이썬을 정리한 글입니다
저작권에 문제가 있으면 삭제하도록 하겠습니다

profile
Python Server Developer

0개의 댓글