meta class & attributes (1)

About_work·2023년 1월 18일
0

python 기초

목록 보기
13/65

meta class 란

  • meta class
    • 파이썬의 특성으로 자주 언급됨
    • 실제로 어떤 목적으로 쓰이는지 이해하는 프로그래머는 거의 없다.
    • meta class라는 이름은 어렴풋이 이 개념이 클래스를 넘어서는 것임을 암시한다.
    • 메타클래스를 사용하면, 파이썬의 class 문을 가로채서, 클래스가 정의될 때마다 특별한 동작을 제공할 수 있다
  • 동적 attribute 접근
    • metaclass처럼 신비하고 강력한 파이썬 기능으로는, 동적으로 attribute 접근을 custom화 해주는 내장 기능을 들 수 있다.
  • 하지만 이런 2가지 강력함에는 많은 함정이 뒤따른다.(이해하기 어려움 + 부작용)
  • 최소 놀람의 법칙을 따르고, 잘 정해진 관용어로만 이런 기능을 사용하는 것이 중요하다.

44: 세터와 게터 메서드 대신, 평범한 attribute를 사용하라.

간단 요약

  • 새로운 class interface를 정의할 때는 간단한 공개 attribute에서 시작하고, 가급적 세터나 게터 메서드를 사용하지 말라.
  • 객체에 있는 attribute에 접근할 때, 특별한 동작이 필요하면 @property로 이를 구현할 수 있다.
  • @property 메서드를 만들 때는, 최소 놀람의 법칙을 따르고, 이상한 부작용을 만들어내지 말라.
  • @property 메서드가 빠르게 실행되도록 유지하라.
    • 느리거나 복잡한 작업의 경우(특히 i/O를 수행하는 등의 부수 효과가 있는 경우)에는 @property 대신 일반적인 메서드를 사용하라.

본문

  • 파이썬에서는 명시적인 세터나 게터 메서드를 구현할 필요가 전혀 없다. 항상 단순한 공개 attribute로 구현을 시작하라.
  • attribute가 설정될 때, 특별한 기능을 수행하고 싶다면 -> attribute를 @property 데코레이터와 대응하는 setter attribute로 옮겨갈 수 있다.
  • 게터나 세터를 정의할 때, 가장 좋은 정책은 관련이 있는 객체 상태를 @property.setter 메서드 안에서만 변경하는 것이다.
  • 동적으로 모듈을 import 하거나, 시간이 오래 걸리는 도우미 함수를 호출하거나, I/O를 수행하거나, 비용이 많이 드는 database 질의를 수행한다면, 다른 일반적인 메서드를 만들어라.
class VoltageResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)
        self._voltage = 0

    @property
    def voltage(self):
        return self._voltage

    @voltage.setter
    def voltage(self, voltage):
        self._voltage = voltage
        self.current = self._voltage / self.ohms

r2 = VoltageResistance(1e3)
print(f'이전: {r2.current:.2f} 암페어')
r2.voltage = 10
print(f'이후: {r2.current:.2f} 암페어')

구글 스타일 가이드

public attribute 언제 써야해?

  • 단순히 attribute 값을 get set 할꺼면, public attribute를 써라.

@property 장점

  • 클래스의 메소드를, 마치 속성처럼 간단하게 접근하고 수정할 수 있습니다.
    • 예를 들어, 객체의 특정 값을 가져오거나 설정하는 getter와 setter 메소드를 정의하지 않고도, 속성처럼 값을 읽고 쓸 수 있습니다.
  • property를 사용하여 특정 속성을 읽기 전용으로 설정할 수 있습니다. 즉, 사용자가 해당 속성의 값을 변경할 수 없도록 할 때 유용합니다.
  • 클래스의 내부 구현이 변경되더라도, property를 사용함으로써 클래스를 사용하는 외부 코드에는 영향을 주지 않고 인터페이스를 유지할 수 있습니다.

