파이썬 코딩의 기술

파이썬코딩의기술 : Betterway 47 - 지연 계산 애트리뷰트가 필요하면 __getattr__, __getattribute__, __setattr__을 사용하라

J-Chris 2023. 3. 23. 14:24

스키마를 표현하는 클래스를 더 일반화하는 방법

파이썬에서는 __getattr__이라는 특별 메서드를 사용해 이런 동적 기능을 활용할 수 있다.

- 어떤 클래스 안에 __getattr__ 메서드 정의가 있으면, 이 객체의 인스턴스 딕셔너리에서 찾을 수 없는 애트리뷰트에 접근할 때만다 __getattr__이 호출됩니다.

 

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

    def __getattr__(self, name):
        value = f'{name}를 위한 값'
        setattr(self, name, value)
        return value
        
        
 >>> 
이전: {'exists': 5}
foo:  foo를 위한 값
이후: {'exists': 5, 'foo': 'foo를 위한 값'}

 

LazyRecord에 로그를 추가해서 __getattr__이 실제로 언제 호출되는지 살펴봅니다.

- 무한 재귀를 피하고 실제 프로퍼티 값을 가져오기 위해 super().__getattr__()을 통해 상위 클래스의 __getattr__ 구현을 사용했다는 점에 유의할 것.

 

class LoggingLazyRecord(LazyRecord):
    def __getattr__(self, name):
        print(f'* 호출: __getattr__({name!r}),'
              f'인스턴스 딕셔너리 채워 넣음')
        result = super().__getattr__(name)
        print(f'*반환: {result!r}')
        return result
    
data = LoggingLazyRecord()
print('exists: ', data.exists)
print('첫 번째 foo:', data.foo)
print('두 번째 foo:', data.foo)

>>> 

exists:  5
* 호출: __getattr__('foo'),인스턴스 딕셔너리 채워 넣음
*반환: 'foo를 위한 값'
첫 번째 foo: foo를 위한 값
두 번째 foo: foo를 위한 값

고급 사용법을 제공하기 위해 파이썬은 __getattribute__라는 다른 object 훅을 제공합니다.

- 이 특별 메서드는 객체의 애트리뷰트에 접근할 때마다 호출됨

- 애트리뷰트 디렉터리에 존재하는 애트리뷰트에 접근할 때도 이 훅이 호출됨 

이를 통해 프로퍼티에 접근할 때마다 항상 전역 트랜잭션 상태를 검사하는 등의 작업을 수행할 수 있습니다.

 

다음 코드는  __getattribute__가 호출될 때마다 로그를 남기는 ValidatingRecord를 정의합니다.

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

    def __getattribute__(self, name):
        print(f'* 호출: __getattr__({name!r})')
        try:
            value = super().__getattribute__(name)
            print(f'* {name!r} 찾음, {value!r} 반환')
            return value
        except AttributeError:
            value = f'{name}를 위한 값'
            print(f'* {name!r}를 {value!r}로 설정')
            setattr(self, name, value)
            return value
        
data = ValidatingRecord()
print('exists: ', data.exists)
print('첫 번째 foo: ', data.foo)
print('두 번째 foo: ', data.foo)

>>> 
* 호출: __getattr__('exists')
* 'exists' 찾음, 5 반환
exists:  5
* 호출: __getattr__('foo')
* 'foo'를 'foo를 위한 값'로 설정
첫 번째 foo:  foo를 위한 값
* 호출: __getattr__('foo')
* 'foo' 찾음, 'foo를 위한 값' 반환
두 번째 foo:  foo를 위한 값

 

존재하지 않는 프로퍼티에 동적으로 접근하는 경우에는 AttributeError 예외가 발생합니다.

- __getattr__과 __getattribute__에서 존재하지 않는 프로퍼티를 사용할 때 발생하는 표준적인 예외가 AttributeError다.

class MissingPropertyRecord:
    def __getattr__(self, name):
        if name == 'bad_name':
            raise AttributeError(f'{name}을 찾을 수 없음')
        ...
data = MissingPropertyRecord()
data.bad_name


>>>
AttributeError: bad_name을 찾을 수 없음

파이썬에서 일반적인 기능을 구현하는 코드가 

1) hasattr 내장 함수를 통해 프로퍼티가 존재하는지 검사하는 기능

2) getattr 내장 함수를 통해 프로퍼티 값을 꺼내오는 기능에 의존할 때도 있다.

 

이 두 함수도 __getattr__을 호출하기 전에 애트리뷰트 이름을 인스턴스 딕셔너리에서 검색함

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

    def __getattr__(self, name):
        value = f'{name}를 위한 값'
        setattr(self, name, value)
        return value

class LoggingLazyRecord(LazyRecord):
    def __getattr__(self, name):
        print(f'* 호출: __getattr__({name!r}),'
              f'인스턴스 딕셔너리 채워 넣음')
        result = super().__getattr__(name)
        print(f'*반환: {result!r}')
        return result

class MissingPropertyRecord:
    def __getattr__(self, name):
        if name == 'bad_name':
            raise AttributeError(f'{name}을 찾을 수 없음')
        ...

