본문 바로가기

프로그래머/Python

[Effective Python 복습] Chapter 4. 메타클래스와 속성

파이썬 코딩의 기술


Chapter4. 메타클래스와 속성

- 메타클래를 이용하면 파이썬의 class문을 가로채서 클래스가 정의될 때마다 특별한 동작을 제공할 수 있다
- 또 하나의 강력한 기능은 속성 접근을 동적으로 사용자화하는 파이썬의 내장 기능이다
- 동적 속성은 객체들을 오버라이드하다가 예상치 못한 부작용을 일으키게 할 수 있다
- 최소 놀랍 규칙을 따르자

29. getter와 setter method 대신에 일반 속성을 사용하자

  • 간단한 공개 속성을 사용하여 새 클래스 인터페이스를 정의하고 setter와 getter method는 사용하지 말자
  • 객체의 속성에 접근할 때 특별한 동작을 정의하려면 @property를 사용하자
  • @property method에서 최소 놀람 규칙을 따르고 이상한 부작용은 피하자
  • @property method가 빠르게 동작하도록 만들자. 느리거나 복잡한 작업은 일반 method로 하자
class OldResistor(object):
    def __init__(self, ohms):
        self._ohms = ohms

    def get_ohms(self):
        return self._ohms

    def set_ohms(self, ohms):
        self._ohms = ohms

r0 = OldResistor(50e3)
r0.set_ohms(10e3)
  • 이런 getter와 setter를 사용하는 방법은 간단하지만 파이썬답지 않다
class Resistor(object):
    def __init__(self, ohms):
        self.ohms = ohms
        self.voltage = 0
        self.current = 0

r1 = Resistor(50e3)
r1.ohms = 10e3
r1.ohms += 53        # 즉석에서 증가시키기 같은 연산이 자연스럽고 명확해진다


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)
r2.voltage = 10
  • setter와 getter method의 이름이 의도한 property 이름과 일치한다(voltage)
class BoundResistance(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 ohms must be > 0' % ohms)
        self._ohms = ohms


class FixedResistance(Resistor):
    # ...
    @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
  • property에 setter를 설정하면 클래스에 전달된 값들의 타입을 체크하고 값을 검증할 수도 있다
  • 부모 클래스의 속성을 불변으로 만드는데도 @property를 사용할 수 있다

30. 속성을 리팩토링하는 대신 @property를 고려하자

  • 기존의 인스턴스 속성에 새 기능을 부여하려면 @property를 사용하자
  • @property를 사용하여 점점 나은 데이터 모델로 발전시키자
  • @property를 너무 많이 사용한다면 클래스와 이를 호출하는 모든 곳을 리팩토링하는 방안을 고려하자
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):
        return self.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

31. 재사용 가능한 @property 메서드에는 디스크립터를 사용하자

  • 직접 디스크립터 클래스를 정의하여 @property 메서드의 동작과 검증을 재사용하자
  • WeakKeyDictionary를 사용하여 디스크립터 클래스가 메모리 누수를 일으키지 않게 하자
  • getattribute가 디스크립터 프로토콜을 사용하여 속성을 얻어오고 설정하는 원리를 정확히 이해하려는 함정에 빠지지 말자
class Grade(object):
    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('Grade must be between 0 and 100')
        self._values[instance] = value

class Exam(object):
    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

32. 지연 속성에는 getattr, getattribute, setattr을 사용하자

  • 객체의 속성을 지연 방식으로 로드하고 저장하려면 getattrsetattr을 사용하자
  • getattr은 존재하지 않는 속성에 접근할 때 한 번만 호출되는 반면에 getattribute는 속성에 접근할 때마다 호출된다는 점을 이해하자
  • getattributesetattr에서 인스턴스 속성에 직접 접근할 때 super()(즉, object 클래스)의 메서드를 사용하여 무한 재귀가 일어나지 않게 하자
class LazyDB(object):
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        value = 'Value for %s' % name
        setattr(self, name, vlaue)
        return value

data = LazyDB()
print('Before:', data.__dict__)
print('foo:', data.foo)
print('After:', data.__dict__)

class LoggingLazyDB(LazyDB):
    def __getattr__(self, name):
        print('Called __getattr__(%s)' % name)
        return super().__getattr__(name)

data = LoggingLazyDB()
print('exists:', data.exists)
print('foo:', data.foo)
print('foo:', data.foo())
  • getattr이 property loading을 한 번 실행하면 다음 접근부터는 기존 결과를 가져온다
class ValidatingDB(object):
    def __init__(self):
        self.exists = 5

    def __getattribute__(self, name):
        print('Called __getattribute__(%s)' % name)
        try:
            return super().__getattribute__(name)
        except AttributeError:
            value = 'Value for %s' % name
            setattr(self, name, vlaue)
            return value

