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

김승환·2021년 7월 11일

코딩의 기술

목록 보기
28/36

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

  • 파이썬 내장 API중 상당수는 함수를 전달해서 동작을 원하는 대로 바꿀 수 있게 해준다.
  • API가 실행되는 과정에서 여러분이 전달한 함수를 실행하는 경우 이런함수를 훅이라고 부른다.

예를 들어

  • 리스트 타입의 sort메서드는 정렬 시 각 인덱스에 대응하는 비교값을 결정하는 선택적인 key인자를 받을 수 있다.
  • 아래 코드는 key훅으로 len 내장 함수를 전달해서 이름이 들어 있는 리스트를 이름의 길리에 따라 정렬한다.
names = ['소크라테스', '아르키메데스', '플라톤', '아리스토텔레스']
names.sort(key=len)
print(names)

['플라톤', '소크라테스', '아르키메데스', '아리스토텔레스']

  • 훅을 추상 클래스를 통해 정의해야 하는 언어도 있지만, 파이썬에서는 단순히 인자와 반환 값이 잘 정의된 상태가 없는 함수를 훅으로 사용하는 경우가 많다.
  • 함수는 클래스보다 정의하거나 기술하기가 더 쉬우므로 훅으로 사용하기에는 함수가 이상적이다.
  • 파이썬은 함수를 일급 시민 객체로 취급하기 때문에 함수를 훅으로 사용할 수 있다.
  • 함수나 메서드가 일급 시민 객체라는 말은 파이썬 언어에서 사용할 수 있는 다른 일반적인 값과 마친가지로 함수나 메서드를 다른 함수에 넘기거나 변수등으로 참조할 수 있다는 의미다.

예를들어 defaultdict 클래스의 동작을 사용자 정의하고 싶다고 하자.

  • defaultdict에는 딕셔너리 안에 없는 키에 접근할 경우 호출되는 인자가 없는 함수를 전달할 수 있다.
  • 이 함수는 존재하지 않는 키에 해당하는 값이 될 객체를 반환해야 한다.
  • 아래 코드는 존재하지 않는 키에 접근할 때 로그를 남기고 0을 디폴트 값으로 반환한다.
def log_missing():
    print('키 추가됨')
    return 0

원본 딕셔너리와 변경할 내용이 주어진 경우, log_missing 함수는 로그를 두번 남길 수 있다.


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}

log_missing과 같은 함수를 사용할 수 있으면 정해진 동작과 부수 효과를 분리할 수 있기 때문에 API를 더 쉽게 만들 수 있다.

  • 예를 들어 defaultdict에 전달하는 디폴트 값 훅이 존재하지 않는 키에 접근한 총횟수를 세고싶다.
  • 이런 상태를 만드는 법중 하나는 클로저를 사용하는 것이다.
  • 다음 예제는 이런 클로저가 있는 도우미 함수를 디폴트 값 훅으로 사용한다.
    - defaultdict는 missing 훅이 상태를 관리한다는 점을 알지 못하지만, 이 함수를 실행하면 원하는 결과를 볼 수 있다.
    - 인터페이스에서 간단한 함수를 인자로 받으면 클로저 안에 상태를 감추는 기능 계층을 쉽게 추가 할 수 있다.
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
print(result)
print(count)

defaultdict(<function increment_with_report..missing at 0x000002106229B670>, {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9})
2

하지만 상태를 다루기 위한 훅으로 클로저를 사용하면 상태가 없는 함수에 비해 읽고 이해하기 어렵다. 다른 접근방법은 여러분이 추적하고 싶은 상태를 저장하는 작은 클래스를 정의하는 것이다.

class CountMissing:
    def __init__(self):
        self.added = 0

    def missing(self):
        self.added += 1
        return 0
  • 다른 언어에서는 CountMissing이 제공하는 인터페이스를 만족시키기 위해 defaultdict의 코드를 변경해야 할 수도 있다.
  • 하지만 파이썬에서는 일급 함수를 사용해 객체에 대한 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 클래스의 목적이 무엇인지 분명히 알기는 어렵다.
  • 누가 CountMissing 객체를 만들까? 누가 missing 메서드를 호출할까? 이 클래스에 나중에 공개 메서드가 더 추가될 수도 있을까?
  • Defaultdict와 함께 사용하는 예제는 보기 전까지 이 클래스는 수수께끼일 뿐이다.

이런 경우를 더 명확히 표현하기 위해 파이썬에서는 클래스에 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)

다음 코드는 BetterCountMssing 인스턴스를 defaultdict의 디폴트 값 훅으로 사용해서 존재하지 않는 키에 접근한 횟수를 추적한다.

counter = BetterCountMissing()
result = defaultdict(counter, current) # __call__에 의존함
for key, amount in increments:
    result[key] += amount
assert counter.added == 2
  • 이 코드가 CountMissing.missing을 사용한 코드보다 깔끔하다.

  • call메서드는 함수가 인자로 쓰일 수 있는 부분에 이 클래스의 인스턴스를 사용할 수 있다는 사실을 나타낸다.

  • 코드를 처음 읽는 사람도 이 클래스의 동작을 알아보기 위한 시작점이 call이라는 사실을 쉽게 알 수 있다.

  • 이 클래스를 만든 목적이 상태를 저장하는 클로저 역할이라는 사실을 잘 알 수 있다.

  • 무엇보다 가장 좋은 점은 defaultdict가 call 내부에서 어떤 일이 벌어지는지에 대해 알 필요가 없다.

  • defaultdict에게 필요한 것은 키가 없는 경우를 처리하기 위한 디폴트 값 훅 뿐이다.

  • 파이썬은 단순한 함수 인터페이스를 만족시킬 수 있는 여러가지 방법을 제공한다.

profile
인공지능 파이팅!

0개의 댓글