while (1): study();

Part4. 객체지향 상용구 본문

독서

Part4. 객체지향 상용구

전국민실업화 2021. 12. 5. 09:03
728x90

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

변수

변수는 객체에 붙은 레이블

변수가 객체에 바인딩됐다는 표현이 타당함

 

정체성

정체성은 메모리 내의 객체 주소, is연산자는 두 객체의 정체성을 비교

* is연산자가 ==연산자보다 빠르다.

 

참조로서의 함수 매개변수

1) call-by-value: 값을 복사 (원본 변경 x)

2) call-by-reference: 참조를 복사 (원본 변경 o)

3) call-by-sharing: 객체 참조를 복사 (가변형 변경 o, 불변형 변경 x) -> 정체성 변화 x

* 가변형을 매개변수로 사용하는 경우

1) 기본값으로 사용: 모듈 로딩 시 평가, 기본값은 함수 객체의 속성 -> 불안정

class HauntedBus:
    def __init__(self, passengers=[]): # default []값이 객체 간 공유된다.
        self.passengers = passengers
            
    def pick(self, name):
        self.passengers.append(name)
        
    def drop(self, name):
        self.passengers.remove(name)

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)

 

del과 가비지 컬렉션

* __del__(): 객체가 제거되기 전 외부 리소스를 해제하도록 인터프리터가 호출

del: 이름 제거 (객체 제거x)

가비지컬렉터: (CPython 기준) 참조 카운트가 0이 되거나, 접근할 수 없을때(순환참조) 객체 제거

 

약한 참조: 참조 카운트를 증가시키지 않고 객체를 참조

weakref모듈: finalize(), WeakValueDictionary, WeakKeyDictionary, WeakSet

ex) WeakValueDictionary: 캐싱에 이용

import weakref

class Cheese:
    def __init__(self, kind):
        self.kind = kind
        
    def __retpr__(self):
        return 'Cheese(%r)' % self.kind
        
stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]

for cheese in catalog:
    stock[cheese.kind] = cheese
    
del catalog
print(sorted(stock.keys())) # for문의 임시변수 cheese에 의해 전부 해제되지는 않음
del cheese
print(sorted(stock.keys()))

* list, dict는 서브클래스 생성 시 약한 참조 가능, set과 사용자 정의형은 약한 참조 가능, int와 tuple은 불가능

 

* 파이썬 불변형은 복사 시 편의상 동일 객체에 대한 참조 반환

 


9. 파이썬스러운 객체

 

요구사항을 만족하는 한 가장 단순해야 한다

 

덕 타이핑 메커니즘

객체에 필요한 메서드만 구현하면 기대한 대로 작동

사용자 정의 자료형도 내장 자료형과 마찬가지로 동작 가능하게 함

 

@classmethod와 @staticmethod

@classmethod: 클래스에 연산을 수행하는 메서드를 정의, 대한 생성자를 구현하기 위해 주로 사용

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

@staticmethod: 특별한 첫번째 인수를 받지 않음. 클래스 내에 정의된 평범한 함수

 

이름 장식 (Name mangling)

두 개의 언더바로 시작하는 속성명 정의->언더바와 클래스명을 변수명 앞에 붙여 __dict__에 저장

    @property
    def x(self):
        return self.__x
    
    @property
    def y(self):
        return self.__y
        
 print(v1._Vector2d__x)

보호되는 것이 아니라, 비공개로 사용하고 싶은 것. 실제로 비공개 기능을 제공하는 것도 아님

-> 공개속성으로 시작하라

 

__slots__

class Vector2d:
    __slots__ = ('__x', '__y')
    typecode = 'd'

객체 속성을 딕셔너리 대신 튜플에 저장하게 만듦

아주 많은 객체를 다룰때만 사용할 가치가 있음

1) 인터프리터는 상속된 __slots__ 속성을 무시

2) __slots__에 나열된 속성만 가질 수 있음

3) __weakref__를 __slots__에 추가하지 않으면 객체가 약한 참조의 대상이 될 수 없음

 

클래스 속성 오버라이드

각 객체가 서로 다른 속성을 갖도록 커스터마이즈

-> 상속해서 서브클래스의 클래스 수준에서 덮어쓰는 방법

 