@property 단점

  • property를 통해 데이터를 설정할 때, 내부적으로 다른 작업이 수행될 수 있는데, 이러한 부작용이 코드를 읽는 사람에게 명확하지 않을 수 있습니다.
  • 상위 클래스에서 property로 정의된 속성을 하위 클래스에서 오버라이드하려 할 때 혼란이 발생할 수 있습니다.
    • 서브클래스에서는 이 속성이 메서드인지, 아니면 실제 속성인지 구분하기 어려울 수 있습니다.

@property 언제 써야해?

  • 사용 적합성: 속성 접근이 간단하고 직관적일 필요가 있으며, 비용이 많이 들지 않는 작업에 대해 사용하는 것이 좋습니다.
  • 계산 오버라이딩 금지: 서브클래스에서 오버라이드하고 확장할 수 있는 계산에 대해서는 property를 사용하지 않는 것이 좋습니다. 이는 서브클래스가 상위 클래스의 로직을 자유롭게 확장할 수 있도록 하기 위함입니다.

getter & setter 언제 써야해?

  • (현재 혹은 미래 관점에서)변수를 얻는 것이 복잡하거나, cost가 상당할 때 써야한다.
  • 단순히 내부 attribute를 읽고 쓰는 경우, public attribute로 사용해야 한다.
  • 이에 비해, 변수를 setting 하는 것이, "어떤 상태가 무효화되거나 재구축된다는 것을 의미한다면" setter 을 써야한다.
  • 대안으로, @properies는 단순한 로직이 필요하거나, 더 이상 getter와 setter가 필요하지 않아 리펙토링의 대상이 될 때 사용할 수 있는 option이다.

45: attribute를 refactoring 하는 대신 @property를 사용하라.

요약

  • @property를 사용해, 기존 instance attribute에 새로운 기능을 제공할 수 있다.
  • @property를 사용해, 데이터 모델을 점진적으로 개선하라.
  • @property 메서드를 너무 과하게 쓰고 있다면, 클래스와 클래스를 사용하는 모든 코드를 refactoring하는 것을 고려하라.
class BoundedResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)

    @property
    def ohms(self):
        return self._ohms

    @ohms.setter
    def ohms(self, ohms):
        if ohms <= 0:
            raise ValueError(f'저항 > 0이어야 합니다. 실제 값: {ohms}')
        self._ohms = ohms
class FixedResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)

    @property
    def ohms(self):
        return self._ohms

    @ohms.setter
    def ohms(self, ohms):
        if hasattr(self, '_ohms'):
            raise AttributeError("Ohms는 불변객체입니다")
        self._ohms = ohms

46: 재사용 가능한 @property 메서드를 만드려면 descriptor을 사용하라.

요약

  • @property 메서드의 동작과 검증 기능을 재사용하고 싶다면, descriptor 클래스를 만들라.
  • descriptor 클래스를 만들 때는, 메모리 누수를 방지하기 위해, WeakKeyDictionary 를 사용하라.
  • __getattribute__가 descriptor protocol을 사용해, attribute 값을 읽거나 설정하는 방식을 정확히 이해하라.

본문

  • 아래와 같은 코드는 반복적이기 때문에 비효율적이다.
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(
                '점수는 0과 100 사이입니다')

    @property
    def writing_grade(self):
        return 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을 사용하라.
    • descriptor protocol은 파이썬 언어에서, attribute 접근을 해석하는 방법을 정의한다.
    • descriptor class는 __get____set___ 메서드를 제공하고, 이 두 메서드를 사용하면, 별다른 준비 코드 없이도 _checking_grade 와 같은 동작을 재사용할 수 있다.
    • 이런 경우 같은 logic을 한 클래스 안에 속한 여러 다른 attribute에 적용할 수 있으므로, descriptor가 mix-in 보다 낫다.
  • 다음 코드는 Grade의 instance인 class attribute가 들어있는 Exam 클래스를 정의한다.
  • Grade 클래스는 다음과 같은 descriptor protocol을 구현한다.
