Computer Engineering/Fluent Python 정리

Fluent Python Chapter 9. 파이썬스러운 객체

jordan.bae 2022. 1. 29. 23:21

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. 메타프로그래밍)

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

 

 

Chapter9 - Introduction

Chapter9의 제목은 파이썬스러운 객체이다. 

여기서 말하는 파이썬스러운 객체는 1장에서 파이썬의 데이터 모델을 살펴본 것 처럼 사용자가 정의한 자료형도 내장형과 같이 매직 메서드들을 구현함으로써 자연스럽게 그리고 일관성 있게 동작할 수 있는 부분을 의미한다. 이를 상속하지 않고 덕 타이핑 메커니즘을 통해 구현할 수 있다. 매직 메서드들을 구현함으로써 내장 데이터 객체처럼 다양한 인터페이스들을 사용하라 수 있다.

이 장에서는 Vector class를 구현하면서 점진적으로 파이썬스러운 객체를 만들어 나간다.

 

 

객체를 표현

객체를 문자열과/바이트로 표현하는 magic method들

- __repr__() -> 객체를 개발자가 보기 위한 용도로 표현해서 문자열로 반환하는 함수.

- __str__() -> 사용자 보고자 하는 상태로 표현해서 문자열로 반환하는 함수

- __format__() -> 객체를 formating해서 표현해서 문자열로 반환하는 함수

- __bytes__() -> 객체를 bytes로 변환해서 반환하는 함수

 

아래는 책에서 Vector2d 클래스에 해당 magic method를 구현한 코드이다. magic method들을 구현해서 파이썬 내장형 데이터 모델들처럼 내장형 함수를 사용할 수 있게된다. 

from array import array
import math


class Vector2d:
    typecode = 'd'  # <1>

    def __init__(self, x, y):
        self.x = float(x)    # <2>
        self.y = float(y)

    def __iter__(self):
        return (i for i in (self.x, self.y))  # <3>

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)  # <4>

    def __str__(self):
        return str(tuple(self))  # <5>

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +  # <6>
                bytes(array(self.typecode, self)))  # <7>
    def angle(self):
        return math.atan2(self.y, self.x)
        
    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self), self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)
    
    def __eq__(self, other):
        return tuple(self) == tuple(other)  # <8>

    def __abs__(self):
        return math.hypot(self.x, self.y)  # <9>

    def __bool__(self):
        return bool(abs(self))  # <10>

# magic method를 구현함으로써 파이썬 내장형 모델과 같이 내장 함수들을 이용.

v = Vector2d(1,2)

print(repr(v), str(v), format(v), bytes(v))
Vector2d(1.0, 2.0) (1.0, 2.0) (1.0, 2.0) b'd\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@'

 

객체를 숫자로 변환하는 magic method들

- __abs__() : abs() 함수에서 호출하는 magic method

- __bool__(): bool() 함수에서 호출하는 magic method

 

해시할 수 있게 해주는 magic method들

__hash__(), __eq__() 함수를 구현하면 해시 가능한 객체가 되어서 dictionary, set 의 key로 사용할 수 있다.

- __hash__(): hash()함수에서 호출하는 magic method

- __eq__() : 두 객체의 값을 비교할 수 있게 해주는 magic method

 

class Vector2d:
    typecode = 'd'  # <1>

    def __init__(self, x, y):
        self.x = float(x)    # <2>
        self.y = float(y)
        
    def __eq__(self, other):
        return tuple(self) == tuple(other)  # <8>

    def __hash__(self):
        return hash(self.x) ^ hash(self.y)
 
abs(v)
2.23606797749979

bool(v)
True

hash(v)
3

{v: 1}
{Vector2d(1.0, 2.0): 1}

 

 

@classmethod와 @staticmethod

- @classmethod decorator는 객체가 아닌 클래스에 연산을 수행하는 메서드

- @staticmethod decoraotr는 클래스나 객체를 직접적으로 사용하지 않으나 클래스에 내에 두는 메서드. 밖에서 정의해도 상관없음.

 

아래 코드를 보면 클래스를 함수 내에서 사용하고 있음. -> cls(*memv)

class Vector2d:
    typecode = 'd'
	
    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(*memv)

 

 

파이썬에서의 비공개 속성과 보호된 속성

@property decorator로 property getter 메서드 만들기. 

class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y
        
v = Vector2d(1,2)

print(v.x, v.y)
1.0 2.0

v.x = 5
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [30], in <module>
----> 1 v.x = 5

AttributeError: can't set attribute

파이썬에서에서는 private 과 같은 keyword가 있는 자바와 달리 비공개 변수를 생성할 수 있는 방법은 없지만 서브클래스에서 '비공개' 성격의 속성을 실수로 변경하지 못하게 하는 간단한 메커니즘은 있다. 파이썬은 속명을 두 개의 언버바로 시작하고 언더바 없이 또는 하나의 언더바로 끝나도록 정의하면, 파이썬은 언더바와 클래스명을 변수명 앞에 붙여 객체의 __dict__에 저장한다. 이러한 파이썬 언어 기능을 name mangling(이름 장식)이라고 한다. 

