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