hook, __call__

chromis·2022년 3월 7일
0

Python

목록 보기
3/3

Better way 38

간단한 인터페이스의 경우 클래스 대신 함수를 받아라

3줄 요약

  1. 파이썬에서 컴포넌트 사이의 간단한 인터페이스용으로 클래스를 정의하고 인스턴스를 생성하는 대신에 함수
  2. 파이썬에서 함수와 메서드에 대한 참조는 일급 시민 객체이기에, 함수 참조 식으로 사용할 수 있다.
  3. __call__ 메서드로 클래스의 인스턴스를 일반 파이썬 함수처럼 호출할 수 있게 해준다.

훅 (hook)

  • 파이썬 내장 API중 전달한 함수를 실행하는 함수
# ex) sort() : key 훅으로 len 내장 함수를 전달
# key 후크로 넘겨서 이름 리스트를 길이로 정렬

names = ['소크라테스', '아르키메데스', '플라톤', '아리스토텔레스']
names.sort(key=len)
print(names)

>>
['플라톤', '소크라테스', '아르키메데스', '아리스토텔레스']
  • 인자와 반환 값이 잘 정의된, 상태가 없는 함수를 훅으로 사용 (정의하기 쉬움)
  • 함수는 일급 시민 객체로 취급하기 때문에 다른 함수나 메서드에 넘기거나 변수로 참조 가능
  • 부수적인 동작을 쉽게 관리 가능
# 훅 : 키를 찾을 수 없을 때마다 로그를 남기고 기본값으로 0을 반환
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)
assert count == 2

이 함수의 문제는, 상태가 없는 함수의 예제보다 이해하기 어렵다는 점.

  • 해결책 : 여기서, 캡슐화를 통해 더욱 작게 쪼개보자
class CountMissing(object):
    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

하지만 여기서도 문제, CountMissing이 어디에 쓰일지, missing이라고 명명됐기에, 용도를 파악하기 어려움

  • 해결책 : 아예, __call__로 명확하게 정의하자
class BetterCountMissing(object):
    def __init__(self):
        self.added = 0

    def __call__(self):
        self.added += 1
        return 0

counter = BetterCountMissing()
counter()
assert callable(counter)

counter = BetterCountMissing()
result = defaultdict(counter, current) # __call__ 사용
for key, amount in increments:
    result[key] += amount
assert counter.added == 2
  • __call__ 메서드는 (API 후크처럼) 함수 인수를 사용하기 적합한 위치에 클래스의 인스턴스를 사용할 수 있다는 사실을 드러낸다.
  • 이 코드를 처음 보는 사람을 클래스의 주요 동작을 책임지는 진입점(entry point)으로 안내하는 역할도 한다.
  • 클래스의 목적이 상태 보존 클로저로 동작하는 것이군! 확인 가능

0개의 댓글