v.__dict__
{'_Vector2d__x': 1.0, '_Vector2d__y': 2.0}

 

name mangling은 안전을 제공하지만, 보안 기능은 아니다. 실수로 접근하는 것을 막도록 설계되어 있지만 고의적인 악용을 막지는 못한다. 고의적인 악용을 막기 위해 언어를 설계하는 것은 대부분은 쓸데 없는 걱정이다. 개발을 하다보면 정말 그런 경우가 많다.

 

 

 

__slots__  클래스 속성으로 메모리 공간 절약하기

파이썬은 객체 속성을 각 객체 속성을 각 객체 안의 __dict__라는 딕셔너리형 속성에 저장한다. 딕셔너리는 빠른 접근 속도를 제공하기 위해서 해 테이블을 유지하므로 메모리 부담이 크다. 그래서 수 만개의 객체를 다룰 경우가 있다. __slots__ 클래스 속성을 이용해서 메모리 속성을 엄청나게 줄일 수 있다. __slots__을 사용하면 불변형인 tuple에 저장하기 때문에 메모리를 많이 아낄 수 있다. 

class Vector3d:
    typecode = 'd'

    def __init__(self, x, y, z):
        self.x = float(x)
        self.y = float(y)
        self.z = float(z)
        
        self.test_attr = x/y

vectors = [Vector3d(i, i, i) for i in range(1, 11)]
for v in vectors:
    print(v.__dict__)
    
{'x': 1.0, 'y': 1.0, 'z': 1.0, 'test_attr': 1.0}
{'x': 2.0, 'y': 2.0, 'z': 2.0, 'test_attr': 1.0}
{'x': 3.0, 'y': 3.0, 'z': 3.0, 'test_attr': 1.0}
{'x': 4.0, 'y': 4.0, 'z': 4.0, 'test_attr': 1.0}
{'x': 5.0, 'y': 5.0, 'z': 5.0, 'test_attr': 1.0}
{'x': 6.0, 'y': 6.0, 'z': 6.0, 'test_attr': 1.0}
{'x': 7.0, 'y': 7.0, 'z': 7.0, 'test_attr': 1.0}
{'x': 8.0, 'y': 8.0, 'z': 8.0, 'test_attr': 1.0}
{'x': 9.0, 'y': 9.0, 'z': 9.0, 'test_attr': 1.0}
{'x': 10.0, 'y': 10.0, 'z': 10.0, 'test_attr': 1.0}


class Vector3d:
    __slots__ = ('x', 'y', 'z', 'test_attr')
    typecode = 'd'

    def __init__(self, x, y, z):
        self.x = float(x)
        self.y = float(y)
        self.z = float(z)
        
        self.test_attr = x/y
        
vectors = [Vector3d(i, i, i) for i in range(1, 11)]
for v in vectors:
    print(v.__dict__)

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [50], in <module>
      1 vectors = [Vector3d(i, i, i) for i in range(1, 11)]
      2 for v in vectors:
----> 3     print(v.__dict__)

AttributeError: 'Vector3d' object has no attribute '__dict__'


for v in vectors:
    print(v.__slots__)

('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')
('x', 'y', 'z', 'test_attr')

책에서 소개한 천만개의 객체를 테스트 한 결과 1.5GB가 655MB로 줄은 결과를 소개해준다.

 

그러나 __slots__을 사용할 때 주의할 점이 생각보다 많아서 효과를 볼 수 있는지 생각해봐야한다.

- 인터프리터는 상속된 __slots__ 속성을 무시하므로 각 클래스마다 __slots__ 속성을 다시 정의해야 한다.. (여기부터 살짝 충격).

- __dict__를 __slots__에 추가하지 않는 한 객체는 __slots__에 나열된 속성만 가질 수 있다.

- weekref__를 __slots__에 추가하지 않으면 객체가 약한 참조의 대상이 될 수 없다. 

 

책에서는 프로그램이 수백만 개의 객체를 다루는 경우가 아니라면 굳이 동적 속성을 받아들이지 않고 약한 참조를 지원하지 않는 까다로운 클래스를 만들 필요가 없다고 권한다.

 

 

정리

Chapter9에서는 파이썬스러운 사용자 정의 클래스를 만들어 보았다. magic method들을 구현하면 파이썬 내장 자료형의 객체처럼 동작할 수 있어서 일관성있는 프로그래밍이 가능하다. 그 외에도 decorator chapter에서 잠시 봤던 내장형 decorator인 @classmethod, @staticmethod에 대해서 살펴봤고, 속성을 비공개하거나 읽기전용으로 만드는 법에 대해서도 알아봤다.

 

 

Reference

- Fluent Python Chapter9

- https://github.com/fluentpython/example-code

 

반응형