-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create 6. Metaclasses and Attributes.md
- Loading branch information
Showing
1 changed file
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
# Chapter 4. 메타클래스와 속성 | ||
|
||
- 메타클래스를 이용하면 파이썬의 class문을 가로채서 클래스가 정의될 때마다 특별한 동작을 제공할 수 있다. | ||
- 또 하나의 강력한 기능은 속성 접근을 동적으로 사용자화하는 파이썬의 내장 기능이다. | ||
- 동적 속성은 객체들을 오버라이드하다가 예상치 못한 부작용을 일으키게 할 수 있다. | ||
- 놀람 최소화 원칙을 따르자 (principle of least astonishment, principle of least surprise, **POLA)** | ||
- 사용자 인터페이스(UI)와 소프트웨어 설계에 적용되는 원칙이다. | ||
- “필요한 기능에 크나큰 깜짝 놀래킬만한 요소가 있다면 해당 기능을 다시 설계할 필요가 있을 수 있다.”는 것이 이 원칙의 일반적인 공식. | ||
- 더 일반적으로 이야기하면 이 원칙은 시스템의 구성 요소가 대부분의 사용자들이 행동할 것으로 예측되는 방식으로 동작하는 것이 좋다는 것을 의미한다. 즉, 해당 동작이 사용자들을 놀래키지 않는 것이 좋다는 것이다. | ||
|
||
## Better Way 29. getter와 setter method 대신에 일반 속성을 사용하자 | ||
|
||
보통 자바와 같은 언어에 익숙한 사람이라면, 아래와 같이 getter / setter 함수에 익숙할 것이다. | ||
|
||
```python | ||
class OldResistor: | ||
def __init__(self, ohms): | ||
self._ohms = ohms | ||
|
||
def get_ohms(self): | ||
return self._ohms | ||
|
||
def set_ohms(self, ohms): | ||
self._ohms = ohms | ||
|
||
# 이렇게 setter와 getter를 사용하는 것은 다음과 같이 사용할 수 있다. | ||
r0 = OldResistor(50e3) | ||
r0.set_ohms(10e3) | ||
``` | ||
|
||
간단하고, 클래스의 인터페이스를 정의하는데 도움이 되고, 사용법을 검증할 수 있게 하고, 경계를 정의하기 쉽게 해준다. | ||
|
||
그러나 파이썬 답지 않다. | ||
|
||
아마 아래와 같이 쓸 수 있으면, 좀 더 심플하고, 명확하고, 따라서 파이썬다워 질 것이다. | ||
|
||
(setter, getter를 쓰지 않고, 해당 속성에 직접 접근) | ||
|
||
```python | ||
r1 = Resistor(50e3) | ||
r1.ohms = 10e3 | ||
r1.ohms += 5e3 # 즉석에서 증가시키기 같은 연산이 자연스럽고 명확해진다. | ||
``` | ||
|
||
만약 setter, getter에서 사용법을 검증하는 것과 같이, 속성을 설정할 때 (혹은 읽어올 때) 특별한 동작이 일어나야 한다면, | ||
|
||
`@property` 데코레이터와 이에 대응하는 setter 속성을 사용하면 된다. | ||
|
||
```python | ||
class NewResistor: | ||
def __init__(self, ohms): | ||
self._ohms = ohms | ||
|
||
@Property | ||
def ohms(self): | ||
return self._ohms | ||
|
||
@ohms.setter | ||
def ohms(self, ohms): | ||
if ohms <= 0: | ||
raise ValueError(f'{ohms} ohms must be > 0') | ||
self._ohms = ohms | ||
``` | ||
|
||
- property에 setter를 설정하면 클래스에 전달된 값들의 타입을 체크하고 값을 검증할 수도 있다. | ||
- 아래와 같이 부모 클래스의 속성을 불변(immutable)으로 만드는데도 `@property`를 사용할 수 있다. | ||
|
||
```python | ||
class FixedResistor: | ||
def __init__(self, ohms): | ||
self._ohms = phms | ||
|
||
@property | ||
def ohms(self): | ||
return self._ohms | ||
|
||
@ohms.setter | ||
def ohms(self, ohms): | ||
if hasattr(self, ohms): | ||
raise AttributeError("Can't set attribute") | ||
self._ohms = ohms | ||
|
||
r2 = FixedResistor(1e3) | ||
r2.ohms = 2e3 # Output: AttributeError: Can't set attribute | ||
``` | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 간단한 공개 속성을 사용하여 새 클래스 인터페이스를 정의하고 getter와 setter method는 사용하지 말자 | ||
- 객체의 속성에 접근할 때 특별한 동작을 정의하려면 `@property`를 사용하자 | ||
- `@property` method에서 최소 놀람 규칙을 따르고 이상한 부작용은 피하자 | ||
- `@property` method가 빠르게 동작하도록 만들자. 느리거나 복잡한 작업은 일반 method로 하자 | ||
|
||
## Better Way 30. 속성을 리팩토링하는 대신 @property를 고려하자 | ||
|
||
TODO: 책 내용 확인 더 필요 | ||
|
||
```python | ||
class Bucket(object): | ||
def __init__(self, period): | ||
self.period_delta = timedelta(seconds=period) | ||
self.reset_time = datetime.now() | ||
self.max_quota = 0 | ||
self.quota_consumed = 0 | ||
|
||
def __repr__(self): | ||
return ('Bucket(max_quota=%d, quota_consumed=%d)' % | ||
(self.max_quota, self.quota_consumed)) | ||
|
||
@property | ||
def quota(self): | ||
returnself.max_quota - self.quota_consumed | ||
|
||
@quota.setter | ||
def quota(self, amount): | ||
delta = self.max_quota - amount | ||
if amount == 0: | ||
# 새 가간의 할달량을 리셋함 | ||
self.quota_consumed = 0 | ||
self.max_quota = 0 | ||
elif delta < 0: | ||
# 새 기간의 할당량을 채움 | ||
assert self.quota_consumed == 0 | ||
self.max_quota = amount | ||
else: | ||
# 기간 동안 할당량을 소비함 | ||
assert self.max_quota >= self.quota_consumed | ||
self.quota_consumed += delta | ||
|
||
def fill(bucket, amount): | ||
now = datatime.now() | ||
if now - bucket.reset_time > bucket.period_delta: | ||
bucket.quota = 0 | ||
bucket.reset_time = now | ||
bucket.quota += amount | ||
|
||
def deduct(bucket, amount): | ||
now = datatime.now() | ||
if now - bucket.reset_time > bucket.period_delta: | ||
return False | ||
if bucket.quota - amount < 0: | ||
return False | ||
bucket.quota - amount | ||
return True | ||
``` | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 기존의 인스턴스 속성에 새 기능을 부여하려면 `@property`를 사용하자 | ||
- `@property`를 사용하여 점점 나은 데이터 모델로 발전시키자 | ||
- `@property`를 너무 많이 사용한다면 클래스와 이를 호출하는 모든 곳을 리팩토링하는 방안을 고려하자 | ||
|
||
## Better Way 31. 재사용 가능한 @property 메서드에는 디스크립터를 사용하자 | ||
|
||
“Better Way 29. Getter와 Setter method 대신에 일반 속성을 사용하자”에서 소개된 @property의 가장 큰 문제점은 재사용성이다. | ||
|
||
다시 말해, @property로 테코레이트하는 메서드를 같은 클래스에 속한 여러 속성에 사용하지 못한다. | ||
|
||
또한, 관련 없는 클래스에서도 재사용할 수 없다. | ||
|
||
```python | ||
class Exam: | ||
def __init__(self): | ||
self._writing_grade = 0 | ||
self._math_grade = 0 | ||
|
||
@staticmethod | ||
def _check_grade(value): | ||
if not (0 <= value <= 100): | ||
raise ValueError('Grade must be between 0 and 100') | ||
``` | ||
|
||
_check_grade 메소드를 이용해 각 시험 점수에 입력되는 내용을 검증하려고 한다면, 코드 중복 코드로 인해 아래와 같이 금방 장황해진다. | ||
|
||
```python | ||
@property | ||
def writing_grade(self): | ||
retur self._writing_grade | ||
|
||
@writing_grade.setter | ||
def writing_grade(self, value): | ||
self._check_grade(value) | ||
self._writing_grade = value | ||
|
||
@property | ||
def math_grade(self): | ||
return self._math_grade | ||
|
||
@math_grade.setter | ||
def math_grade(self, value): | ||
self._check_grade(value) | ||
self._math_grade = value | ||
``` | ||
|
||
이렇게 중복되는 코드를 없애주기 위해, 디스크립터(descriptor)를 사용할 수 있다. | ||
|
||
```python | ||
class Grade: | ||
def __get__(*args, **kwargs): | ||
# ... | ||
|
||
def __set__(*args, **kwargs): | ||
# ... | ||
|
||
class Exam: | ||
math_grade = Grade() | ||
writing_grade = Grade() | ||
``` | ||
|
||
Grade 라는 클래스를 만들고, Exam의 각 속성을 Grade 클래스에 의해 값이 생성되고 관리되게 할 수 있다. | ||
|
||
이 때, 속성으로 이용되는 클래스(Grade)에 __get__, __set__ 매직 메소드를 재정의하면, 해당 클래스로 선언된 속성 값에 접근할 때 원하는 공통적인 동작을 적용할 수 있다. | ||
|
||
```python | ||
class Grade: | ||
def __init__(self): | ||
self._value = 0 | ||
|
||
def __get__(self, instance, instance_type): | ||
return self._value | ||
|
||
def __set__(self, instance, value): | ||
if not (0 <= value <= 100): | ||
raise ValueError('Grade must be between 0 and 100') | ||
self._value = value | ||
``` | ||
|
||
그런데, 이렇게만 하면 한가지 문제가 발생한다. | ||
|
||
한 Grade 인스턴스가 모든 Exam 인스턴스의 writing_grade 클래스 속성으로 공유된다는 점이다. | ||
|
||
```python | ||
first_exam = Exam() | ||
first_exam.writing_grade = 80 | ||
|
||
second_exam = Exam() | ||
second_exam.writing_grade = 75 | ||
|
||
print(f'Second {second_exam.writing_grade} is right') # Output: Second 75 is right | ||
print(f'First {first_exam.writing_grade} is wrong') # Output: First 75 is wrong | ||
``` | ||
|
||
이 문제를 해결하기 위해서는, Grade 클래스 안에서 Exam 인스턴스 별로 값을 추적하도록 해야한다. | ||
|
||
```python | ||
class Grade: | ||
def __init__(self): | ||
self._value = {} | ||
|
||
def __get__(self, instance, instance_type): | ||
if instance is None: | ||
return self | ||
return self._values.get(instance, 0) | ||
|
||
def __set__(self, instance, value): | ||
if not (0 <= value <= 100): | ||
raise ValueError('Grade must be between 0 and 100') | ||
self._values[instance] = value | ||
``` | ||
|
||
이 방법은 간단하면서도 잘 동작하지만, “메모리 누수”라는 문제점이 남아있다. | ||
|
||
_values 딕셔너리는 프로그램의 수명 동안 __set__에 전달된 모든 Exam 인스턴스의 참조를 저장하고 있고, 때문에 인스턴스의 참조 개수가 절대로 0이 되지 않아 Garbage Collector가 정리하지 못하게 된다. | ||
|
||
이럴땐, 파이썬의 내장 모듈 weakref를 사용하면 된다. | ||
|
||
이 모듈은 _values에 사용한 간단한 딕셔너리를 대체할 수 있는 WeakKeyDictionary 라는 특별한 클래스를 제공한다. | ||
|
||
WeakKeyDictionary 클래스 고유의 동작은 런타임에 마지막으로 남은 Exam 인스턴스의 참조를 갖고 있다는 사실을 알면 키 집합에서 Exam 인스턴스를 제거하는 것이다. | ||
|
||
```python | ||
class Grade: | ||
def __init__(self): | ||
self._values = WeakKeyDictionary() | ||
``` | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 직접 디스크립터 클래스를 정의하여 `@property` 메서드의 동작과 검증을 재사용하자 | ||
- WeakKeyDictionary를 사용하여 디스크립터 클래스가 메모리 누수를 일으키지 않게 하자 | ||
- getattribute가 디스크립터 프로토콜을 사용하여 속성을 얻어오고 설정하는 원리를 정확히 이해하려는 함정에 빠지지 말자 | ||
|
||
## Better Way 32. 지연 속성에는 getattr, getattribute, setattr을 사용하자 | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 객체의 속성을 지연 방식으로 로드하고 저장하려면 getattr과 setattr을 사용하자 | ||
- getattr은 존재하지 않는 속성에 접근할 때 한 번만 호출되는 반면에 getattribute는 속성에 접근할 때마다 호출된다는 점을 이해하자 | ||
- getattribute와 setattr에서 인스턴스 속성에 직접 접근할 때 super()(즉, object 클래스)의 메서드를 사용하여 무한 재귀가 일어나지 않게 하자 | ||
|
||
## Better Way 33. 메타클래스로 서브클래스를 검증하자 | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 서브클래스 타입의 객체를 생성하기에 앞서 서브클래스가 정의 시점부터 제대로 구성되었음을 보장하려면 메타클래스를 사용하자 | ||
- 파이썬 2와 파이썬 3의 메타클래스 문법은 약간 다르다. | ||
- 메타클래스의 new 메서드는 class 문의 본문 전체가 처리된 후에 실행된다. | ||
|
||
## Better Way 34. 메타클래스로 클래스의 존재를 등록하자 | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 클래스의 등록은 모듈 방식의 파이썬 프로그램을 만들 때 유용한 패턴이다. | ||
- 메타클래스를 이용하면 프로그램에서 기반 클래스로 서브클래스를 만들 때마다 자동으로 등록 코드를 실행할 수 있다. | ||
- 메타클래스를 이용해 클래스를 등록하면 등록 호출을 절대 빠뜨리지 않으므로 오류를 방지할 수 있다. | ||
|
||
## Better Way 35. 메타클래스로 클래스 속성에 주석을 달자 | ||
|
||
### 기억해야 할 내용 | ||
|
||
- 메타클래스를 이용하면 클래스가 완전히 정의되기 전에 클래스 속성을 수정할 수 있다. | ||
- 디스크립터와 메타클래스는 선언적 동작과 런타임 내부 조사(introspection)용으로 강력한 조합을 이룬다. | ||
- 메타클래스와 디스크립터를 연계하여 사용하면 메모리 누수와 weakref 모듈을 모두 필할 수 있다. |