class Grade: # descriptor protocol을 구현함
    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(
                '점수는 0과 100 사이입니다')
        self._value = value

class Exam:
    # 클래스 애트리뷰트
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

first_exam = Exam()
first_exam.science_grade = 99
first_exam.writing_grade = 82
>>> [중요, 다음과 같이 작동함] Exam.__dict__['writing_grade'].__set__(exam, 40)

first_exam.writing_grade
>>> [중요, 다음과 같이 작동함] Exam.__dict__['writing_grade'].__get__(exam, Exam)
  • 아래 4줄을 보면, Exam 인스턴스에 writing_grade 라는 이름의 attribute가 없으면, python은 Exam 클래스의 attribute를 대신 사용한다.

  • 이 클래스 attribute가 __get____set__ 메서드가 정의된 객체라면, 파이썬은 descriptor protocol을 따라야 한다고 결정한다.

  • 하지만, 위 코드의 문제점은 Exam 클래스가 처음 정의될 때 1번만 이 attribute에 대한 Grade 인스턴스가 단 한번만 생성된다는 점이다. Exam 인스턴스가 생성될 때마다, 매번 Grade 인스턴스가 생성되지는 않는다.

  • 그래서 아래와 같은 2번째 Exam 인스턴스를 만들면, 문제가 발생한다.

second_exam = Exam()
second_exam.writing_grade = 75
print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
print(f'첫 번째 쓰기 점수 {first_exam.writing_grade} 틀림; '
      f'82점이어야 함')
  • 이를 해결하기 위해 아래와 같이 코딩하면 된다. (인스턴스별 상태를 dictionary에 저장하면 이런 구현이 가능하다.)
class Grade:
    def __init__(self):
        self._values = {}

    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(
                '점수는 0과 100 사이입니다')
        self._values[instance] = value
  • 하지만, 위 구현의 문제점은 메모리를 leak시킨다는 점이다. _values 딕셔너리는 프로그램이 실행되는 동안 __set__ 호출에 전달된 모든 Exam 인스턴스에 대한 참조를 저장하고 있다.
  • 이로 인해 instance에 대한 참조 카운터가 절대로 0이 될 수 없고, 따라서 garbage collector가 instance memory를 결코 재활용하지 못한다.
  • 이 문제를 해결하기 위해, 파이썬 weakref내장 모듈의 WeakKeyDictionary라는 특별한 클래스를 사용하자.
    • 딕셔너리에 객체를 저장할 때, 일반적인 strong reference 대신에 weak reference를 사용한다.
    • 파이썬 garbage collector는 weak reference로만 참조되는 객체가 사용 중인 메모리를 언제든지 재활용할 수 있다.
from weakref import WeakKeyDictionary
class Grade:
    def __init__(self):
        self._values = WeakKeyDictionary()

    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(
                '점수는 0과 100 사이입니다')
        self._values[instance] = value

47: 지연 계산 attribute가 필요하면 __getattr__, __getattribute__, __setattr__을 사용하라.

요약

  • __getattr____setattr__을 사용해, 객체의 attribute를 지연해 가져오거나 저장할 수 있다.
  • __getattr__은 attribute가 존재하지 않을 때만 호출되지만, __getattribute__는 attribute를 읽을 때마다 항상 호출된다는 점을 이해하라.
  • _getattribute____setattr__에서 무한 재귀를 피하려면, super()에 있는(즉, object 클래스에 있는) 메서드를 사용해 instance attribute에 접근하라.

내용

  • 파이썬 object hook을 사용하면, 시스템을 서로 접합하는 generic 코드를 쉽게 작성할 수 있다.
    • object hook의 예: __getattr__ / __getattribute__
  • 어떤 class 안에 __getattr__ 메서드 정의가 있으면, 이 객체의 Instance dictionary에서 찾을 수 없는 attribute에 접근할 때마다 __getattr__이 호출된다.
