{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## 38. 간단한 인터페이스의 경우 클래스 대신 함수를 받아라" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "파이썬 내장 API 중 상당수는 함수를 전달해서 동작을 원하는 대로 바꿀 수 있게 해준다.\n", "\n", "전달한 함수를 실행 하는 경우 이런 함수를 훅(hook)이라고 부른다.\n", "\n", "다음 코드는 key 훅으로 len 내장 함수를 전달해서 이름이 들어 있는 리스트를 이름의 길이에 따라 정렬한다." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['플라톤', '소크라테스', '아르키메데스', '아리스토텔레스']\n" ] } ], "source": [ "names = ['소크라테스', '아르키메데스', '플라톤', '아리스토텔레스']\n", "names.sort(key=len)\n", "print(names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "함수는 클래스보다 정의하거나 기술하기가 더 쉬우므로 훅으로 사용하기에는 함수가 이상적이다.\n", "\n", "또한, 파이썬은 함수를 일급 시민 객체로 취급하기 때문에 함수를 훅으로 사용할 수 있다.\n", "\n", "예를 들어 defaultdict 클래스의 동작을 사용자 정의하고 싶다고 하자." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def log_missing():\n", " print('키 추가됨')\n", " return 0" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from collections import defaultdict" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "이전: {'초록': 12, '파랑': 3}\n", "키 추가됨\n", "키 추가됨\n", "이후: {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9}\n" ] } ], "source": [ "current = {'초록': 12, '파랑':3}\n", "increments = [\n", " ('빨강', 5),\n", " ('파랑', 17),\n", " ('주황', 9),\n", "]\n", "result = defaultdict(log_missing, current)\n", "print('이전:', dict(result))\n", "for key, amount in increments:\n", " result[key] += amount\n", "print('이후:', dict(result))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "상태가 있는 클로저를 사용할 수 있다." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def increment_with_report(current, increments):\n", " added_count = 0\n", " \n", " def missing():\n", " nonlocal added_count # 상태가 있는 클로저\n", " added_count += 1\n", " return 0\n", " \n", " result = defaultdict(missing, current)\n", " for key, amount in increments:\n", " result[key] += amount\n", " \n", " return result, added_count" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "defaultdict(<function increment_with_report.<locals>.missing at 0x1156150d0>, {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9})\n", "2\n" ] } ], "source": [ "result, count = increment_with_report(current, increments)\n", "print(result)\n", "print(count)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "하지만 상태를 다루기 위한 훅으로 클로저를 사용하면 상태가 없는 함수에 비해 읽고 이해하기 어렵다.\n", "\n", "다른 접근 방법은 여러분이 추적하고 싶은 상태를 저장하는 작은 클래스를 정의하는 것이다." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class CountMissing:\n", " def __init__(self):\n", " self.added = 0\n", " \n", " def missing(self):\n", " self.added += 1\n", " return 0" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "counter = CountMissing()\n", "result = defaultdict(counter.missing, current)\n", "for key, amount in increments:\n", " result[key] += amount\n", "assert counter.added == 2" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n", "defaultdict(<bound method CountMissing.missing of <__main__.CountMissing object at 0x11540fe20>>, {'초록': 12, '파랑': 20, '빨강': 5, '주황': 9})\n" ] } ], "source": [ "print(counter.added)\n", "print(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "도우미 클래스로 상태가 있는 클로저와 같은 동작을 제공하는 것이 increment_with_Report 같은 함수를 사용하는 것보다 더 깔끔하다.\n", "\n", "하지만 클래스의 목적이 무엇인지 분명히 알기는 어렵다.\n", "\n", "이런 경우를 더 명확히 표현하기 위해 파이썬에서는 클래스에 __call__ 특별 메서드를 정의할 수 있다.\n", "\n", "__call__을 사용하면 객체를 함수처럼 호출할 수 있다.\n", "\n", "그리고 __call__이 정의된 클래스의 인스턴스에 대해 callable 내장 함수를 호출하면, 다른 일반 함수나 메서드와 마찬가지로 True가 반환된다.\n", "\n", "이런 방식으로 정의돼서 호출될 수 있는 모든 객체를 호출가능 객체라고 부른다." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "class BetterCountMissing:\n", " def __init__(self):\n", " self.added = 0\n", " \n", " def __call__(self):\n", " self.added += 1\n", " return 0" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n" ] }, { "data": { "text/plain": [ "True" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "counter = BetterCountMissing()\n", "print(counter())\n", "callable(counter)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n" ] } ], "source": [ "counter = BetterCountMissing()\n", "result = defaultdict(counter, current)\n", "for key, amount in increments:\n", " result[key] += amount\n", "\n", "print(counter.added)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이 코드가 가장 깔끔하다. 이 클래스의 인스턴스를 함수처럼 사용할 수 있다." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 기억해야 할 내용\n", "- 파이썬의 여러 컴포넌트 사이에 간단한 인터페이스가 필요할 떄는 클래스를 정의하고 인스턴스화하는 대신 간단히 함수를 사용할 수 있다.\n", "- 파이썬 함수나 메서드는 일급 시민이다. 따라서 (다른 타입의 값과 마찬가지로) 함수나 함수 참조를 식에 사용할 수 있다.\n", "- \\_\\_call\\_\\_ 특별 메서드를 사용하면 클래스의 인스턴스인 객체를 일반 파이썬 함수처럼 호출할 수 있다.\n", "- 상태를 유지하기 위한 함수가 필요한 경우에는 상태가 있는 클로저를 정의하는 대신 \\_\\_call\\_\\_ 메서드가 있는 클래스를 정의할지 고려해보라." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5" } }, "nbformat": 4, "nbformat_minor": 4 }