{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 31. 인자에 대해 이터레이션할 떄는 방어적이 돼라"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize(numbers):\n",
    "    total = sum(numbers)\n",
    "    result = []\n",
    "    for value in numbers:\n",
    "        percent = 100 * value / total\n",
    "        result.append(percent)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[11.538461538461538, 26.923076923076923, 61.53846153846154]\n"
     ]
    }
   ],
   "source": [
    "visits = [15, 35, 80]\n",
    "percentages = normalize(visits)\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "assert sum(percentages) == 100.0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def read_visits(data_path):\n",
    "    with open(data_path) as f:\n",
    "        for line in f:\n",
    "            yield int(line)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[]\n"
     ]
    }
   ],
   "source": [
    "it = read_visits('my_numbers.txt')\n",
    "percentages = normalize(it)\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이런 현상이 일어난 이유는 이터레이터가 결과를 단 한번만 만들어내기 때문이다"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "it = read_visits('my_numbers.txt')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[15, 35, 80]\n"
     ]
    }
   ],
   "source": [
    "print(list(it))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[]\n"
     ]
    }
   ],
   "source": [
    "print(list(it))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이런 경우 이터레이터와 이미 소진돼버린 것을 구분하기 어렵다.\n",
    "\n",
    "이 문제를 해결하기 위해 입력 이터레이터를 명시적으로 소진시키고 이터레이터의 전체 내용을 리스트에 넣을 수 있다.\n",
    "\n",
    "그 후 데이터를 담아둔 리스트에 대해 원하는 수만큼 이터레이션을 수행할 수 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize_copy(numbers):\n",
    "    numbers_copy = list(numbers) # 이터레이션 복사\n",
    "    total = sum(numbers_copy)\n",
    "    \n",
    "    result = []\n",
    "    \n",
    "    for value in numbers_copy:\n",
    "        percent = 100 * value / total\n",
    "        result.append(percent)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[11.538461538461538, 26.923076923076923, 61.53846153846154]\n"
     ]
    }
   ],
   "source": [
    "it = read_visits('my_numbers.txt')\n",
    "percentages = normalize_copy(it)\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "하지만 이러한 방식은 이터레이터의 내용을 복사하여 메모리를 많이 사용하게 된다.\n",
    "\n",
    "이 문제를 해결하는 다른방법은 호출될 떄마다 새로 이터레이터를 반환하는 함수를 받는 것이다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize_func(get_iter):\n",
    "    total = sum(get_iter())\n",
    "    \n",
    "    result = []\n",
    "    \n",
    "    for value in get_iter():\n",
    "        percent = 100 * value / total\n",
    "        result.append(percent)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[11.538461538461538, 26.923076923076923, 61.53846153846154]\n"
     ]
    }
   ],
   "source": [
    "path = 'my_numbers.txt'\n",
    "percentages = normalize_func(lambda: read_visits(path))\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "하지만 람다함수를 넘기는 것은 보기에 좋지 않다.\n",
    "\n",
    "같은 결과를 달성하는 더 나은 방법은 이터레이터 프로토콜을 구현한 새로운 컨테이너 클래스를 제공하는 것이다.\n",
    "\n",
    "이터레이터 프로토콜은 파이썬의 for 루프나 그와 연관된 식들이 컨테이너 타입의 내용을 방문할 때 사용하는 절차다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ReadVisits:\n",
    "    def __init__(self, data_path):\n",
    "        self.data_path = data_path\n",
    "        \n",
    "    def __iter__(self):\n",
    "        with open(self.data_path) as f:\n",
    "            for line in f:\n",
    "                yield int(line)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[11.538461538461538, 26.923076923076923, 61.53846153846154]\n"
     ]
    }
   ],
   "source": [
    "visits = ReadVisits(path)\n",
    "percentages = normalize(visits)\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이 코드가 정삭 작동 하는 이유는 normalizer 함수 안의 sum 메서드와 for 루프가 __iter__ 를 호출해서 각각 객체를 만들기 때문이다.\n",
    "\n",
    "이 접근법의 유일한 단점은 입력 데이터를 여러번 읽는다는 것이다.\n",
    "\n",
    "아래는 반복적으로 이터레이션할 수 없는 인자인 경우에는 TypeError를 발생시켜줄 수 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize_defensive(numbers):\n",
    "    if iter(numbers) is numbers:  # 이터레이터 -- 나쁨!\n",
    "        raise TypeError('컨테이너를 제공해야 합니다')\n",
    "    \n",
    "    total = sum(numbers)\n",
    "    result = []\n",
    "    for value in numbers:\n",
    "        percent = 100 * value / total\n",
    "        result.append(percent)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[16.666666666666668, 33.333333333333336, 50.0]"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "normalize_defensive([1,2,3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다른 대안으로 collections.abc 내장 모듈은 isinstance를 사용해 잠재적인 문제를 검사할 수 있는 Iterator 클래스를 제공한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections.abc import Iterator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "def normalize_defensive(numbers):\n",
    "    if isinstance(numbers, Iterator): \n",
    "        raise TypeError('컨테이너를 제공해야 합니다')\n",
    "    \n",
    "    total = sum(numbers)\n",
    "    result = []\n",
    "    for value in numbers:\n",
    "        percent = 100 * value / total\n",
    "        result.append(percent)\n",
    "    return result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[11.538461538461538, 26.923076923076923, 61.53846153846154]\n"
     ]
    }
   ],
   "source": [
    "visits = [15, 35, 80]\n",
    "percentages = normalize_defensive(visits)\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[11.538461538461538, 26.923076923076923, 61.53846153846154]\n"
     ]
    }
   ],
   "source": [
    "visits = ReadVisits(path)\n",
    "percentages = normalize_defensive(visits)\n",
    "print(percentages)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "컨테이너를 제공해야 합니다",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-24-fc30947d22fc>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0mvisits\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m15\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m35\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m80\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[0mit\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0miter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvisits\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mpercentages\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnormalize_defensive\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;32m<ipython-input-20-6a9b815bc910>\u001b[0m in \u001b[0;36mnormalize_defensive\u001b[0;34m(numbers)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mnormalize_defensive\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumbers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m     \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumbers\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mIterator\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m         \u001b[0;32mraise\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'컨테이너를 제공해야 합니다'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m     \u001b[0mtotal\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumbers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mTypeError\u001b[0m: 컨테이너를 제공해야 합니다"
     ]
    }
   ],
   "source": [
    "visits = [15, 35, 80]\n",
    "it = iter(visits)\n",
    "percentages = normalize_defensive(it)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 기억해야 할 내용\n",
    "- 입력 인자를 여러 번 이터레이션하는 함수나 메서드를 조심하라. 입력받은 인자가 이터레이터면 함수가 이상하게 작동하거나 결과가 없을 수 있다.\n",
    "- 파이썬의 이터레이터 프로토콜은 컨테이너와 이터레이터가 iter, next 내장 함수나 for 루프 등의 관련 식과 상호작용하는 절차를 정의한다.\n",
    "- __iter__ 메서드를 제너레이터로 정의하면 쉽게 이터러블 컨테이너 타입을 정의할 수 있다.\n",
    "- 어떤 값이 이터레이터인지 감지하려면, 이 값을 iter 내장 함수에 넘겨서 반환되는 값이 원래 값과 같은지 확인하면 된다. 다른 방법으로 collections.abc.Iterator 클래스를 isintance와 함께 사용할 수 있다."
   ]
  }
 ],
 "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.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}