Chapter 6. 메타클래스와 애트리뷰트를 읽으면서 정리합니다.

새로 알게 되었거나 중요하다고 생각되는 부분 위주로 정리하고자 합니다.

Effective Python 2nd

Chapter 6. 메타클래스와 애트리뷰트

44. 세터와 게터 메서드 대신 평범한 애트리뷰트를 사용하라

  • 새로운 클래스 인터페이스를 정의할 때는 간단한 공개 애트리뷰트에서 시작하고, 세터나 게터 메서드를 가급적 사용하지 말라

    class OldResistor:
        def __init__(self, ohms):
            self._ohms = ohms
      
        def get_ohms(self):
            return self._ohms
      
        def set_ohms(self, ohms):
            self._ohms = ohms
    
  • @property로 이를 구현하자

    class Resistor:
        def __init__(self, ohms):
            self.ohms = ohms
            self.voltage = 0
            self.current = 0
              
    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
    
  • @propert 메서드가 빠르게 실행되도록 유지하라.

    느리거나 복잡한 작업의 경우 프로퍼티 대신 일반 메서드를 사용하라.

45. 애트리뷰트를 리팩터링하는 대신 @property를 사용하라

  • @property를 사용해 데이터 모델을 점진적으로 개선하라
  • @property 매서드를 너무 과하게 쓰고 있다면, 클래스와 클래스를 사용하는 모든 코드를 리팩터링하는 것을 고려하라

46. 재사용 가능한 @property 메서드를 만들려면 디스크립터를 사용하라

  • @property 메서드의 동작과 검증 기능을 재사용하고 싶다면 디스크립터 클래스를 만들라.

  • 디스크립터 클래스를 만들 때는 메모리 누수를 방지하기 위해 WeakKeyDictionary를 사용하라

    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
      
    class Exam:
        # 클래스 애트리뷰트
        math_grade = Grade()
        writing_grade = Grade()
        science_grade = Grade()
      
    first_exam = Exam()
    first_exam.writing_grade = 82
    second_exam = Exam()
    second_exam.writing_grade = 75
    print(f'첫 번째 쓰기 점수 {first_exam.writing_grade} 맞음')
    print(f'두 번째 쓰기 점수 {second_exam.writing_grade} 맞음')
    

47. 지연 계산 애트리뷰트가 필요하면 __getatrr__, __getattribute__, __setattr__ 을 사용하라

  • 객체의 애트리뷰트를 지연해 가져오거나 저장할 수 있다.

  • __getatrr__ 은 애트리뷰트가 존재하지 않을 때 호출되지만__getattribute__ 는 애트리뷰트를 읽을 때마다 항상 호출된다.

  • 무한 재귀를 피하려면 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)
    

