본문 바로가기

파이썬 코딩의 기술

파이썬코딩의 기술 - BW38 : 간단한 인터페이스의 경우 클래스 대신 함수를 받아라

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__ 메서드가 있는 클래스를 정의할지 고려해보자.