{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 45. 애트리뷰트를 리팩터링하는 대신 @property를 사용하라"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "내장 @property 데코레이터를 사용하면, 겉으로는 단순한 애트리뷰트처럼 보이지만 실제로는 지능적인 로직을 수행하는 애트리뷰트를 정의할 수 있다.\n",
    "\n",
    "리키 버킷 (leaky bucket) 흐름 제어 알고리즘을 구현한다고 하자.\n",
    "\n",
    "다음 코드의 bucket 클래스는 남은 가용 용량과 이 가용 용량의 잔존 시간을 표현한다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import datetime, timedelta\n",
    "\n",
    "class Bucket:\n",
    "    def __init__(self, period):\n",
    "        self.period_delta = timedelta(seconds=period)\n",
    "        self.reset_time = datetime.now()\n",
    "        self.quota = 0\n",
    "        \n",
    "        \n",
    "    def __repr__(self):\n",
    "        return f'Bucket(quota={self.quota})'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "리키 버킷 알고리즘은 시간을 일정한 간격으로 구분하고 가용 용량을 소비할 떄 마다 시간을 검사해서 주기가 달라질 경우에는 이전 주기에 미사용한 가용 용량이 새로운 주기로 넘어오지 못하게 막는다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fill(bucket, amount):\n",
    "    now = datetime.now()\n",
    "    if (now - bucket.reset_time) > bucket.period_delta:\n",
    "        bucket.quota = 0\n",
    "        bucket.reset_time = now\n",
    "    bucket.quota += amount"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def deduct(bucket, amount):\n",
    "    now = datetime.now()\n",
    "    if (now - bucket.reset_time) > bucket.period_delta:\n",
    "        return False # 새 주기가 시작됐는데 아직 버킷 할당량이 재설정되지 않았다\n",
    "    if bucket.quota - amount < 0:\n",
    "        return False # 버킷의 가용 용량이 충분하지 못하다\n",
    "    else:\n",
    "        bucket.quota -= amount\n",
    "        return True  # 버킷의 가용 용량이 충분하므로 필요한 분량을 사용한다"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Bucket(quota=100)\n"
     ]
    }
   ],
   "source": [
    "bucket = Bucket(60)\n",
    "fill(bucket, 100)\n",
    "print(bucket)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "가용 용량이 작아서 99 용량을 처리할 수 없음\n",
      "Bucket(quota=100)\n"
     ]
    }
   ],
   "source": [
    "if deduct(bucket, 99):\n",
    "    print('99 용량 사용')\n",
    "else:\n",
    "    print('가용 용량이 작아서 99 용량을 처리할 수 없음')\n",
    "print(bucket)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "가용 용량이 작아서 3 용량을 처리할 수 없음\n",
      "Bucket(quota=100)\n"
     ]
    }
   ],
   "source": [
    "if deduct(bucket, 3):\n",
    "    print('3 용량 사용')\n",
    "else:\n",
    "    print('가용 용량이 작아서 3 용량을 처리할 수 없음')\n",
    "print(bucket)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NewBucket:\n",
    "    def __init__(self, period):\n",
    "        self.period_delta = timedelta(seconds=period)\n",
    "        self.reset_time = datetime.now()\n",
    "        self.max_quota = 0\n",
    "        self.quota_consumed = 0\n",
    "\n",
    "    def __repr__(self):\n",
    "        return (f'NewBucket(max_quota={self.max_quota}, '\n",
    "                f'quota_consumed={self.quota_consumed})')\n",
    "\n",
    "    @property\n",
    "    def quota(self):\n",
    "        return self.max_quota - self.quota_consumed\n",
    "\n",
    "    @quota.setter\n",
    "    def quota(self, amount):\n",
    "        delta = self.max_quota - amount\n",
    "        if amount == 0:\n",
    "            # 새로운 주기가 되고 가용 용량을 재설정하는 경우\n",
    "            self.quota_consumed = 0\n",
    "            self.max_quota = 0\n",
    "        elif delta < 0:\n",
    "            # 새로운 주기가 되고 가용 용량을 추가하는 경우\n",
    "            assert self.quota_consumed == 0\n",
    "            self.max_quota = amount\n",
    "        else:\n",
    "            # 어떤 주기 안에서 가용 용량을 소비하는 경우\n",
    "            assert self.max_quota >= self.quota_consumed\n",
    "            self.quota_consumed += delta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "최초 NewBucket(max_quota=0, quota_consumed=0)\n",
      "보충 후 NewBucket(max_quota=100, quota_consumed=0)\n"
     ]
    }
   ],
   "source": [
    "bucket = NewBucket(60)\n",
    "print('최초', bucket)\n",
    "fill(bucket, 100)\n",
    "print('보충 후', bucket)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "99 용량 사용\n",
      "사용 후 NewBucket(max_quota=100, quota_consumed=99)\n",
      "가용 용량이 작아서 3 용량을 처리할 수 없음\n",
      "여전히 NewBucket(max_quota=100, quota_consumed=99)\n"
     ]
    }
   ],
   "source": [
    "if deduct(bucket, 99):\n",
    "    print('99 용량 사용')\n",
    "else:\n",
    "    print('가용 용량이 작아서 99 용량을 처리할 수 없음')\n",
    "print('사용 후', bucket)\n",
    "\n",
    "if deduct(bucket, 3):\n",
    "    print('3 용량 사용')\n",
    "else:\n",
    "    print('가용 용량이 작아서 3 용량을 처리할 수 없음')\n",
    "\n",
    "print('여전히', bucket)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 기억해야 할 내용\n",
    "- @property를 사용해 기존 인스턴스 애트리뷰트에 새로운 기능을 제공할 수 있다.\n",
    "- @property를 사용해 데이터 모델을 점진적으로 개선하라.\n",
    "- @property 메서드를 너무 과하게 쓰고 있다면, 클래스와 클래스를 사용하는 모든 코드를 리팩터링하는 것을 고려하라."
   ]
  }
 ],
 "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
}