Skip to content

Commit

Permalink
Create 6. Metaclasses and Attributes.md
Browse files Browse the repository at this point in the history
  • Loading branch information
deepbig authored Apr 26, 2024
1 parent bdc173f commit 5963ae5
Showing 1 changed file with 313 additions and 0 deletions.
313 changes: 313 additions & 0 deletions hong/effective-python/6. Metaclasses and Attributes.md
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 모듈을 모두 필할 수 있다.

0 comments on commit 5963ae5

Please sign in to comment.