data = ValidatingDB()
print('exists:', data.exists)
print('foo:', data.foo)
print('foo:', data.foo)
class SavingDB(object):
    def __setattr__(self, name, value):
        # 데이터 저장
        # ...
        super().__setattr__(name, value)

class LoggingSavingDB(SavingDB):
    def __setattr__(self, name, value):
        print('Called __setattr__(%s, %r) % (name, value)')
        super().__setattr__(name, value)

data = LoggingSavingDB()
print('Before: ', data.__dict__)
data.foo = 5
print('After: ', data.__dict__)
data.foo = 7
print('Finally: ', data.__dict__)

class DictionaryDB(object):
    def __init__(self, data):
        self._data = data

    def __getattribute__(self, name):
        data_dict = super().__getattribute__('_data')
        return data_dict[name]

33. 메타클래스로 서브클래스를 검증하자

  • 서브클래스 타입의 객체를 생성하기에 앞서 서브클래스가 정의 시점부터 제대로 구성되어음을 보장하려면 메타클래스를 사용하자
  • 파이썬 2와 파이썬 3의 메타클래스 문법은 약간 다르다
  • 메타클래스의 new 메서드는 class 문의 본문 전체가 처리된 후에 실행된다
class Meta(type):
    def __new__(meta, name, bases, class_dict):
        print((meta, name, bases, class_dict))
        return type.__new__(meta, name, bases, class_dict)

class MyClass(object, metaclass=Meta):
    stuff = 123

    def foo(self):
        pass

# 파이썬2
class Meta(type):
    def __new__(meta, name, bases, class_dict):
        # ...

class MyClassInPython2(object):
    __metaclass__ = Meta
    # ...
  • 메타클래스는 클래스의 이름, 클래스가 상속하는 부모 클래스, class 본문에서 정의한 모든 클래스 속성에 접근할 수 있다

클래스가 정의도기 전에 모든 파라미터를 검증하려면 Meta.new메서드에 기능을 추가하면 된다

class ValidataPolygon(type):
    def __new__(meta, name, bases, class_dict):
        # 추상 Polygon 클래스는 검증하지 않음
        if bases != (object, ):
            if class_dict['sides'] < 3:
                raise ValueError('Polygons need 3+ sides')
        return type.__new__(meta, name, bases, class_dict)

class Polygon(object, metaclass=ValidationPolygon):
    sizes = None # 서브클래스에서 설정함

    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

class Triangle(Polygon):
    sides = 3

class Line(Polygon):
    sides = 1    # valueerror

34. 메타클래스로 클래스의 존재를 등록하자

  • 클래스의 등록은 모듈 방식의 파이썬 프로그램을 만들 때 유용한 패턴이다
  • 메타클래스를 이용하면 프로글매에서 기반 클래스로 서브클래스를 만들 때마다 자동으로 등록 코드를 실행할 수 있다
  • 메타클래스를 이용해 클래스를 등록하면 등록 호출을 절대 빠뜨리지 않으므로 오류를 방지할 수 있다
class Serializable(object):
    def __init__(self, *args):
        self.args = args

    def serializae(self):
        return json.dumps({'args': self.args})

class Deserializable(Serializable):
    @classmethod
    def deserialize(cls, json_data):
        params = json.loads(json_data)
        return cls(*params['args'])

class BetterSerializable(object):
    def __init__(self, *args):
        self.args = args

    def serializae(self):
        return json.dumps({
            'class': self.__class__.__name__,
            'args': self.args
        })

registry = {}

def register_class(target_class):
    registry[target_class.__name__] = target_class

def deserialize(data):
    param = 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

35. 메타클래스로 클래스 속성에 주석을 달자

  • 메타클래스를 이용하면 클래스가 완전히 정의되기 전에 클래스 속성을 수정할 수 있다
  • 디스크립터와 메타클래스는 선언적 동작과 런타임 내부 조사(introspection)용으로 강력한 조합을 이룬다
  • 메타클래스와 디스크립터를 연계하여 사용하면 메모리 누수와 weakref 모듈을 모두 피할 수 있다
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.__name__(meta, name, bases, class_dict)
        return cls

class DatabaseRow(object, metaclass=Meta):
    pass

class Field(object):
    def __init__(self, name):
        self.name = name
        self.internal_name = '_' + self.name
    #...

class BetterCustomer(DatabaseRow):
    first_name = Field()
    last_name = Field()
    prefix = Field()
    suffix = Field()

foo = BetterCustomer()
print('Before:', repr(foo.first_name), foo__dict__)
foo.first_name = 'Euler'
print('After:', repr(foo.first_name), foo__dict__)