들어가기 전
- 이 글은 이펙티브 파이썬을 공부하며 작성한 글입니다.
- 코드 : https://github.com/chickenchickenlove/effective-python/tree/master/item47
요약
- 인스턴스 어트리뷰트에 접근(hello.value, hasattr 등)하면 항상 __getattribute__() 메서드가 호출됨.
- __getattribute__()를 호출했을 때, 어트리뷰트가 없으면 AttributeError가 발생함. 이 때, __getattr__() 메서드가 호출됨.
- 인스턴스 어트리뷰트에 값을 대입(예시, a.hello = 10)하면, __setattr__()이 항상 호출됨.
- __getattribute__, __getattr__, setattr__에서 무한 재귀 문제를 막기 위해 항상 super().__getattribute__() 같은 형식으로 사용해야만 한다.
Item 47. 지연 계산 어트리뷰트가 필요하면 __getattr__, __getattribute__, __setattr__을 사용하라
파이썬에서 object가 제공하는 훅을 사용하면, 좀 더 제네릭하게 동적으로 어트리뷰트를 가져오고 정의할 수 있게 된다. 기존에 존재하는 다음 방법으로는 동적 어트리뷰트를 제공할 수는 없다. 왜냐하면 미리 정의해서 사용해야하기 때문이다.
- 평범한 인스턴스의 어트리뷰트
- @property 메서드
- 디스크립터
이 때, object 클래스가 제공하는 __getattribute__, __getattr__, __setattr__을 사용하면 동적 어트리뷰트 할당 / 조회 기능을 사용할 수 있다.
# 인스턴스의 어트리뷰트에 접근하면 항상 __getattribute__()가 호출됨.
# __getattribute__()에서 어트리뷰트를 찾을 수 없으면 __getattr__()이 호출됨.
# __getattr__()에서 setattr()로 인스턴스에 어트리뷰트를 동적으로 셋팅하고, 그 값을 반환할 수 있음.
# 즉, 이 메타 메서드를 이용하면 동적으로 인스턴스의 어트리뷰트를 추가할 수 있음.
class Hello:
def __init__(self):
self.hello = 'hello100'
def __getattribute__(self, item):
print('__getattribute__ called.')
value = super().__getattribute__(item)
return value
def __getattr__(self, item):
print('__getattr__ called.')
value = f'{item}을 위한 값'
setattr(self, item, value)
return value
h = Hello()
print(h.hello)
print(h.a)
>>>
__getattribute__ called.
hello100
__getattribute__ called.
__getattr__ called.
a을 위한 값
간단한 사용법은 위의 예시에서 살펴볼 수 있고, 호출 결과도 함께 확인할 수 있다.
- hello 어트리뷰트는 있기 때문에 __getattribute__만 호출됨.
- a 어트리뷰트를 조회할 때 __getattribute__가 호출되었으나, 그 값이 없기 때문에 __getattr__이 호출된다. __getattr__ 내에서 setattr()을 이용해서 인스턴스에 어트리뷰트를 셋팅할 수 있다.
주요 유즈 케이스는 다음과 같다.
- 동적 어트리뷰트 접근 : 외부 데이터 소스로부터 동적으로 속성을 생성하고 싶은 경우
- 어트리뷰트 접근 로깅 : 개발 작업 시, Attribute에 접근하는 것을 추적하고 싶을 때 사용할 수 있음.
- 속성 접근 제어 : 특정 경우에는 Attribute에 접근하지 못하도록 함.
- 속성 접근 시 전처리 : 모든 속성 접근 시, 특정 로직을 실행하고 싶을 때 사용할 수 있음.
- 프록시 패턴 구현 : 모든 속성에 접근할 때, 프록시 객체를 통해서 처리하도록 후킹 포인트를 가져갈 수 있음.
이 때, __getattribute__를 사용하는 것은 예기치 않은 부작용 (무한 재귀 호출 문제), 성능 저하를 불러 일으킬 수 있기 때문에 항상 조심해야한다. 아래에서 자세한 유즈 케이스를 살펴보자.
코드 1. getattr().
class LazyRecord:
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = f'{name}를 위한 값'
setattr(self, name, value)
return value
data = LazyRecord()
print(f'이전: {data.__dict__}')
print(f'foo: {data.foo}')
print(f'이후: {data.__dict__}')
위 코드는 다음과 같이 동작한다.
- foo 어트리뷰트에 처음 접근할 때 존재하지 않는 값임. __getattr__이 호출됨.
- __getattr__ 내에서 setattr()로 값을 셋팅하고, 그 값은 반환함.
- __dict__를 다시 살펴보면, foo 어트리뷰트 값이 존재하는 것을 확인할 수 있음.
# 이전: {'exists': 5}
# foo: foo를 위한 값
# 이후: {'exists': 5, 'foo': 'foo를 위한 값'}
위 코드의 출력 결과는 다음과 같다.
코드2. 상속에서 __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
data = LoggingLazyRecord()
print(f'이전: {data.__dict__}')
print(f'foo: {data.foo}')
print(f'이후: {data.__dict__}')
위 코드는 상속 구조에서 __getattr__()을 사용하는 방법을 나타낸다.
- LoggingLazyRecord()의 __getattr__()이 호출된다.
- 이 때 super().__getattr__()이 호출되면서 LazyRecord의 __getattr__()이 호출된다.
- LazyRecord.__getattr__()에서 반환된 value가 result에 전달되고, result가 반환됨.
여기서 주의할 점은 __getattr__()에서는 무한 재귀를 막기 위해 super().__getattr__()를 사용해야한다는 점이다. 만약 __getattr__() 내에서 self.foo로 접근하면, 다시 한번 __getattr__()이 호출된다. 따라서 무한 재귀가 반복되는 큰 문제가 생기기 때문에 super.__getattr__()를 호출해야한다.
이전: {'exists': 5}
* 호출: __getattr__('foo') 인스턴스 딕셔너리 채워 넣음
* 반환: 'foo를 위한 값'
foo: foo를 위한 값
이후: {'exists': 5, 'foo': 'foo를 위한 값'}
코드의 호출결과는 위와 같다.
코드3. __getattribute__() 사용하기
class ValidatingRecord:
def __init__(self):
self.exists = 5
def __getattribute__(self, name):
print(f'* 호출: __getattribute__({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(f'이전: {data.__dict__}')
print(f'foo: {data.foo}')
print(f'이후: {data.__dict__}')
위 코드는 __getattribute__()를 사용한 예시를 보여준다.
- data.foo가 호출될 때 마다 __getattribute__()가 호출됨.
- data.foo로 처음 접근했을 때, __getattribute__()가 호출된다. 이 때, 재귀를 막기 위해 super().__getattribute__()가 호출됨.
- 그러나 foo라는 어트리뷰트가 없기 때문에 super().__getattribute__()의 결과로 AttributeError가 발생함.
- Except 절에서 setattr()를 호출해서 어트리뷰트를 설정하고 그 값을 반환함.
* 호출: __getattribute__('__dict__')
* '__dict__' 찾음, {'exists': 5} 반환
이전: {'exists': 5}
* 호출: __getattribute__('foo')
* 'foo'를 'foo를 위한 값'로 설정
foo: foo를 위한 값
* 호출: __getattribute__('__dict__')
* '__dict__' 찾음, {'exists': 5, 'foo': 'foo를 위한 값'} 반환
이후: {'exists': 5, 'foo': 'foo를 위한 값'}
위 코드의 호출 결과를 살펴보면 다음과 같다. 위 코드에서 어트리뷰트에 총 3번 접근했는데, 로그에서 확인할 수 있듯이 __getattribute__()는 총 세번 호출된 것을 알 수 있다.
코드4. hasattr()도 __getattribute__()를 호출함.
class LazyRecord:
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = f'{name}를 위한 값'
setattr(self, name, value)
return value
data = LazyRecord()
print(f'이전: {data.__dict__}')
print(f'최초에 foo가 있나:', hasattr(data, 'foo'))
print(f'이후: {data.__dict__}')
print(f'다음에 foo가 있나:', hasattr(data, 'foo'))
hasattr() 메서드를 이용해 인스턴스/클래스에 어트리뷰트가 존재하는지 확인할 때가 있다. hasattr() 역시 기본적으로 __getattribute__()를 호출하는 형태로 동작한다.
이전: {'exists': 5}
최초에 foo가 있나: True
이후: {'exists': 5, 'foo': 'foo를 위한 값'}
다음에 foo가 있나: True
따라서 위 코드의 실행 결과는 다음과 같다.
- hasattr()을 호출하는 순간, 어트리뷰트가 없어 AttributeError가 발생하고 __getattr__()이 호출된다.
- 여기서 'foo' 어트리뷰트에 대한 값이 셋팅되고, '최초에 foo가 있나'라는 물음은 true가 된다.
코드5. __setattr__()
class SavingRecord:
def __setattr__(self, key, value):
print('set attr 호출')
super().__setattr__(key, value)
a = SavingRecord()
a.q = 10
위 코드는 __setattr__()을 사용한 예시다.
- 어트리뷰트에 값을 대입할 때 마다 항상 __setattr__()이 호출된다.
- 만약 이 부분을 후킹하고 싶다면 __setattr__()을 구현하면 된다.
- 이 때, 무한 재귀등의 문제를 막기 위해서 super().__setattr__()을 호출하도록 한다.
set attr 호출
이 코드의 호출 결과는 다음과 같다.
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
Effective Python Item 41. 기능을 합성할 때는 믹스인 클래스를 사용하라 (0) | 2024.02.16 |
---|---|
Effective Python Item 51. 합성 가능한 클래스 확장이 필요하면 메타클래스보다는 클래스 데코레이터를 사용하라. (0) | 2024.02.12 |
Effective Python Item 39. @classmethod를 통해 클래스 다형성을 이용해라. (0) | 2024.02.05 |
Effective Python Item 38. 간단한 인터페이스의 경우 클래스 대신 함수를 받아라. (0) | 2024.02.04 |
Effective Python Item 35. 제네레이터 안에서 throw로 상태를 변화시키지 마라. (0) | 2024.01.21 |