Computer Engineering/Fluent Python 정리

Fluent Python Chapter 8. 객체 참조, 가변성, 재활용

jordan.bae 2022. 1. 27. 23:06

Chapter1의 Introduction 부분에서 이야기 한 것 처럼 지난 5년간 다양한 언어나 소프트웨어를 공부하고 이용하여 소프트웨어를 개발했는데 이것 저것 하다보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올 해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한해를 만들고 싶은 소망을 가지고 있습니다. 21장 까지 정리를 성공하고 맛있는걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요!

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

 

 

Chapter8 - Introduction

이 장은 Part 객체 지향 상용구의 시작 chapter이다. 객체에 대한 본격적인 이야기를 나누기전에 파이썬의 객체에서 대해서 조금 더 알아보는 느낌을 받았다. 주로 파이썬에서 변수가 가변객체와 불변객체를 각각 참조하는 방식과 이런 방식에 따른 특성 및 주의 할 점등을 다룬다. 그 외에 객체의 종류에 따라서 얕은 복사, 깊은 복사를 했을 때 동작하는 방식에 대해 다루고, Garbage Collector가 어떻게 사용하지 않는 객체를 제거 하는지에 대해서도 가볍게 다룬다.

 

 

변수

변수에 대한 정확한 이해가 중요하다. 객체 지향 언어에서는 변수가 참조 변수이기 때문에 흔히 비유하는 '상자로서의 변수' 개념이 아니다. 책에서는 '파이썬에서의 변수는 자바에서의 참조 변수와 같으므로 변수는 객체에 붙은 레이블이라고 생각하는 것이 좋다.' 라고 말합니다.

 

아래 불변형 객체, 가변형 객체를 참조하는 변수가 어떻게 다른지 이해하는 건 파이썬에서 변수를 잘 이해하고 왜 가변형 객체를 다룰 때 유의해야 하는지를 보여줍니다.

 

불변형 객체를 참조할 때 -> 변수를 이용해서 객체를 변경하려고 할 때 새로운 객체를 만들어서 참조해서 원래 객체를 참조하고 있던 변수에는 영향이 없음.

 

가변형 객체를 참조할 때 -> 변수를 이용해서 객체를 변경할 때 기존에 객체를 참조하던 변수에도 영향을 미침.

 

정체성, 동질성, 별명

위에서 본 것 처럼 변수는 객체를 참조하는 참조 변수이다. 그래서 변수를 객체의 alias(별명)라고도 한다.

 

아래는 동일한 객체를 참조하는 두 변수(lewis, charles)이고, lewis와 charles는 별명이다. 동일한 객체 바인딩 되어 있다.

charles = {"name": "charles"}
charles2 = {"name": "charles"}
lewis = charles


# 정체성 (identity)
print(charles is lewis)
True

print(charles is charles2)
False

id(lewis), id(charles)
(4551894528, 4551894528)

# 동치성
charles == charles2
True

Chapter2에서 이야기 했던 것 처럼 모든 객체는 정체성, 자료형, 값을 가지고 잇다. 정체성은 생성된 후에 변경되지 않는다.

정체성은 메모리 내의 객체 주소이다. is 연산자는 두 객체의 정체성을 비교한다. id

 

얕은 복사, 깊은 복사

얕은 복사는 객체를 복사할 때 새로운 객체를 만들어 참조하지만 내부의 가변형 객체에 대해서는 새로운 객체를 만들지 않고 그대로 참조한다. 이 때문에 메모리 관점에서의 효율성은 있지만 문제가 생기는 경우가 발생할 수 있다.

 

얇은 복사를 하는 방법은 크게 3가지 이다.

import copy


l1 = [1,[1,2],3]

# shallow copy
l1_1 = list(l1)
l1_2 = l1[:]
l1_3 = copy.copy(1)

위에 코드에서 처럼

1) 생성자

2) [:]

3) copy.copy

 

위에 코드가 어떻게 동작하는지 python tutor를 통해서 살펴보면 아래와 같다.

두 번째 원소의 가변형 객체인 [1, 2]를 새로 생성하지 않고 같은 객체를 가리키고 있다. 이렇게 되면 l1에서 변경하면 l1_1, l2_1, l3_1이 가리키는 객체의 값이 모두 바뀌게 된다.

 