data = LoggingLazyRecord() # __getattr__을 구현
print('이전:', data.__dict__)
print('최초에 foo가 있나:', hasattr(data, 'foo'))
print('이후:', data.__dict__)
print('다음에 foo가 있나:', hasattr(data, 'foo'))


>>> 
이전: {'exists': 5}
* 호출: __getattr__('foo'),인스턴스 딕셔너리 채워 넣음
*반환: 'foo를 위한 값'
최초에 foo가 있나: True
이후: {'exists': 5, 'foo': 'foo를 위한 값'}
다음에 foo가 있나: True

이 예제에서는 __getattr__이 단 한 번만 호출됩니다.

 

다음 예제에서는 __getattribute__를 구현하는 클래스에서  인스턴스에 대해 hasattr이나 getattr이 쓰일 때마다 

__getattribute__가 호출되는 모습을 볼 수 있습니다.

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

    def __getattribute__(self, name):
        print(f'* 호출: __getattr__({name!r})')
        try:
            value = super().__getattribute__(name)
            print(f'* {name!r} 찾음, {value!r} 반환')
            return value
        except AttributeError:
            value = f'{name}를 위한 값'
            print(f'* {name!r}를 {value!r}로 설정')
            setattr(self, name, value)
            return value

data = ValidatingRecord()
print('최초에 foo가 있나:', hasattr(data, 'foo'))
print('다음에 foo가 있나:', hasattr(data, 'foo'))

>>> 
* 호출: __getattr__('foo')
* 'foo'를 'foo를 위한 값'로 설정
최초에 foo가 있나: True
* 호출: __getattr__('foo')
* 'foo' 찾음, 'foo를 위한 값' 반환
다음에 foo가 있나: True

파이썬 객체에 값이 대입된 경우, 나중에 이 값을 데이터베이스에 다시 저장하고 싶다고 할 때,

- 임의의 애트리뷰트에 값을 설정할 때마다 호출되는 object 훅인 __setattr__을 사용하면, 이런 기능을 비슷하게 구현할 수 있다.

 

__setattr__은 인스턴스의 애트리뷰트에 (직접 대입하든 setattr 내장 함수를 통해서든) 대입이 이뤄질 때마다 항상 호출됩니다. 

class SavingRecord:
    def __setattr__(self, name, value):
        # 데이터를 데이터베이스 레코드에 저장한다
        ...
        super().__setattr__(name, value)

다음 코드는 로그를 남기는 하위 클래스로 SavingRecord를 정의합니다.

- 이 클래스에 속한 인스턴스의 애트리뷰트 값을 설정할 때마다 __setattr__메서드가 항상 호출됩니다.

class SavingRecord:
    def __setattr__(self, name, value):
        # 데이터를 데이터베이스 레코드에 저장한다
        ...
        super().__setattr__(name, value)

class LoggingSavingRecord(SavingRecord):
    def __setattr__(self, name, value):
        print(f'* 호출: __setattr__({name!r}, {value!r})')
        super().__setattr__(name, value)
    
data = LoggingLazyRecord()
print('이전: ', data.__dict__)
data.foo = 5
print('이후:', data.__dict__)
data.foo = 7
print('최후:', data.__dict__)


>>> 
이전:  {'exists': 5}
이후: {'exists': 5, 'foo': 5}
최후: {'exists': 5, 'foo': 7}

__getattribute__와 __setattr__의 문제점은 어떤 객체의 모든 애트리뷰트에 접근할 때마다 함수가 호출된다는 것입니다.

- (예) 어떤 객체와 관련된 딕셔너리에 키가 있을 때만 이 객체의 애트리뷰트에 접근하고 싶다고 하자.

1

이 클래스 정의는 self._data에 대한 접근을 __getattribute__를 통해 수행하도록 요구합니다.

하지만,

실제로 이 코드를 실행해보면, 파이썬이 스택을 다 소모할 때까지 재귀를 수행하다 죽어버립니다.

2

이 문제는 __getattribute__가 self._data에 접근해서 __getattribute__가 다시 호출되기 떄문입니다.

 

해결방법은 super().__getattribute__를 호출해 인스턴스 앤트리뷰트 딕셔너리에서 값을 가져오는 것 입니다.

- 이렇게 하면 재귀를 피할 수 있습니다.

3

__setattr__ 메서드 안에서 애트리 뷰트를 변경하는 경우에도 super().__setattr__을 적절히 호출해야 합니다.

 

 

### 기억해야 할 내용 ###

- __getattr__과 __setattr__을 사용해 객체의 애트리뷰트를 지연해 가져오거나 저장할 수 있다.

- __getattr__은 애트리뷰트가 존재하지 않을 때만 호출되지만, __getattribute__는 애트리뷰트를 읽을 때마다 항상 호출된다는 점을 이해하라.

- __getattribute__와 __setattr__에서 무한 재귀를 피하려면 super()에 있는 (즉, object 클래스에 있는) 메서드를 사용해 인스턴스 애트리뷰트에 접근하라.