{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 15. 딕셔너리 삽입 순서에 의존할 떄는 조심하라"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "파이썬 3.5 이전에는 딕셔너리에 대해 이터레이션을 수행하면 키를 임의의 순서로 돌려줬으며, 이터레이션 순서는 원소가 삽입된 순서와 일치하지 않았다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'cat': 'kitten', 'dog': 'puppy'}\n"
     ]
    }
   ],
   "source": [
    "baby_names = {\n",
    "    'cat': 'kitten',\n",
    "    'dog': 'puppy',\n",
    "}\n",
    "# 파이써 3.5에서는 {'dog': 'puppy', 'cat': 'kitten'}가 출력되지만 3.7 이후에서는 {'cat': 'kitten', 'dog': 'puppy'}\n",
    "print(baby_names)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이러한 이유는 예전 딕셔너리 구현이 내장 hash 함수와 파이썬 인터프리터가 시작할 때 초기화되는 난수 씨드를 사용하는 해시 테이블 알고리즘으로 만들어졌기 때문이다.\n",
    "\n",
    "파이썬 3.6부터 삽입순서를 보존하도록 개선되었다. (3.7 아닌가??) -> 3.7부터 파이썬 언어 명세에 내용이 포함됨"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['cat']\n",
      "['kitten']\n",
      "[('cat', 'kitten')]\n",
      "('cat', 'kitten')\n"
     ]
    }
   ],
   "source": [
    "print(list(baby_names.keys()))\n",
    "print(list(baby_names.values()))\n",
    "print(list(baby_names.items()))\n",
    "print(baby_names.popitem())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "함수에 대한 키워드 인자는 예전에는 이러한 이슈때문에 뒤죽박죽 처럼 보였다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def my_func(**kwargs):\n",
    "    for key, value in kwargs.items():\n",
    "        print(f'{key} = {value}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "goose = gosling\n",
      "kangaroo = joey\n"
     ]
    }
   ],
   "source": [
    "my_func(goose='gosling', kangaroo='joey')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyClass:\n",
    "    def __init__(self):\n",
    "        self.alligator = 'hatchling'\n",
    "        self.elephant = 'calf'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "alligator = hatchling\n",
      "elephant = calf\n"
     ]
    }
   ],
   "source": [
    "a = MyClass()\n",
    "for key, value in a.__dict__.items():\n",
    "    print(f'{key} = {value}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "collections에 포함되어 있는 OrderDict은 dict과는 특성이 조금 다름.\n",
    "\n",
    "popitem 호출을 많이 한다면, OrderDict이 더 나을 수 있다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "파이썬에서는 list, dict 등의 프로토콜을 흉내내는 커스텀 컨테이너 타입을 정의할 수 있다.\n",
    "\n",
    "대부분의 경우 엄격한 클래스 계층보다 객체의 동작이 객체의 실질적인 타입을 결정하는 **덕 타이핑**에 의존하며, 이로인해 함정에 빠질 수 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "votes = {\n",
    "    'otter': 1281,\n",
    "    'polar bear': 587,\n",
    "    'fox': 863,\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "def populate_ranks(votes, ranks):\n",
    "    names = list(votes.keys())\n",
    "    names.sort(key=votes.get, reverse=True)\n",
    "    for i, name in enumerate(names, 1):\n",
    "        ranks[name] = i"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_winner(ranks):\n",
    "    return next(iter(ranks))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'otter': 1, 'fox': 2, 'polar bear': 3}\n",
      "otter\n"
     ]
    }
   ],
   "source": [
    "ranks = {}\n",
    "populate_ranks(votes, ranks)\n",
    "print(ranks)\n",
    "winner = get_winner(ranks)\n",
    "print(winner)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "만약 등수가 아닌 알파벳 순으로 표시해야 할때, collections.abc 모듈을 사용해 딕셔너리와 비슷하지만 알파벳 순서대로 이터레이션 해주는 클래스를 새로 정의할 수 있다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections.abc import MutableMapping"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SortedDict(MutableMapping):\n",
    "    def __init__(self):\n",
    "        self.data = {}\n",
    "\n",
    "    def __getitem__(self, key):\n",
    "        return self.data[key]\n",
    "\n",
    "    def __setitem__(self, key, value):\n",
    "        self.data[key] = value\n",
    "\n",
    "    def __delitem__(self, key):\n",
    "        del self.data[key]\n",
    "\n",
    "    def __iter__(self):\n",
    "        keys = list(self.data.keys())\n",
    "        keys.sort()\n",
    "        for key in keys:\n",
    "            yield key\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'otter': 1, 'fox': 2, 'polar bear': 3}\n",
      "fox\n"
     ]
    }
   ],
   "source": [
    "sorted_ranks = SortedDict()\n",
    "populate_ranks(votes, sorted_ranks)\n",
    "print(sorted_ranks.data)\n",
    "winner = get_winner(sorted_ranks)\n",
    "print(winner)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "fox 가 출력되므로 요구사항에 맞지 않다.\n",
    "\n",
    "해결 방법\n",
    "\n",
    "1. ranks 딕셔너리가 어떤 특정 순서로 이터레이션된다고 가정하지 않고 get_winner 함수를 구현하는 것"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_winner(ranks):\n",
    "    for name, rank in ranks.items():\n",
    "        if rank == 1:\n",
    "            return name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "otter\n"
     ]
    }
   ],
   "source": [
    "winner = get_winner(sorted_ranks)\n",
    "print(winner)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2. 함수 맨 앞에 ranks의 타입이 우리가 원하는 타입인지 검사하는 코드를 추가"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_winner(ranks):\n",
    "    if not isinstance(ranks, dict):\n",
    "        raise TypeError('dict 인스턴스가 필요합니다')\n",
    "    return next(iter(ranks))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "dict 인스턴스가 필요합니다",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-19-19075fd8aa1c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_winner\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msorted_ranks\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-18-5d795183d0cc>\u001b[0m in \u001b[0;36mget_winner\u001b[0;34m(ranks)\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget_winner\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mranks\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[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mranks\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\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'dict 인스턴스가 필요합니다'\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;32mreturn\u001b[0m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mranks\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;31mTypeError\u001b[0m: dict 인스턴스가 필요합니다"
     ]
    }
   ],
   "source": [
    "get_winner(sorted_ranks)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3. 타입 annotation을 사용해서 get_winner에 전달 되는 값이 딕셔너리와 비슷한 동작을 하는 MutableMapping이 아니라 dict 인스턴스가 되도록 강제하는 것"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Dict, MutableMapping"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "def populate_ranks(votes: Dict[str, int],\n",
    "                   ranks: Dict[str, int]) -> None:\n",
    "    names = list(votes.keys())\n",
    "    names.sort(key=votes.get, reverse=True)\n",
    "    for i, name in enumerate(names, 1):\n",
    "        ranks[name] = i"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_winner(ranks: Dict[str, int]) -> str:\n",
    "    return next(iter(ranks))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "class SortedDict(MutableMapping[str, int]):\n",
    "    def __init__(self):\n",
    "        self.data = {}\n",
    "\n",
    "    def __getitem__(self, key):\n",
    "        return self.data[key]\n",
    "\n",
    "    def __setitem__(self, key, value):\n",
    "        self.data[key] = value\n",
    "\n",
    "    def __delitem__(self, key):\n",
    "        del self.data[key]\n",
    "\n",
    "    def __iter__(self):\n",
    "        keys = list(self.data.keys())\n",
    "        keys.sort()\n",
    "        for key in keys:\n",
    "            yield key\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'otter': 1, 'fox': 2, 'polar bear': 3}\n",
      "fox\n"
     ]
    }
   ],
   "source": [
    "sorted_ranks = SortedDict()\n",
    "populate_ranks(votes, sorted_ranks)\n",
    "print(sorted_ranks.data)\n",
    "winner = get_winner(sorted_ranks)\n",
    "print(winner)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Looking in indexes: http://mirror.kakao.com/pypi/simple\n",
      "Collecting mypy\n",
      "  Downloading http://mirror.kakao.com/pypi/packages/57/2b/e619ca2c36cd5be7c37cc4b4a6e5d2c90c874a384e6ff86c5e898a02412c/mypy-0.790-cp38-cp38-manylinux1_x86_64.whl (22.0 MB)\n",
      "\u001b[K     |████████████████████████████████| 22.0 MB 12.3 MB/s eta 0:00:01\n",
      "\u001b[?25hCollecting mypy-extensions<0.5.0,>=0.4.3\n",
      "  Downloading http://mirror.kakao.com/pypi/packages/5c/eb/975c7c080f3223a5cdaff09612f3a5221e4ba534f7039db34c35d95fa6a5/mypy_extensions-0.4.3-py2.py3-none-any.whl (4.5 kB)\n",
      "Collecting typed-ast<1.5.0,>=1.4.0\n",
      "  Downloading http://mirror.kakao.com/pypi/packages/77/49/51308e8c529e14bb2399ff6d22998583aa9ae189ec191e6f7cbb4679f7d5/typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl (768 kB)\n",
      "\u001b[K     |████████████████████████████████| 768 kB 15.7 MB/s eta 0:00:01\n",
      "\u001b[?25hCollecting typing-extensions>=3.7.4\n",
      "  Downloading http://mirror.kakao.com/pypi/packages/60/7a/e881b5abb54db0e6e671ab088d079c57ce54e8a01a3ca443f561ccadb37e/typing_extensions-3.7.4.3-py3-none-any.whl (22 kB)\n",
      "Installing collected packages: mypy-extensions, typed-ast, typing-extensions, mypy\n",
      "Successfully installed mypy-0.790 mypy-extensions-0.4.3 typed-ast-1.4.1 typing-extensions-3.7.4.3\n"
     ]
    }
   ],
   "source": [
    "!pip install mypy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dict_test.py:6: \u001b[1m\u001b[31merror:\u001b[m Argument \u001b[m\u001b[1m\"key\"\u001b[m to \u001b[m\u001b[1m\"sort\"\u001b[m of \u001b[m\u001b[1m\"list\"\u001b[m has incompatible type overloaded function; expected \u001b[m\u001b[1m\"Callable[[str], _SupportsLessThan]\"\u001b[m\u001b[m\n",
      "dict_test.py:16: \u001b[1m\u001b[31merror:\u001b[m Function is missing a return type annotation\u001b[m\n",
      "dict_test.py:16: \u001b[34mnote:\u001b[m Use \"-> None\" if function does not return a value\n",
      "dict_test.py:19: \u001b[1m\u001b[31merror:\u001b[m Function is missing a type annotation\u001b[m\n",
      "dict_test.py:22: \u001b[1m\u001b[31merror:\u001b[m Function is missing a type annotation\u001b[m\n",
      "dict_test.py:25: \u001b[1m\u001b[31merror:\u001b[m Function is missing a type annotation\u001b[m\n",
      "dict_test.py:28: \u001b[1m\u001b[31merror:\u001b[m Function is missing a return type annotation\u001b[m\n",
      "dict_test.py:34: \u001b[1m\u001b[31merror:\u001b[m Function is missing a return type annotation\u001b[m\n",
      "dict_test.py:43: \u001b[1m\u001b[31merror:\u001b[m Call to untyped function \u001b[m\u001b[1m\"SortedDict\"\u001b[m in typed context\u001b[m\n",
      "dict_test.py:44: \u001b[1m\u001b[31merror:\u001b[m Argument 2 to \u001b[m\u001b[1m\"populate_ranks\"\u001b[m has incompatible type \u001b[m\u001b[1m\"SortedDict\"\u001b[m; expected \u001b[m\u001b[1m\"Dict[str, int]\"\u001b[m\u001b[m\n",
      "dict_test.py:46: \u001b[1m\u001b[31merror:\u001b[m Argument 1 to \u001b[m\u001b[1m\"get_winner\"\u001b[m has incompatible type \u001b[m\u001b[1m\"SortedDict\"\u001b[m; expected \u001b[m\u001b[1m\"Dict[str, int]\"\u001b[m\u001b[m\n",
      "\u001b[1m\u001b[31mFound 10 errors in 1 file (checked 1 source file)\u001b[m\n"
     ]
    }
   ],
   "source": [
    "!python3 -m mypy --strict dict_test.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 기억해야 할 내용\n",
    "- 파이썬 3.7부터는 dict 인스턴스에 들어 있는 내용을 이터레이션할 때 키를 삽입한 순서대로 돌려받는다는 사실에 의존할 수 있다.\n",
    "- 파이썬은 dict는 아니지만 딕셔너리와 비슷한 객체를 쉽게 만들 수 있게 해준다. 이런 타입의 경우 키 삽입 순서가 그대로 보존된다고 가정할 수 없다.\n",
    "- 딕셔너리와 비슷한 클래스를 조심스럽게 다루는 방법으로 dict 인스턴스의 삽입 순서 보존에 의존하지 않고 코드를 작성하는 방법, 실행 시점에 명시적으로 dict 타입을 검사하는 방법, 타입 애너테이션과 정적분석을 사용해 dict 값을 요구하는 방법이 있다."
   ]
  }
 ],
 "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
}