48. __init_subclass__를 사용해 하위 클래스를 검증하라

  • 메타클래스의 new 메서드는 class 문의 모든 본문이 처리된 직후에 호출된다.

    class Meta(type):
        def __new__(meta, name, bases, class_dict):
            print(f'* 실행: {name}의 메타 {meta}.__new__')
            print('기반클래스들:', bases)
            print(class_dict)
            return type.__new__(meta, name, bases, class_dict)
      
    class MyClass(metaclass=Meta):
        stuff = 123
      
        def foo(self):
            pass
      
    class MySubclass(MyClass):
        other = 567
      
        def bar(self):
            pass
        
    # * 실행: MyClass의 메타 <class '__main__.Meta'>.__new__
    # 기반클래스들: ()
    # {'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 123, 'foo': <function # MyClass.foo at 0x1060a9040>}
    # * 실행: MySubclass의 메타 <class '__main__.Meta'>.__new__
    # 기반클래스들: (<class '__main__.MyClass'>,)
    # {'__module__': '__main__', '__qualname__': 'MySubclass', 'other': 567, 'bar': <function MySubclass.bar at 0x1060a90d0>}
    
  • 메타클래스를 사용해 클래스가 정의된 직후이면서 클래스가 생성되기 직전에 클래스 정의를 변경할 수 있다.

    하지만 원하는 목적을 달성하기에 너무 복잡하다.

    class ValidatePolygon(type):
        def __new__(meta, name, bases, class_dict):
            # Polygon 클래스의 하위 클래스만 검증한다
            if bases:
                if class_dict['sides'] < 3:
                    raise ValueError('다각형 변은 3개 이상이어야 함')
            return type.__new__(meta, name, bases, class_dict)
      
    class Polygon(metaclass=ValidatePolygon):
        sides = None # 하위 클래스는 이 애트리뷰트에 값을 지정해야 한다
        @classmethod
        def interior_angles(cls):
            return (cls.sides - 2) * 180
      
    class Triangle(Polygon):
        sides = 3
      
    class Rectangle(Polygon):
        sides = 4
      
    class Nonagon(Polygon):
        sides = 9
      
    assert Triangle.interior_angles() == 180
    assert Rectangle.interior_angles() == 360
    assert Nonagon.interior_angles() == 1260
      
    class Line(Polygon):
       print('sides 이전')
       sides = 2
       print('sides 이후')
      
    #sides 이전
    #sides 이후
    #    raise ValueError('다각형 변은 3개 이상이어야 함')
    #ValueError: 다각형 변은 3개 이상이어야 함
    
  • __init_subclass__ 를 사용해 하위 클래스가 정의된 직후, 하위 클래스 타입이 만들어지기 직전에 요건을 확인하라

    class ValidatePolygon(type):
        def __new__(meta, name, bases, class_dict):
            # 루트 클래스가 아닌 경우만 검증한다
            if not class_dict.get('is_root'):
                if class_dict['sides'] < 3:
                    raise ValueError('다각형 변은 3개 이상이어야 함')
            return type.__new__(meta, name, bases, class_dict)
      
    class Polygon(metaclass=ValidatePolygon):
        is_root = True
        sides = None  # 하위 클래스에서 이 애트리뷰트 값을 지정해야 한다
      
    class ValidateFilledPolygon(ValidatePolygon):
        def __new__(meta, name, bases, class_dict):
            # 루트 클래스가 아닌 경우만 검증한다
            if not class_dict.get('is_root'):
                if class_dict['color'] not in ('red', 'green'):
                    raise ValueError('지원하지 않는 color 값')
            return super().__new__(meta, name, bases, class_dict)
      
    class FilledPolygon(Polygon, metaclass=ValidateFilledPolygon):
        is_root = True
        color = None  # 하위 클래스에서 이 애트리뷰트 값을 지정해야 한다
      
    class GreenPentagon(FilledPolygon):
        color = 'green'
        sides = 5
      
    greenie = GreenPentagon()
    assert isinstance(greenie, Polygon)
    
  • super().__init_subclass__ 를 호출해 여러 계층에 걸쳐 클래스를 검증하고 다중 상속을 처리하라

    class Top:
        def __init_subclass__(cls):
            super().__init_subclass__()
            print(f'{cls}의 Top')
      
    class Left(Top):
        def __init_subclass__(cls):
            super().__init_subclass__()
            print(f'{cls}의 Left')
      
      
    class Right(Top):
        def __init_subclass__(cls):
            super().__init_subclass__()
            print(f'{cls}의 Right')
      
      
    class Bottom(Left, Right):
        def __init_subclass__(cls):
            super().__init_subclass__()
            print(f'{cls}의 Bottom')
    

49. __init_subclass__ 를 사용해 클래스 확장을 등록하라

  • 클래스 등록은 파이썬 프로그램을 모듈화할 때 유용한 패턴이다.

    import json
    class Serializable:
        def __init__(self, *args):
            self.args = args
      
        def serialize(self):
            return json.dumps({'args': self.args})
      
    class Point2D(Serializable):
        def __init__(self, x, y):
            super().__init__(x, y)
            self.x = x
            self.y = y
      
        def __repr__(self):
            return f'Point2D({self.x}, {self.y})'
      
    point = Point2D(5, 3)
    print('객체:', point)
    print('직렬화한 값:', point.serialize())
      
    #객체: Point2D(5, 3)
    #직렬화한 값: {"args": [5, 3]}
    
  • 메타클래스를 사용하면, 프로그램 안에서 기반 클래스를 상속한 하위 클래스가 정의될 때마다 등록 코드를 자동으로 실행할 수 있다.

    class BetterSerializable:
        def __init__(self, *args):
            self.args = args
      
        def serialize(self):
            return json.dumps({
                'class': self.__class__.__name__,
                'args': self.args,
            })
      
        def __repr__(self):
            name = self.__class__.__name__
            args_str = ', '.join(str(x) for x in self.args)
            return f'{name}({args_str})'
      registry = {}
      
    def register_class(target_class):
        registry[target_class.__name__] = target_class
      
    def deserialize(data):
        params = json.loads(data)
        name = params['class']
        target_class = registry[name]
        return target_class(*params['args'])
    
    class Meta(type):
        def __new__(meta, name, bases, class_dict):
            cls = type.__new__(meta, name, bases, class_dict)
            register_class(cls)
            return cls
      
    class RegisteredSerializable(BetterSerializable,
                                 metaclass=Meta):
        pass
      
    class Vector3D(RegisteredSerializable):
        def __init__(self, x, y, z):
            super().__init__(x, y, z)
            self.x, self.y, self.z = x, y, z
    
  • 표준적인 메타클래스 방식보다는 __init_subclass 가 더 낫다.

    class BetterRegisteredSerializable(BetterSerializable):
        def __init_subclass__(cls):
            super().__init_subclass__()
            register_class(cls)
      
      
    class Vector1D(BetterRegisteredSerializable):
        def __init__(self, magnitude):
            super().__init__(magnitude)
            self.magnitude = magnitude
    