class LazyRecord:
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        value = f'{name}의 값'
        setattr(self, name, value)
        return value

data = LazyRecord()
print('이전:', data.__dict__)
print('foo:', data.foo)
print('이후:', data.__dict__)

>>>
이전: {'exists': 5}
foo: foo를 위한 값
이후: {'exists": 5, 'foo': 'foo를 위한 값'}
  • __getattribute__는 attribute를 읽을 때마다 항상 호출된다.
  • 이를 이용하면, 프로퍼티에 접근할 때마다 항상 로그를 남기는 등의 작업을 수행할 수 있다.
  • 이런 연산은 부가 비용이 많이 들고, 성능에 부정적인 영향을 끼치기도 하지만, 때로는 이런 비용을 감수할 만한 가치를 지닌 경우도 있다는 점을 명심하자.
class ValidatingRecord:
    def __init__(self):
        self.exists = 5
    def __getattribute__(self, name):
        print(f'* 호출: __getattr__({name!r})')
        try:
            value = super().__getattribute__(name)
            print(f'* {name!r} 찾음, {value!r} 반환')
            return value
        except AttributeError:
            value = f'{name}을 위한 값'
            print(f'* {name!r}{value!r}로 설정')
            setattr(self, name, value)
            return value

data = ValidatingRecord()
print('exists: ', data.exists)
print('첫 번째 foo: ', data.foo)
print('두 번째 foo: ', data.foo)
  • 존재하지 않는 프로퍼티에 동적으로 접근하는 경우에는 AttributeError 예외가 발생한다.

  • (__getattr____getattribute__에서 존재하지 않는 property를 사용할 때 발생하는 표준적인 예외가 AttributeError)

  • __setattr__은 인스턴스의 attribute에(직접 대입하든 setattr 내장 함수를 통해서든) 대입이 이루어질 때마다 항상 호출된다.

    • __getattr__ 이나 __getattribute__로 값을 읽을 떄와는 달리, 메서드가 2개 있을 필요가 없다.
class SavingRecord:
    def __setattr__(self, name, value):
        # 데이터를 데이터베이스 레코드에 저장한다
        super().__setattr__(name, value)

class LoggingSavingRecord(SavingRecord):
    def __setattr__(self, name, value):
        print(f'* 호출: __setattr__({name!r}, {value!r})')
        super().__setattr__(name, value)

data = LoggingSavingRecord()
print('이전: ', data.__dict__)
>>> 이전: {}

data.foo = 5
print('이후: ', data.__dict__)
>>> * 호출: __setattr__('foo', 5)
>>> 이후: {'foo': 5}

data.foo = 7
print('최후:', data.__dict__)
>>> * 호출: __setattr__('foo', 7)
>>> 이후: {'foo': 7}
  • 중요!!!!! (__getattribute__ / __setattr__에서 무한 재귀를 피하려면, super()에 있는 메서드를 사용해 instance attribute에 접근하라)
  • 아래와 같은 코드는 문제다.
class BrokenDictionaryRecord:
    def __init__(self, data):
        self._data = {}
    def __getattribute__(self, name):
        print(f'* 호출: __getattribute__({name!r})')
        return self._data[name]

data = Brokedata = BrokenDictionaryRecord({'foo': 3})
data.foo
>>>
* 호출: __getattribute__('foo')
* 호출: __getattribute__('_data')
* 호출: __getattribute__('_data')
* 호출: __getattribute__('_data')
  • 이를 아래와 같이 super을 써서 해결하라.
class DictionaryRecord:
    def __init__(self, data):
        self._data = data

    def __getattribute__(self, name):
        print(f'* 호출: __getattribute__({name!r})')
        data_dict = super().__getattribute__('_data')
        return data_dict[name]

data = DictionaryRecord({'foo': 3})
print('foo: ', data.foo)
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글