파이썬 내장 API 중 상당수는 함수를 전달해서 동작을 원하는 대로 바꿀 수 있게 해준다.
전달한 함수를 실행 하는 경우 이런 함수를 훅(hook)이라고 부른다.
다음 코드는 key 훅으로 len 내장 함수를 전달해서 이름이 들어 있는 리스트를 이름의 길이에 따라 정렬한다.
names = ['소크라테스', '아르키메데스', '플라톤', '아리스토텔레스']
names.sort(key=len)
print(names)
['플라톤', '소크라테스', '아르키메데스', '아리스토텔레스']
함수는 클래스보다 정의하거나 기술하기가 더 쉬우므로 훅으로 사용하기에는 함수가 이상적이다.
또한, 파이썬은 함수를 일급 시민 객체로 취급하기 때문에 함수를 훅으로 사용할 수 있다.
예를 들어 defaultdict 클래스의 동작을 사용자 정의하고 싶다고 하자.
def log_missing():
print('키 추가됨')
return 0
from collections 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}
상태가 있는 클로저를 사용할 수 있다.
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)
print(result)
print(count)
defaultdict(<function increment_with_report.<locals>.missing at 0x1156150d0>, {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9}) 2
하지만 상태를 다루기 위한 훅으로 클로저를 사용하면 상태가 없는 함수에 비해 읽고 이해하기 어렵다.
다른 접근 방법은 여러분이 추적하고 싶은 상태를 저장하는 작은 클래스를 정의하는 것이다.
class CountMissing:
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
counter = CountMissing()
result = defaultdict(counter.missing, current)
for key, amount in increments:
result[key] += amount
assert counter.added == 2
print(counter.added)
print(result)
2 defaultdict(<bound method CountMissing.missing of <__main__.CountMissing object at 0x11540fe20>>, {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9})
도우미 클래스로 상태가 있는 클로저와 같은 동작을 제공하는 것이 increment_with_Report 같은 함수를 사용하는 것보다 더 깔끔하다.
하지만 클래스의 목적이 무엇인지 분명히 알기는 어렵다.
이런 경우를 더 명확히 표현하기 위해 파이썬에서는 클래스에 call 특별 메서드를 정의할 수 있다.
__call__을 사용하면 객체를 함수처럼 호출할 수 있다.
그리고 __call__이 정의된 클래스의 인스턴스에 대해 callable 내장 함수를 호출하면, 다른 일반 함수나 메서드와 마찬가지로 True가 반환된다.
이런 방식으로 정의돼서 호출될 수 있는 모든 객체를 호출가능 객체라고 부른다.
class BetterCountMissing:
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
print(counter())
callable(counter)
0
True
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
result[key] += amount
print(counter.added)
2
이 코드가 가장 깔끔하다. 이 클래스의 인스턴스를 함수처럼 사용할 수 있다.