From 5963ae59441f518e026ce1745d7c75c630fc2ec2 Mon Sep 17 00:00:00 2001 From: Hongsuk Ryu <34956359+deepbig@users.noreply.github.com> Date: Sat, 27 Apr 2024 02:01:08 +0900 Subject: [PATCH] Create 6. Metaclasses and Attributes.md --- .../6. Metaclasses and Attributes.md | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 hong/effective-python/6. Metaclasses and Attributes.md diff --git a/hong/effective-python/6. Metaclasses and Attributes.md b/hong/effective-python/6. Metaclasses and Attributes.md new file mode 100644 index 0000000..d10ede1 --- /dev/null +++ b/hong/effective-python/6. Metaclasses and Attributes.md @@ -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 모듈을 모두 필할 수 있다.