ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 인터페이스가 간단하면 클래스 대신 함수
    언어/파이썬 & 장고 2016. 10. 22. 19:21

    파이썬 내장 API의 상당수는 함수를 넘겨서 동작을 사용자화하는 기능이 있습니다. API는 이런 후크(hook)를 이용해서 사용자가 작성한 코드를 실행 중에 호출합니다. 다음 예제는 list 타입의 sort 메서드는 정렬에 필요한 각 인덱스의 값을 결정하는 선택적인 key 인수를 받습니다. lambda 표현식을 key 후크로 넘겨서 이름 리스트를 길이로 정렬합니다.

    names = ['aocrates', 'archimedes', 'plato', 'aristotle']
    names.sort(key=lambda x: len(x))
    print(names)
    
    # 결과
    # ['plato', 'aocrates', 'aristotle', 'archimedes']


    다른 언어에서라면 후크를 추상 클래스로 정의할 것이라고 예상할 수도 있습니다. 하지만 파이썬의 후크 중 상당수는 인수와 반환 값을 잘 정의해놓은 단순히 상태가 없는 함수입니다. 함수는 클래스보다 설명하기 쉽고 정의하기도 간단해서 후크로 쓰기에 이상적입니다. 함수가 후크로 동작하는 이유는 파이썬이 일급 함수(first-class function)를 갖췄기 때문입니다. 다시 말해, 언어에서 함수와 메서드를 다른 값처럼 전달하고 참조할 수 있기 때문입니다.

    함수(function)와 메서드(method)의 차이


    함수

    • 절차지향에서 class 로 사용하지 않고 def 로 함수 선언하여 사용하는 것. (print(), type() 과 같은 형태)

    메소드

    • 객체지향에서 class를 사용하고 인스턴스를 만들었을 경우, class 내 함수를 사용할 때 이를 메소드라 부름. (list.append(...), list.extend(...) 와 같은 형태)


    예로 defaultdict 클래스의 동작을 사용화한다고 합니다. (defaultdict()는 dictionary 에 기본값을 정의하고 키 값이 없더라도 에러를 출력하지 않고 기본값을 출력합니다. 그리고, dict.set_default를 사용하는 것보다 빠른 것이 장점입니다.) 이 자료 구조는 찾을 수 없는 키에 접근할 때마다 호출될 함수를 받습니다. defaultdict에 넘길 함수는 딕셔너리에서 찾을 수 없는 키에 대응할 기본값을 반환해야 합니다. 다음은 키를 찾을 수 없을 때마다 로그를 남기고 기본값으로 0을 반환하는 후크를 정의한 코드입니다.


    from collections import defaultdict
    
    
    def log_missing():
        print('key added')
        return 0
    
    
    current = {'green': 12, 'blue': 3}
    increments = [
        ('red', 5),
        ('blue', 17),
        ('orange', 9),
    ]
    
    result = defaultdict(log_missing, current)
    print('beforeL ', dict(result))
    for key, amount in increments:
        result[key] += amount
    print('after: ', dict(result))
    
    # 결과
    # beforeL  {'green': 12, 'blue': 3}
    # key added
    # key added
    # after:  {'green': 12, 'blue': 20, 'orange': 9, 'red': 5}

    초깃값을 담은 딕셔너리와 원하는 증가 값 리스트로 log_missing 함수를 두번 (각 red와 orange일 때) 실행하여 로그를 출력했습니다.


    log_missing 같은 함수를 넘기면 결정 동작과 부작용을 분리하므로 API를 쉽게 구축하고 테스트할 수 있습니다. 예를 들어 기본값 후크를 defaultdict에 넘겨서 찾을 수 없는 키의 총 개수를 센다고 가정합니다. 이렇게 만드는 한 가지 방법은 상태 보존 클로저를 사용하는 것입니다. 다음은 상태 보존 클로저를 기본값 후크로 사용하는 헬퍼 함수입니다.

    from collections import defaultdict
    
    current = {'green': 12, 'blue': 3}
    increments = [
        ('red', 5),
        ('blue', 17),
        ('orange', 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, count)
    assert count == 2

    defaultdict는 missing 후크가 상태를 유지한다는 사실을 모르지만, increment_with_report 함수를 실행하면 튜플의 요소로 기대한 갯수인 2를 얻습니다. 이는 간단한 함수를 인터페이스용으로 사용할 때 얻을 수 있는 또 다른 이점입니다. 클로저 안에 상태를 숨기면 나중에 기능을 추가하기도 쉽습니다. 


    상태 보존 후크용으로 클로저를 정의할 때 생기는 문제는 상태가 없는 함수의 예제보다 어렵다는 점입니다. 또 다른 방법은 보존할 상태를 캡슐화하는 작은 클래스로 정의하는 것입니다.

    from collections import defaultdict
    
    current = {'green': 12, 'blue': 3}
    increments = [
        ('red', 5),
        ('blue', 17),
        ('orange', 9),
    ]
    
    
    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의 인터페이스를 수용하도록 defaultdict를 수정해야 한다고 생각할 것입니다. 하지만 파이썬에서는 일급 함수 덕분에 객체로 CountMissing.missing 메서드를 직접 참조해서 defaultdict의 기본값 후크로 넘길 수 있습니다. 메서드가 함수 인터페이스를 충족하는 건 자명합니다.


    헬퍼클래스로 상태 보존 클로저의 동작을 제공하는 방법이 앞에서 increment_with_report 함수를 사용한 방법보다 명확합니다. 그러나 CountMissig 클래스 자체만으로는 용도가 무엇인지 바로 이해하기 어렵습니다. 누가 CountMissing 객체를 생성하는지, 누가 missing 메서드를 호출하는지, 나중에 다른 공개 메서드를 클래스에 추가할 일이 있는지 defaultdict와 연계해서 사용한 예를 보기 전까지는 이 클래스가 수수께끼로 남습니다.


    파이썬에서는 클래스에 __call__이라는 특별한 메서드를 정의해서 이런 상황을 명확하게 할 수 있습니다. __call__메서드는 객체를 함수처럼 호출할 수 있게 해줍니다. 또한 내장 함수 callable이 이런 인스턴스에 대해서는 True를 반환하게 만듭니다.

    from collections import defaultdict
    
    current = {'green': 12, 'blue': 3}
    increments = [
        ('red', 5),
        ('blue', 17),
        ('orange', 9),
    ]
    
    
    class BetterCountMissing(object):
        def __init__(self):
            self.added = 0
    
        def __call__(self):
            self.added += 1
            return 0
    
    
    counter = BetterCountMissing()
    counter()
    assert callable(counter)


    다음은 BetterCountMissing 인스턴스를 defaultdict의 기본값 후크로 사용하여 딕셔너리에 없어서 새로 추가된 키의 개수를 알아내는 코드입니다.

    from collections import defaultdict
    
    current = {'green': 12, 'blue': 3}
    increments = [
        ('red', 5),
        ('blue', 17),
        ('orange', 9),
    ]
    
    
    class BetterCountMissing(object):
        def __init__(self):
            self.added = 0
    
        def __call__(self):
            self.added += 1
            return 0
    
    
    counter = BetterCountMissing()
    result = defaultdict(counter, current) # __call__이 필요함
    
    for key, amount in increments:
        result[key] += amount
    
    assert counter.added == 2

    이 예제가 CountMissing.missing 예제보다 명확합니다. __call__ 메서드는 (API 후크처럼) 함수 인수를 사용하기 적합한 위치에 클래스의 인스턴스를 사용할 수 있다는 사실을 드러냅니다. 이 코드를 처음 보는 사람을 클래스의 주요 동작을 책임지는 진입점(entry point)으로 안내하는 역할도 합니다. 클래스의 목적이 상태 보존 클로저로 동작하는 것이라는 강력한 힌트를 제공합니다.


    무엇보다도 __call__을 사용할 때 defaultdict는 여전히 무슨 일이 일어나는지 모릅니다. defaultdict에 필요한 건 기본값 후크용 함수뿐입니다. 파이썬은 하고자 하는 작업에 따라 간단한 함수 인터페이스를 충족하는 다양한 방법을 제공합니다.

    요약

    파이썬에서 컴포넌트 사이의 간단한 인터페이스용으로 클래스를 정의하고 인스턴스를 생성하는 대신 함수만 써도 종종 충분

    파이썬에서 함수와 메서드에 대한 참조는 일급. 즉, 다른 타입처럼 표현식에서 사용할 수 있음

    __call__이라는 특별한 메서드는 클래스의 인스턴스를 일반 파이썬 함수처럼 호출할 수 있게 해줌

    상태를 보존하는 함수가 필요할 때 상태 보존 클로저를 정의하는 대신 __call__메서드를 제공하는 클래스를 정의하는 방안을 고려


    댓글