ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 지연 속성에는 __getattr__, __getattribute__, __setattr__을 사용
    언어/파이썬 & 장고 2016. 11. 26. 17:22

    파이썬의 언어 후크를 이용하면 시스템들을 연계하는 범용 코드를 쉽게 만들 수 있습니다. 예를 들어 데이터베이스의 row를 파이썬 객체로 표현한다고 할 때, 데이터베이스에는 스키마 세트가 있습니다. 그러므로 로우에 대응하는 객체를 사용하는 코드는 데이터베이스 형태도 알아야 합니다. 하지만 파이썬에서는 객체와 데이터베이스를 연결하는 코드에서 로우의 스키마를 몰라도 됩니다. 코드를 범용으로 만들면 됩니다.

    사용하기에 앞서 정의부터 해야하는 일반 인스턴스 속성, @property 메서드, 디스크립터로는 이렇게 할 수 없습니다. 파이썬은 __getattr__이라는 특별한 메서드로 이런 동작을 가능하게 합니다. 클래스에 __getattr__ 메서드를 정의하면 객체의 인스턴스 딕셔너리에서 속성을 찾을 수 없을 때마다 이 메서드가 호출됩니다.


    class LazyDB:
        def __init__(self):
            self.exists = 5
        def __getattr__(self, name):
            value = 'Value for %s' % name
            setattr(self, name, value)
            return value
    
    data = LazyDB()
    
    
    print('Before: ', data.__dict__)
    print('foo ', data.foo)
    print('After: ', data.__dict__)
    
    # 결과
    # ('Before: ', {'exists': 5})
    # ('foo ', 'Value for foo')
    # ('After: ', {'foo': 'Value for foo', 'exists': 5})

    존재하지 않은 속성인 foo에 접근하려 할 때, 파이썬이 __getattr__메서드를 호출하게 되고 이어서 인스턴스 딕셔너리 __dict__를 변경하게 됩니다.


    다음 코드에서 __getattr__이 실제로 호출되는 시점을 보여주려고 LazyDB에 로깅을 추가합니다. 무한 반복을 피하려고 super().__getattr__()로 실제 프로퍼티 값을 얻어오는 부분을 주시해야합니다.


    class LazyDB:
        def __init__(self):
            self.exists = 5
    
        def __getattr__(self, name):
            value = 'Value for %s' % name
            setattr(self, name, value)
            return value
    
    
    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)
    
    # 결과
    # exists:  5
    # Called __getattr__(foo)
    # foo  Value for foo
    # foo  Value for foo

    exists 속성은 인스턴스 딕셔너리에 있으므로 __getattr__이 절대 호출되지 않습니다. foo 속성은 원래는 인스턴스 딕셔너리에 없으므로 처음에는 __getattr__이 호출됩니다. 하지만 foo에 대응하는 __getattr__호출은 setattr을 호출하며, setattr은 인스턴스 딕셔너리에 foo를 저장합니다. 따라서 foo에 두 번째로 접근할 때는 __getattr__이 호출되지 않습니다.


    이런 동작은 스키마리스 데이터( 스키마가 정해지지 않은 데이터)에 지연 접근하는 경우에 도움이 됩니다. __getattr__이 프로퍼티 로딩이라는 어려운 작업을 한 번만 실행하면 다음 접근부터는 기존 결과를 가져옵니다. 

    데이터베이스 시스템에서 트랜잭션도 원한다고 할 때, 사용자가 다음 번에 속성에 접근할 때는 대응하는 데이터베이스의 로우가 여전히 유효한지, 트랜잭션이 여전히 열려 있는지 알고 싶다고 가정합니다. __getattr__ 후크는 기존 속성에 빠르게 접근하려고 객체의 인스턴스 딕셔너리를 사용할 것이므로 이 작업에는 믿고 쓸 수 없습니다. 파이썬에서는 이런 쓰임새를 고려한 __getattribute__라는 또 다른 후크가 있습니다. 이 특별한 메서드는 객체의 속성에 접근할 때마다 호출되며, 심지어 해당 속성이 속성 딕셔너리에 있을 때도 호출됩니다. 이런 동작 덕분에 속성에 접근할 때마다 전역 트랜잭션 상태를 확인하는 작업 등에 쓸 수 있습니다. 여기서는 __getattribute__가 호출될 때마다 로그를 남기려고 ValidatingDB를 정의했습니다. 


    class ValidatingDB:
        def __init__(self):
            self.exists = 5
    
        def __getattribute__(self, item):
            print('Called __getattribute__(%s)' % item)
    
            try:
                return super().__getattribute__(item)
            except AttributeError:
                value = 'Value for %s' % item
                setattr(self, item, value)
                return value
    
    
    data = ValidatingDB()
    print('exists: ', data.exists)
    print('foo ', data.foo)
    print('foo ', data.foo)
    
    # 결과
    # Called __getattribute__(exists)
    # exists:  5
    # Called __getattribute__(foo)
    # foo  Value for foo
    # Called __getattribute__(foo)
    # foo  Value for foo


    동적으로 접근한 프로퍼티가 존재하지 않아야 하는 경우에는 AttributeError를 일으켜서 __getattr__, __getattribute__에 속성이 없는 경우의 파이썬 표준 동작이 일어나게 해야합니다.

    class MissingPropertyDB:
        def __getattr__(self, item):
            if item == 'bad_name':
                raise AttributeError('%s is missing' % item)
    
    data = MissingPropertyDB()
    data.bad_name
     
    # 결과
    # AttributeError: bad_name is missing 에러 발생


    파이썬 코드로 범용적인 기능을 구현할 때, 종종 내장 함수 hasattr로 프로퍼티가 있는지 확인하고 내장 함수 getattr로 프로퍼티 값을 가져옵니다. 이 함수들도 __getattr__을 호출하기 전에 인스턴스 딕셔너리에서 속성 이름을 찾습니다.

    class LazyDB:
        def __init__(self):
            self.exists = 5
    
        def __getattr__(self, name):
            value = 'Value for %s' % name
            setattr(self, name, value)
            return value
    
    
    class LoggingLazyDB(LazyDB):
        def __getattr__(self, name):
            print('Called __getattr__(%s)' % name)
            return super().__getattr__(name)
    
    
    data = LoggingLazyDB()
    print('Before: ', data.__dict__)
    print('foo exists: ', hasattr(data, 'foo'))
    print('After: ', data.__dict__)
    print('foo exists: ', hasattr(data, 'foo'))
    
    # 결과
    # Before:  {'exists': 5}
    # Called __getattr__(foo)
    # foo exists:  True
    # After:  {'foo': 'Value for foo', 'exists': 5}
    # foo exists:  True


    위 예제에서는 __getattr__은 한 번만 호출됩니다. 이와 대조로 __getattribute__를 구현한 클래스인 경우에는 객체에 hasattr이나 getattr을 호출할 때마다 __getattribute__가 실행됩니다.

    class ValidatingDB:
        def __init__(self):
            self.exists = 5
    
        def __getattribute__(self, item):
            print('Called __getattribute__(%s)' % item)
    
            try:
                return super().__getattribute__(item)
            except AttributeError:
                value = 'Value for %s' % item
                setattr(self, item, value)
                return value
    
    
    data = ValidatingDB()
    print('foo exists: ', hasattr(data, 'foo'))
    print('foo exists: ', hasattr(data, 'foo'))
    
    
    # 결과
    # Called __getattribute__(foo)
    # foo exists:  True
    # Called __getattribute__(foo)
    # foo exists:  True


    이제 파이썬 객체에 값을 할당할 때 지연 방식으로 데이터를 데이터베이스에 집어넣고 싶다고 가정합니다. 이 작업은 임의의 속성 할당을 가로채는 __setattr__ 언어 후크로 할 수 있습니다. __getattr__과 __getattribute__로 속성을 추출하는 것과는 다르게 별도의 메서드 두 개가 필요하지 않습니다. __setattr__ 메서드는  인스턴스의 속성이 할당을 받을 때마다 직접 혹은 내장 함수 setattr을 통해 호출됩니다.

    class SavingDB:
        def __setattr__(self, key, value):
            # 몇몇 데이터를 DB로그로 저장함
            super().__setattr__(key, value)
    
    
    class LoggingSavingDB(SavingDB):
        def __setattr__(self, key, value):
            print('Called __setattr__(%s, %r)' % (key, value))
            super().__setattr__(key, value)
    
    
    data = LoggingSavingDB()
    print('Before: ', data.__dict__)
    data.foo = 5
    print('After: ', data.__dict__)
    data.foo = 7
    print('Finally: ', data.__dict__)
    
    
    # 결과
    # Before:  {}
    # Called __setattr__(foo, 5)
    # After:  {'foo': 5}
    # Called __setattr__(foo, 7)
    # Finally:  {'foo': 7}


    __getattribute__와 __setattr__을 사용할 때 부딪히는 문제는 객체의 속성에 접근할 때마다 (심지어 원하지 않을 때도) 호출된다는 점입니다. 예를 들어, 객체의 속성에 접근하면 실제로 연관 딕셔너리에서 키를 찾게 하고 싶다고 가정합니다.

    class BrokenDictionaryDB:
        def __init__(self, data):
            self._data = {}
        
        def __getattribute__(self, item):
            print('Called __getattribute__(%s)' % item)
            return self._data[item]
        
    data = BrokenDictionaryDB({'foo':3})
    data.foo
    
    # 결과
    # Called __getattribute__(foo)
    # Called __getattribute__(_data)
    # Called __getattribute__(_data)
    ...
    #RuntimeError: maximum recursion depth exceeded

    위와 같이 __getattribute__ 메서드에서 self._data에 접근해야 합니다. 하지만 실제로 시도해보면 파이썬이 스택의 한계에 도달할 때까지 재귀 호출을 하게 되어 결국 프로그램이 중단됩니다.


    문제는 __getattribute__가 self._data에 접근하면 __getattribute__가 다시 실행되고, 다시 self._data에 접근한다는 점입니다. 해결책은 인스턴스에서 super().__getattrbute__ 메서드로 인스턴스 속성 딕셔너리에서 값을 얻어오는 것입니다. 이렇게 하면 재귀 호출을 피할 수 있습니다.

    class BrokenDictionaryDB:
        def __init__(self, data):
            self._data = data
    
        def __getattribute__(self, item):
            data_dict = super().__getattribute__('_data')
            return data_dict[item]

    마찬가지 이유로 객체의 속성을 수정하는 __setattr__ 메서드에서도 super()._setattr__을 사용해야 합니다.

    요약

    객체의 속성을 지연방식으로 로드하고 저장하려면 __getattr__과 __setattr__을 사용

    __getattr__은 존재하지 않은 속성에 접근할 때 한 번만 호출되는 반면, __getattribute__는 속성에 접근할 때마다 호출

    __getattribute__와 __setattr__에서 인스턴스 속성에 직접 접근할 때, super() (즉 object 클래스)의 메서드를 사용하여 무한 재귀가 일어나지 않게 주의


    __getattr__, __getattribute__ 상세설명

    https://ziwon.github.io/posts/python_magic_methods/

    댓글