{
 "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
}