파이썬코딩의기술 : Betterway 47 - 지연 계산 애트리뷰트가 필요하면 __getattr__, __getattribute__, __setattr__을 사용하라
스키마를 표현하는 클래스를 더 일반화하는 방법
파이썬에서는 __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 클래스에 있는) 메서드를 사용해 인스턴스 애트리뷰트에 접근하라.