이런 문제를 피하고 싶다면 deep copy를 사용하면 된다. deepcopy(깊은 복사)는 가변형객체도 새로운 객체를 만들어서 복사한다.

 

참조로서의  함수 매개변수

파이썬에서 call by sharing방식으로 전달된다. 

매개 변수는 기본 자료형 (string, integer 등)은 call by value지만 sequence등의 객체등은 인수로 전달받은 각 참조의 사본을 받는 다는 의미다. 이 말은 불편형 객체는 새로운 객체가 생성됨으로 상관없지만 가변형 객체는 같은 객체를 참조함으로 문제가 발생할 수 있다는 의미다. 아래의 코드를 보면 알 수 있다.

 

이런 문제 때문에 함수의 인수로 불변형을 사용함으로써 방어적 프로그래밍을 하는 것이 좋다.

 

가변형 매개변수를 기본값으로 사용하면 다음과 같은 문제가 발생할 수 있다.

# BEGIN HAUNTED_BUS_CLASS
class HauntedBus:
    """A bus model haunted by ghost passengers"""

    def __init__(self, passengers=[]):  # <1>
        self.passengers = passengers  # <2>

    def pick(self, name):
        self.passengers.append(name)  # <3>

    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.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus()
>>> bus3.passengers
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers
True
>>> bus1.passengers
['Bill', 'Charlie']
>>> dir(HauntedBus.__init__)  # doctest: +ELLIPSIS
['__annotations__', '__call__', ..., '__defaults__', ...]
>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True

 

Garbage Collection과 약한 참조

garbage collection에 대해서 이야기 하기 전에 del 명령에 대해 살펴보면 del 명령은 참조 변수를 제거하는 것이지, 객체를 제거하는 것이 아니다. del명령어의 결과로 reference count가 0이 되면 garbage collector가 collect한다. CPython의 경우 garbage collecton은 주로 reference count에 기반한다. refcount가 0이 되자마자 CPython이 객체의 __del__() 메서드를 호출하고 객체에 할당되어 있는 메모리를 해체함으로써 객체가 제거된다.

 

약한 참조는 reference count를 증가시키지 않고 객체를 참조한다. 참조의 대상인 객체를 참조대상 (referent)라고 한다. 약한 참조는 캐시 어플리케이션에서 유용하게 사용된다. 캐시가 참조하고 있다고 해서 캐시된 객체가 계속 남아 있기를 원하지 않기 때문이다.

 

import weakref
s1 = {1, 2, 3}
s2 = s1

def bye():
    print('bye callback function')
    
ender = weakref.finalize(s1, bye)
ender.alive
True

del s1
ender.alive
True

# 더 이상 참조하지 않을 때 callback function이 실행됨. 
# weak reference는 객체가 garbage collect되는데 영향을 주지 않음.
s2 = 'spam'
bye callback function

ender.alive
False



정리

Chapter8에서는 변수가 어떻게 객체를 참조하는지 살펴봤다. 특히, 불변형 객체일 때와 가변형 객체일 때에 대해서 동작이 다르므로 이에 따라서 주의할 부분들을 살펴봤다. 또, shallow copy와 deep copy 그리고 garbage collector에 대해서도 간단히 살펴봤다.

파이썬에서 참조를 이해하는 것은 굉장히 중요하다. 생각보다 이와 관련된 문제들이 자주 발생할 수 있기 때문이다. 이런 부분에 대해서 방어적인 프로그래밍으로 조금 더 좋은 코드를 만들 수 있을 것 이다.


Reference

- 위에 변수 부분의 캡쳐는 python tutor 라는 사이트에서 visualize해주는 기능을 사용했다.

https://pythontutor.com/visualize.html#mode=edit

 

Python Tutor - Visualize Python, Java, JavaScript, C, C++, Ruby code execution

Write code in Python 3.6 Java 8 JavaScript ES6 C (gcc 9.3, C17 + GNU extensions) C++ (g++ 9.3, C++20 + GNU extensions) ------ [unsupported] Python 2.7 [unsupported] C (gcc 4.8, C11) [unsupported] C++ (g++ 4.8, C++11) [unsupported] TypeScript 1.4 [unsupport

pythontutor.com

 

- Fluent Python Chapter 8 

 

반응형