10. 시퀀스 해킹, 해시, 슬라이스

 

* 시퀀스를 제한된 길이로 표현: reprlib.repr()

* reduce(함수, 반복형, 초깃값): 초깃값은 함수에 대한 항등원을 사용

 

프로토콜

문서에만 정의되어 있고, 실제 코드에는 정의되어 있지 않은 비공식 인터페이스

단지 필요한 메서드만 제공하면 된다 -> 덕 타이핑

ex) 시퀀스 프로토콜은 __len__()과 __getitem__() 메서드를 요구

* __getitem__()에 슬라이스 객체를 처리할 방법을 정의해야 슬라이싱 가능

 def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral): # ABC로 검사하는 것이 확장성면에서 안전
            return self._components[index]
        else:
            msg = '{cls.__name__} indices must be integers'
            raise TypeError(msg.format(cls=cls))

 

동적 속성 접근

__getattr__(): 상속 그래프를 모두 올라가도 속성을 찾지 못하면 호출

__setattr__(): 속성명에 값을 할당할 때 호출

* 일관성 없는 작동을 피하려면 둘을 함께 구현해야함.

    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:
            pos = cls.shortcut_names.find(name)
            if 0 <= pos < len(self._components):
                return self._components[pos]
            msg = '{.__name__!r} object has no attribute {!r}'
            raise AttributeError(msg.format(cls, name))
            
    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:
            if name in cls.shortcut_names:
                error = 'readonly attribute {attr_name!r}'
            elif name.islower():
                error = "can't set attributes 'a' to 'z' in {cls_name!r}"
            else:
                error = ''
            if error:
                msg = error.format(cls_name=cls.__name__, attr_name=name)
                raise AttributeError(msg)
        super().__setattr__(name, value) # 에러가 발생하지 않을 시 표준 동작

 


11. 인터페이스: 프로토콜에서 ABC까지

 

ABC가 쉽게 자료형을 검사할 수 있게 해주지만, 프로그램에서 ABC를 남용해서는 안 된다. 파이썬은 상당히 많은 융통성을 부여하는 동적 언어라는 철학을 기반으로 하고 있다. 모든 곳에서 자료형을 통제하려고 하면 필요 이상으로 복잡한 코드가 나온다. 파이썬의 융통성을 받아들이길 바란다.

- 데이비드 비즐리, 브라이언 K. 존스

 

프로토콜

프로토콜: 동적 자료형을 제공하는 언어에서 다형성을 제공하는 비공식 인터페이스

인터페이스: 클래스가 상속하거나 구현한 '공개' 속성들의 집합

* 파이썬에서는 'X와 같은 객체' = 'X 프로토콜' = 'X 인터페이스' 

 

프로토콜의 동적 성질

1) 프로토콜을 일부만 구현해도 특별하게 처리된다.

2) 멍키 패칭: 런타임에 클래스나 모듈을 변경하는 행위 

 

ABC의 정적 인터페이스

ex) collections.abc, numbers

기존 ABC를 상속하거나, ABC에 등록(register) 혹은 ABC를 이용하여 isinstance()로 검사

* __subclasshook__(): 동적으로 서브클래스 탐지

 

자료형 원리

강한 검사: 자료형을 암묵적으로 변환 vs 약한 검사: 암묵적 변환 x

정적 검사: 컴파일 시 자료형 검사(자료형 선언) vs 동적 검사: 런타임에 자료형 검사

-> 파이썬은 자료형을 동적으로 강하게 검사

 


12. 내장 자료형 상속과 다중 상속

내장 자료형 상속의 위험성

CPython의 경우 내장 클래스의 코드는 사용자가 오버라이드한 코드를 호출하지 않는다

-> collections.UserDict, UserList, UserString 등 외장자료형을 사용하라

 

다중 상속

다중 상속: 하나 이상의 상위 클래스로부터 상속하는 경우

메서드 결정 순서(MRO; Method Resolution Order): 다중 상속 시 이름 충돌 문제 해결

- (상속 그래프 + 서브클래스 정의에 나열된 슈퍼클래스들의 순서) 고려

- C3 알고리즘이 계산

- __mro__ 속성으로 조회 가능

 - mro를 우회하여 메서드를 호출하는 것도 가능

