while (1): study();
Part4. 객체지향 상용구 본문
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를 반환
'독서' 카테고리의 다른 글
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 |