Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12주차 1 미완] Effective Python: Chapter 6. Metaclasses and Attributes #147

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 모듈을 모두 필할 수 있다.
Loading