class A:
    def ping(self):
        print('ping:', self)
        
class B(A):
    def pong(self):
        print('pong:', self)
        
class C(A):
    def pong(self):
        print('PONG:', self)
        
class D(B, C):
    def ping(self):
        super().ping()
        print('post-ping:', self)
        
    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self) # MRO 우회

* 다중 상속 다루기

더보기

1) 인터페이스 상속과 구현 상속의 구분

  • 인터페이스 상속 : is-a 관계를 의미하는 서브타입을 생성
  • 구현 상속: 재사용을 통해 코드 중복 회피

2) ABC를 이용하여 인터페이스를 명확히

3) 코드를 재사용하기 위해 믹스인을 사용

  • 믹스인 : 새로운 자료형을 정의하지 않고, 단시 메서드들을 묶어놓음

4) 이름을 통해 믹스인임을 명확히

5) ABC가 믹스인이 될 수 있으나, 믹스인이라고 ABC인 것은 아님

6) 두 개 이상의 구상 클래스에서 상속받지 않는다

  • 구상 클래스(Concrete class):  모든 연산에 대한 구현을 가짐
  • 추상 클래스(Abstract class): 일부 연산에 대한 구현을 생략

7) 사용자에게 집합 클래스(Aggregate class; ABC 또는 믹스인을 적절히 통합하는 클래스)를 제공

8) 상속보다는 구성을 사용

  • 상속(Inheritance): is-a 관계, 부모클래스로부터 기능 상속
  • 구성(Composition): has-a 관계, 다른 클래스의 객체를 포함

    * 위임(Delegation): 어떤 메서드의 처리를 다른 인스턴스의 메서드에 넘긴다.

 


13. 연산자 오버로딩: 제대로 하기

 

덕 타이핑: 자료형을 명시하지 않고 예외만 처리(try ~ except) -> 융통성 
구스 타이핑: isinstance()로 자료형별로 처리 -> 가독성

* 단 isinstance로 자료형 검사 시 ABC를 사용

 

연산자 오버로딩: 하나의 연산자로 여러 기능을 구현

1) 내장 자료형의 연산자는 오버로딩할 수 없다.

2) 새로운 연산자를 생성할 수 없다.

3) is, and, or, not은 오버로딩할 수 없다.

 

단항 연산자

- 피연산자를 수정할 수 없으며, 언제나 새로운 객체를 반환해야 한다.

* x와 +x가 동일하지 않은 경우

1) decimal.Decimal: 산술 콘텍스트의 정밀도가 달라지는 경우

import decimal
ctx = decimal.getcontext()
ctx.prec = 40
one_third = decimal.Decimal('1') / decimal.Decimal('3')
print('one_third : ', one_third)
print('one_third == +one_third : ', one_third == +one_third)

ctx.prec = 28
print('+one_third : ', +one_third)
print('one_third == +one_third : ', one_third == +one_third)

2) collections.Counter: 빈도 0 초과의 값만 유지

from collections import Counter
ct = Counter('abracadabra')
ct['r'] = -3
ct['d'] = 0
print('origin\n', ct)
print('pos\n', +ct)

 

중위 연산자

- 피연산자를 수정할 수 없으며, 언제나 새로운 객체를 반환해야 한다.

- __add__() 연산에서 NotImplemented 반환 시 -> __radd__ 연산에서 NotImplemented 반환 시 -> TypeError

    def __add__(self, other):
        try:
            pairs = itertools.zip_longest(self, other, fillvalue=0.0)
            return Vector(a + b for a, b in pairs)
        except:
            return NotImplemented
    
    def __radd__(self, other):
        return self + other

 

비교 연산자

- >, <, >=, <= 연산은 역순 메서드 논리가 다름

- ==, !=는 정방향 메서드와 역순 메서드가 실패하면 객체의 ID를 비교 (Error X)

 

복합할당연산자

- 언제나 self를 반환

 

728x90

'독서' 카테고리의 다른 글

Part5. 제어 흐름  (0) 2021.12.08
Part3. 객체로서의 함수  (0) 2021.12.05
Part2. 데이터 구조체  (0) 2021.12.05
Part1. 들어가며  (0) 2021.12.05
Ch8. 어텐션  (0) 2021.08.01
Comments