API가 실행되는 과정에서 전달한 함수를 실행하는 경우, 이런 함수를 훅(hook)이라고 부른다.
(예) key 훅으로 len 내장 함수를 전달해서 이름이 들어 있는 리스트를 이름의 길이에 따라 정렬합니다.
names = ['소크라테스', '아르키메데스', '플라톤', '아리스토텔리스']
naems.sort(key=len)
print(names)
>>>
['플라톤', '소크라테스', '아르키메데스', '아리스토텔레스']
파이썬에서는 단순히 인자와 반환 값이 잘 정의된, 상태가 없는 함수를 훅으로 사용하는 경우가 많습니다.
- 함수는 클래스보다 정의하거나 기술하기가 쉬움
- 파이썬은 함수를 일급 시민 객체로 취급
defaultdict에는 딕셔너리 안에 없는 키에 접근할 경우 호출되는 인자가 없는 함수를 전달할 수 있습니다.
- 이 함수는 존재하지 않는 키에 해당하는 값이 될 객체를 반환해야함
다음 코드는 존재하지 않는 키에 접근할 때 로그를 남기고 0을 디폴트 값으로 반환해야함
def log_missing():
print('키 추가됨')
return 0
원본 딕셔너리와 변경할 내용이 주어진 경우, log_missing 함수는 로그를 두 번 남길 수 있습니다.
from collection import defaultdict
current = {'초록': 12, '파랑': 3}
increments = [
('빨강', 5),
('파랑', 17),
('주황', 9),
]
result = defaultdict(log_missing, current)
print('이전:', dict(result))
for key, amount in increments:
result[key] += amount
print('이후:', dict(result))
>>>
이전: {'초록': 12, '파랑': 3}
키 추가됨
키 추가됨
이후: {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9}
log_missing과 같은 함수를 사용할 수 있으면,
정해진 동작과 부수 효과(side effect)를 분리할 수 있기 때문에 API를 더 쉽게 만들 수 있습니다.
(예) defaultdict에 전달하는 디폴터 값 훅이 존재하지 않는 키에 접근한 총횟수를 세고 싶다고 할 때
- 이런 기능을 만드는 방법중 하는는 상태가 있는 클로저를 사용하는 것
다음 코드는 이런 클로저가 있는 도우미 함수를 디폴트 값 훅으로 사용합니다.
def increment_with_report(current, increments):
added_count = 0
def missing():
nonlocal added_count # 상태가 있는 클러저
added_count += 1
return 0
result = defaultdict(missing, current)
for key, amount in increments:
result[key] += amount
return result, added_count
인터페이스에서 간단한 함수를 인자로 받으면 클로저 안에 상태를 감추는 기능 계층을 쉽게 추가할 수 있습니다
result, count = increment_with_report(current, increments)
assert count == 2
그러나!! 상태를 다루기 위한 훅으로 클로적를 사용하면 상태가 없는 함수에 비해 이해하기 어렵습니다.
다른 접근 방법으로 추적하고 싶은 상태를 저장하는 작은 클래스를 정의하면 됩니다.
class CountMissing:
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
파이썬에서는 일급 함수를 사용해 객체에 대한 CountMissing.missing 메서드를 직접 defaultdict의 디폴트 값 훅으로 전달할 수 있습니다.
counter = CountMissing()
result = defaultdict(counter.missing, current) # 메서드 참조
for key, amount in increments:
result[key] += amount
assert counter.added == 2
위에 increment_with_report 같은 함수보다 도우미 클래스로 상태가 있는 클러저와 같은 동작을 제공하는 것이 좋습니다
하지만!!
클래스 자체만 놓고 보면 CountMissing 클래스의 목적이 무엇인지 분명히 알기는 어렵습니다.
이런 경우 파이썬에서는 클래스에 __call__ 특별 메서드를 정의할 수 있습니다.
- __call__을 사용하면 객체를 함수처럼 호출할 수 있습니다.
- __call__이 정의된 클래스의 인스턴스에 대해 callable 내장 함수를 호출하면, True를 반환합니다.
class BetterCountMissing:
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
assert counter() == 0
assert callable(counter)
다음 코드는 BetterCountMissing 인스턴스를 defaultdict의 디폴트 값 훅으로 사용해서
존재하지 않는 키에 접근한 횟수를 추적합니다.
counter = BetterCountMissing()
result = defaultdict(counter, current) # __call__ 에 의존함
for key, amount in increments:
result[key] += amount
assert counter.added == 2
장점
1) __call__메서드는 함수가 인자로 쓰일 수 있는 부분에 이 클래스의 인스턴스를 사용할 수 있습니다.
- 클래스의 동작을 알아보기 위한 시작점이 __call__이라는 사실을 알 수 있음
- 클래스를 만든 목적이 상태를 저장하는 클로저 역할이라는 사실을 알 수 있음
2) defaultdict가 __call__ 내부에서 어떤 일이 벌어지는지에 대해 전혀 알 필요가 없다는 사실
- defaultdict에게 필요한 것은 키가 없는 경우를 처리하기 위한 디폴트 값 훅뿐입니다.
### 기억해야 할 내용
- 파이썬의 여러 컴포넌트 사이에 간단한 인터페이스가 필요할 때는 클래스를 정의하고 인스턴스화하는 대신 간단히 함수를 사용할 수있다.
- 파이썬 함수나 메서드는 일급 시민이다. 따라서 (다른 타입의 값과 마찬가지로) 함수나 함수 참조를 식에 사용할 수 있다.
- __call__ 특별 메서드를 사용하면 클래스의 인스턴스인 객체를 일반 파이썬 함수처럼 호출할 수 있다.
- 상태를 유지하기 위한 함수가 필요한 경우에는 상태가 있는 클로저를 정의하는 대신 __call__ 메서드가 있는 클래스를 정의할지 고려해보자.
'파이썬 코딩의 기술' 카테고리의 다른 글
파이썬코딩의기술 - BW51 : 합성 가능한 클래스 확장이 필요하면 메타클래스보다는 클래스 데코레이터를 사용하라 (0) | 2023.03.17 |
---|---|
파이썬코딩의 기술 - BW50 : __set_name__으로 클래스 애트리뷰트를 표시하라 (0) | 2023.03.17 |
파이썬코딩의 기술 - BW 37: 내장 타입을 여러 단계로 내포시키보다는 클래스를 합성하라 (0) | 2023.03.04 |
파이썬코딩의기술 - BW36 : 이터레이터나 제너레이터를 다룰 때는 itertools를 사용하라 (0) | 2023.02.28 |
파이썬 코딩의 기술 - BW35 : 제너레이터 안에서 throw 상태를 변화시키지 마라 (0) | 2023.02.28 |