ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 메타클래스로 클래스 속성에 주석달기
    언어/파이썬 & 장고 2016. 12. 6. 21:00

    메타클래스로 구현할 수 있는 기능 중 하나는 클래스를 정의한 이후에, 하지만 그 클래스를 실제로 사용하기 전에 프로퍼티를 수정하거나 주석을 붙이는 것입니다. 보통은 이 기법을 디스크립터와 함께 사용하여 클래스에서 디스크립터를 어떻게 사용하는지 자세히 조사한 정보를 디스크립터에 제공합니다.

    예를 들어 고객 데이터베이스의 로우를 표현하는 새 클래스를 정의한다고 가정합니다. 데이터베이스 테이블의 각 컬럼에 대응하는 클래스의 프로퍼티가 있어야 합니다. 따라서 프로퍼티를 컬럼 이름과 연결하는 데 사용할 디스크립터 클래스를 다음과 같이 정의합니다.

    class Field:
    def __init__(self, name):
    self.name = name
    self.internal_name = '_' + self.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) 


    Field 디스크립터에 저장할 컬럼 이름이 있으면 내장함수 setattr과 getattr을 사용해서 모든 인스턴스별 상태를 인스턴스 딕셔너리에 보호 필드로 직접 저장할 수 있습니다. 처음에는 이 방법이 메모리 누수를 피하려고 weakref로 디스크립터를 만드는 방법보다 훨씬 편리해 보입니다.

    로우를 표현하는 클래스를 정의할 때는 각 클래스 속성에 대응하는 컬럼의 이름을 지정해야 합니다.

    class Field:
        def __init__(self, name):
            self.name = name
            self.internal_name = '_' + self.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 Customer:
        # 클래스 속성
        first_name = Field('first_name')
        last_name = Field('last_name')
        prefix = Field('prefix')
        sufix = Field('sufix')
    
    
    # Field 디스크립터가 인스턴스 딕셔너리 __dict__를 기대한 대로 수정한 방식을 볼 수 있음
    foo = Customer()
    print('Before: ', repr(foo.first_name), foo.__dict__)
    foo.first_name = 'Euclid'
    print('After: ', repr(foo.first_name), foo.__dict__)
    
    # 결과
    # Before:  '' {}
    # After:  'Euclid' {'_first_name': 'Euclid'}


    위 결과는 중복되는 것처럼 보입니다. 이미 class문 본문에서 Field 객체를 생성하여 Customer.first_name에 할당할 때 필드의 이름을 선언했습니다. 왜 필드 이름(여기서는 first_name)을 Field 생성자에도 넘겨야 될까요?

    문제는 Customer 클래스 정의에서 연산 순서가 왼쪽에서 오른쪽으로 읽는 방식과는 반대라는 점입니다. 먼저 Field 생성자는 Field('first_name') 형태로 호출합니다. 다음을 이 호출의 반환 값을 Customer.field_name에 할당합니다. 그러므로 Field에서는 자신이 어떤 클래스 속성에 할당될지 미리 알 방법이 없습니다.

    중복성을 제거하려면 메타클래스를 사용하면 됩니다. 메타클래스를 이용하면 class 문을 직접 후킹하여 class 본문이 끝나자마자 원하는 동작을 처리할 수 있습니다. 이 예제에서는 필드 이름을 수동으로 여러 번 지정하지 않고 메타클래스를 사용하여 Field.name과 Field.internal_name을 디스크립터에 자동으로 할당합니다.

    class Field:
    """
    메타클래스를 사용해도 필드 디스크립터는 변경이 거의 없음. 유일한 차이는 생성자에 인수를 넘길 필요가 없음.
    대신 필드 디스크립터의 속성은 위의 Meta.__new__메서드로 설정
    """

    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 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 BetterCustomer(DatabaseRow):
    """
    메타클래스, 새 DatabaseRow 기반 클래스, 새 Field 디스크립터를 사용하면 데이터베이스 로우를 표현하는 클래스의 정의에 이전 같은 중복이 생기지 않음
    """
    # 클래스 속성
    first_name = Field()
    last_name = Field()
    prefix = Field()
    sufix = Field()


    foo = BetterCustomer()
    print('Before: ', repr(foo.first_name), foo.__dict__)
    foo.first_name = 'Euclid'
    print('After: ', repr(foo.first_name), foo.__dict__)

    # 결과
    # Before: '' {}
    # After: 'Euclid' {'_first_name': 'Euclid'}

    요약

    메타클래스를 이용하면 클래스가 완전히 정의되기 전에 클래스 속성을 수정할 수 있음

    디스크립터와 메타클래스는 선언적 동작과 런타임 내부 조사(introspection)용으로 강력한 조합을 이룸

    메타클래스와 디스크립터를 연계하여 사용하면 메모리 누수와 weakref 모듈을 모두 피할 수 있음


    댓글