50. __set_name__ 으로 클래스 애트리뷰트를 표시하라

  • 메타클래스를 사용하면 어떤 클래스가 완전히 정의되기 전에 클래스의 애트리뷰트를 변경할 수 있다.

    class Meta(type):
        def __new__(meta, name, bases, class_dict):
            for key, value in class_dict.items():
                if isinstance(value, Field):
                    value.name = key
                    value.internal_name = '_' + key
            cls = type.__new__(meta, name, bases, class_dict)
            return cls
      
    class DatabaseRow(metaclass=Meta):
        pass
      
    class Field:
        def __init__(self):
            # 이 두 정보를 메타클래스가 채워 준다
            self.name = None
            self.internal_name = None
      
        def __get__(self, instance, instance_type):
            if instance is None:
                return self
            return getattr(instance, self.internal_name, '')
      
        def __set__(self, instance, value):
            setattr(instance, self.internal_name, value)
      
    class BetterCustomer(DatabaseRow):
        first_name = Field()
        last_name = Field()
        prefix = Field()
        suffix = Field()
      
    cust = BetterCustomer()
    print(f'이전: {cust.first_name!r} {cust.__dict__}')
    cust.first_name = '오일러'
    print(f'이후: {cust.first_name!r} {cust.__dict__}')
      
    #이전: '' {}
    #이후: '오일러' {'_first_name': '오일러'}
    
  • __set_name__ 틀별 메서드를 디스크립터 클래스에 정의하면 디스크립터가 포함된 클래스 프로퍼티 이름을 처리할 수 있다.

    class Field:
        def __init__(self):
            self.name = None
            self.internal_name = None
      
        def __set_name__(self, owner, name):
            # 클래스가 생성될 때 모든 스크립터에 대해 이 메서드가 호출된다
            self.name = name
            self.internal_name = '_' + name
      
        def __get__(self, instance, instance_type):
            if instance is None:
                return self
            return getattr(instance, self.internal_name, '')
      
        def __set__(self, instance, value):
            setattr(instance, self.internal_name, value)
      
    class FixedCustomer:
        first_name = Field()
        last_name = Field()
        prefix = Field()
        suffix = Field()
      
    cust = FixedCustomer()
    print(f'이전: {cust.first_name!r} {cust.__dict__}')
    cust.first_name = '메르센'
    print(f'이후: {cust.first_name!r} {cust.__dict__}')
      
    #이전: '' {}
    #이후: '메르센' {'_first_name': '메르센'}
    

51. 합성 가능한 클래스 확장이 필요하면 메타클래스보다는 클래스 데코레이터를 사용하라

  • 클래스 데코레이터는 class 인스턴스를 파라미터로 받아서 변경한 클래스나 새로운 클래스를 반환해주는 간단한 함수이다.

    def my_class_decorator(klass):
        klass.extra_param = '안녕'
        return klass
      
    @my_class_decorator
    class MyClass:
        pass
      
    print(MyClass)
    print(MyClass.extra_param)
      
    #<class '__main__.MyClass'>
    #안녕
    
    def trace_func(func):
        if hasattr(func, 'tracing'):  # 단 한번만 데코레이터를 적용한다
            return func
      
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            try:
                result = func(*args, **kwargs)
                return result
            except Exception as e:
                result = e
                raise
            finally:
                print(f'{func.__name__}({args!r}, {kwargs!r}) -> '
                      f'{result!r}')
      
        wrapper.tracing = True
        return wrapper
      
    def trace(klass):
        for key in dir(klass):
            value = getattr(klass, key)
            if isinstance(value, trace_types):
                wrapped = trace_func(value)
                setattr(klass, key, wrapped)
        return klass
      
    @trace
    class TraceDict(dict):
        pass
      
    trace_dict = TraceDict([('안녕', 1)])
    trace_dict['거기'] = 2
    trace_dict['안녕']
    try:
        trace_dict['존재하지 않음']
    except KeyError:
        pass # 키 오류가 발생할 것으로 예상함
      
    #__new__((<class '__main__.TraceDict'>, [('안녕', 1)]), {}) -> {}
    #__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
    #__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음')