본문 바로가기
Programming Language/Python

Effective Python. 지연 속성에는 __getattr__, __getattribute__, __setattr__을 사용하자.

by 알파해커 2021. 3. 2.
반응형

미리 정의되어 있지 않은 속성 값에 접근하려면 어떻게 해야할까?

다시 말해, 아래와 같은 클래스를 통해 인스턴스를 만들고, data.foo 와 같이 존재하지 않는 속성에 액세스 하려고 하면 에러가 날 것이다.

class LazyDB:
    def __init__(self):
        self.exists = 5

 

만약 이런 상황에서, data.foo와 같은 액세스를 했을 때도 에러 없이 동작하도록 하려면 어떻게 해야할까?

@property 메서드, 디스크립터로는 이렇게 할 수 없다.

 

이럴때 __getattr__ 매직 메소드를 사용하면 된다.

class LazyDB:
    def __init__(self):
        self.exists = 5

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

 

이랬을 때 아래와 같이 동작한다.

data = LazyDB()
print(f'Before: {data.__dict__}')
print(f'foo: {data.foo}')
print(f'After: {data.__dict__}')

>>>
Before: {'exists': 5}
foo: Value for foo
After: {'exists': 5, 'foo': 'Value for foo'}

 

__getattr__ 메소드는 인스턴스 속성으로 존재하지 않을때 (즉, 인스턴스 딕셔너리에 없을때)만 호출된다.

다시말해, 존재하는 속성에 접근할 때는 호출되지 않는다.

 

만약, 속성이 존재하든, 존재하지 않든 매번 호출되게 하려면 __getattribute__ 메소드를 이용하면 된다.

class ValidatingDB:
    def __init__(self):
        self.exists = 5
        
    def __getattribute__(self, name):
        print(f'Called __getattribute__{name}')
        try:
            return super().__getattribute__(name)
        except AttributeError:
            value = f'Value for {name}'
            setattr(self, name, 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

 

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

 

또한, __getattribute__를 구현한 클래스인 경우, hasattr이나 getattr을 호출할 때마다 __getattribute__가 실행된다. 

 

이러한 특성 때문에, __getattribute__와 __setattr__을 사용할 때 부딪히는 문제는 객체의 속성에 접근할 때마다 (심지어 원하지 않을 때도) 호출된다는 점이다. 예를 들어 객체의 속성에 접근하면 실제로 딕셔너리에서 키를 찾게 하고 싶다고 해보자.

class BrokenDictionaryDB:
    def __init__(self, data):
        self._data = {}
    
    def __getattribute__(self, name):
        print(f'Called __getattribute__{name}')
        return self._data[name]

 

그러려면, 위와 같이 __getattribute__ 메서드에서 self._data에 접근해야한다.

하지만 실제로 시도해보면 파이썬이 스택의 한계에 도달할 때까지 재귀 호출을 하게 되어 결국 프로그램이 중단된다.

 

문제는 __getattribute__가 self._data에 접근하면 __getattribute__가 다시 실행되고, 다시 self._data에 접근한다는 점이다.

 

해결책은 인스턴스에서 super().__getattribute__ 메서드로 인스턴스 속성 딕셔너리에서 값을 얻어오는 것이다.

이렇게 하면 재귀 호출을 피할 수 있다.

class DictionaryDB:
    def __init__(self, data):
        self._data = {}
    
    def __getattribute__(self, name):
        data_dict = super().__getattribute__('_data')
        return data_dict[name]

 

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

 

반응형

댓글