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