파이썬 코딩의 기술

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

J-Chris 2023. 3. 8. 13:11

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