├── .autoenv.leave ├── .autoenv ├── Figures └── MRO.png ├── log.py ├── .vscode └── settings.json ├── README.md ├── Ch7-2_ConcurrencyTest.py ├── .gitignore ├── Ch10_CleanArchitecture.ipynb ├── Ch1_CodeFormattingAndTool.ipynb ├── Ch5_Decorator.md ├── Ch5_Decorator.ipynb ├── Ch3_FeaturesOfGoodCodes.md ├── Ch7_Generator.ipynb ├── Ch7-1_Concurrency.ipynb ├── Ch9_Pattern.ipynb └── Ch6_Descriptor.ipynb /.autoenv.leave: -------------------------------------------------------------------------------- 1 | pyenv deactivate 2 | -------------------------------------------------------------------------------- /.autoenv: -------------------------------------------------------------------------------- 1 | pyenv activate 3.7.6/envs/py37-cleancode 2 | -------------------------------------------------------------------------------- /Figures/MRO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dream2globe/CleanCodeInPython/HEAD/Figures/MRO.png -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.INFO, format="%(message)s") 4 | logger = logging.getLogger(__name__) -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "python.formatting.blackArgs": [ 4 | "--line-length", 5 | "100" 6 | ], 7 | "editor.formatOnSave": true, 8 | "python.pythonPath": "/home/shyeon/.pyenv/versions/py37-cleancode/bin/python", 9 | "python.linting.pylintEnabled": true, 10 | "python.linting.mypyEnabled": true, 11 | "python.linting.enabled": true, 12 | "python.testing.pytestArgs": [ 13 | "." 14 | ], 15 | "python.testing.unittestEnabled": false, 16 | "python.testing.nosetestsEnabled": false, 17 | "python.testing.pytestEnabled": true, 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Code In Python 2 | 모두의연구소 풀잎스쿨 10기 "파이썬, 어디까지 써봐썬" 과정에서 사용하는 주교재 "파이썬 클린코드" 학습내용 정리 3 |

4 | 5 | ## 진행내용 6 | ### 1주차 : 진행자 강성현(퍼실) 7 | + 맴버 간 소개 : 멋진 소개를 기대합니다~! 8 | + 이름, 하는 일이나 관심사, 파이썬을 사용한 경험, 우리 모임을 통해 얻고 싶은 것 9 | + 스터디 방식 결정 10 | + 매 주차 발표자 선정 및 발표 방법 11 | + 산출물 공유 방법 12 | + pull requests(전체 공유), 개인 레포지토리 관리 13 | + Clean Code in Python 1, 2장 14 | 15 | ### 2주차 : 진행자 김경수님 16 | + Clean Code in Python 2장 리뷰, 3장 17 | + 스터디 방식 최종 결정 18 | + 2 Track : Clean Code in Python, Effective Python 19 | + 발표자 20 | + Effective python : 정민정, 윤동희, 이다솜, 서미지, 임한동, 손주영, 정경훈, 김경수 21 | + Clean code in python : 최현상, 이영수, 정민정, 정경훈, 서미지, 김보섭, 손주영 22 | + 퍼실의 중간 리뷰 23 | + 부퍼실 : 정민정님 (이름 외우기 너무 좋아요!!) 24 | 25 | ### 3주차 : 진행자 최현상님, 정민정님 26 | + Clean Code in Python 3장 리뷰, 4장 27 | + Effective Python 1장 28 | 29 | ### 4주차 : 진행자 이영수님, 윤동희님 30 | + Clean Code in Python 5장 31 | + Effective Python 2장 32 | 33 | ### 5주차 : 진행자 정민정님, 이다솜님 (without 퍼실) 34 | + Clean Code in Python 6장 35 | + Effective Python 3장 36 | -------------------------------------------------------------------------------- /Ch7-2_ConcurrencyTest.py: -------------------------------------------------------------------------------- 1 | import time 2 | import functools 3 | 4 | import asyncio 5 | from aiofile import AIOFile, Reader, Writer 6 | 7 | import json 8 | import pandas as pd 9 | from collections import defaultdict 10 | from pathlib import Path 11 | from tqdm import tqdm 12 | 13 | DEFAULT_FMT = "[{elapsed:0.8f}s] {name}({args}, {kwargs}) -> {result}" 14 | path = Path("/home/shyeon/workspace/python/SparkDefinitiveGuide/data/activity-data/") 15 | 16 | 17 | def clock(fmt=DEFAULT_FMT): 18 | def decorate(func): 19 | @functools.wraps(func) 20 | def clocked(*_args, **_kwargs): # clocked에서 *, ** 키워드를 통해 설정된 인수를 변수화 21 | t0 = time.time() 22 | _result = func(*_args) 23 | elapsed = time.time() - t0 24 | name = func.__name__ 25 | args = ", ".join(repr(arg) for arg in _args) 26 | pairs = ["%s=%r" % (k, w) for k, w in sorted(_kwargs.items())] 27 | kwargs = ", ".join(pairs) 28 | result = repr(_result) 29 | print(fmt.format(**locals())) 30 | return _result # clocked()는 데커레이트된 함수를 대체하므로, 원래 함수가 반환하는 값을 반환해야 한다. 31 | 32 | return clocked # decorate()는 clocked()를 반환한다. 33 | 34 | return decorate # clock()은 decorate()를 반환한다. 35 | 36 | 37 | async def load_json(path): 38 | async with AIOFile(path, "r", encoding="UTF-8") as afp: 39 | print(await afp.read()) 40 | 41 | 42 | def convert_json2dict(json_str: str, dic: dict): 43 | for key, value in json.loads(json_str).items(): 44 | dic[key].append(value) 45 | 46 | 47 | @clock() 48 | def main(): 49 | dd = defaultdict(list) 50 | files = path.glob("*.json") 51 | 52 | tasks = [load_json(file) for file in files] 53 | 54 | return tasks 55 | 56 | 57 | # result = pd.DataFrame(dd) 58 | # print(result.head()) 59 | 60 | 61 | if __name__ == "__main__": 62 | 63 | loop = asyncio.get_event_loop() 64 | loop.run_until_complete(asyncio.gather(*main())) 65 | # loop.close() 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Ch10_CleanArchitecture.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Clean Architecture" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## 인상깊은 내용\n", 15 | "+ 관심사의 분리\n", 16 | " + 디자인이란 잘 정의된 하나의 책임을 가지고 독립적으로 분리된 유지보수가 쉬운 디자인을 말한다.\n", 17 | " + 이러한 특징들은 함수, 클래스, 메서드와 같은 세부 사항에 적용될 뿐만 아니라 소프트웨어 아키텍처의 컴포넌트에도 동일하게 적용할 수 있다.\n", 18 | "+ 추상화\n", 19 | " + 코드는 그 자체로 문서화가 되는 정도의 표현력을 가져야 한다.\n", 20 | " + 의존성 역전 원칙\n", 21 | " + ORM API와 애플리케이션 사이에 중간 계층인 어댑터를 만드는 것이 더 좋다.\n", 22 | " + 중요한 것은 최대한 기술적 세부사항을 숨기는 것이다.\n", 23 | "+ 애플리케이션에서 호출하기\n", 24 | " + 어떤 프레임워크가 사용되었는지, 데이터가 어디서 왔는지 어느 것도 대답할 수 없다면 좋은 신호이다.\n", 25 | " + 웹 프레임워크를 변경할 경우 애플리케이션 전체가 아니라 어댑터만 변경하면 된다.\n", 26 | "+ 기타\n", 27 | " + 모든 함수는 이름만으로 그 의도를 말할 수 있어야 한다.\n", 28 | " + 실용성이 이상보다 우선이다." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## 패키지\n", 36 | "+ 소프트웨어를 배포하고 보다 일번적인 방식으로 코드를 재사용하기 위한 편리한 방법\n", 37 | " + 빌드 패키지는 저장소에 업로드할 수 있으며 다른 아티펙트 저장소(artifact repository)에서 이 패키지를 다운도르할 수도 있음\n", 38 | "+ setup.py 이 가장 중요한 파일로 프로젝트의 모든 중요한 정의가 지정되어 있음" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### sample 1\n", 46 | "```\n", 47 | "\n", 48 | "\n", 49 | "from setuptools import find_packages, setup\n", 50 | "\n", 51 | "with open(\"README.rst\", \"r\") as longdesc:\n", 52 | " long_description = longdesc.read()\n", 53 | "\n", 54 | "\n", 55 | "install_requires = [\"web\", \"storage\"]\n", 56 | "\n", 57 | "setup(\n", 58 | " name=\"delistatus\",\n", 59 | " description=\"Check the status of a delivery order\",\n", 60 | " long_description=long_description,\n", 61 | " author=\"Dev team\",\n", 62 | " version=\"0.1.0\",\n", 63 | " packages=find_packages(), # 현재폴더 기준으로 하위폴더에서 찾는다.\n", 64 | " install_requires=install_requires, \n", 65 | " entry_points={\n", 66 | " \"console_scripts\": [\n", 67 | " \"status-service = statusweb.service:main\",\n", 68 | " ],\n", 69 | " },\n", 70 | ")\n", 71 | "\n", 72 | "```" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "### sample 2 \n", 80 | "```\n", 81 | "from setuptools import find_packages, setup\n", 82 | "\n", 83 | "with open(\"README.rst\", \"r\") as longdesc:\n", 84 | " long_description = longdesc.read()\n", 85 | "\n", 86 | "\n", 87 | "install_requires = [\"sanic\"]\n", 88 | "\n", 89 | "setup(\n", 90 | " name=\"web\",\n", 91 | " description=\"Library with helpers for the web-related functionality\",\n", 92 | " long_description=long_description,\n", 93 | " author=\"Dev team\",\n", 94 | " version=\"0.1.0\",\n", 95 | " packages=find_packages(where=\"src/\"), # include all packages under src\n", 96 | " package_dir={\"\": \"src\"}, # tell distutils packages are under src\n", 97 | " install_requires=install_requires,\n", 98 | ")\n", 99 | "```" 100 | ] 101 | } 102 | ], 103 | "metadata": { 104 | "kernelspec": { 105 | "display_name": "Python 3", 106 | "language": "python", 107 | "name": "python3" 108 | }, 109 | "language_info": { 110 | "codemirror_mode": { 111 | "name": "ipython", 112 | "version": 3 113 | }, 114 | "file_extension": ".py", 115 | "mimetype": "text/x-python", 116 | "name": "python", 117 | "nbconvert_exporter": "python", 118 | "pygments_lexer": "ipython3", 119 | "version": "3.7.6" 120 | } 121 | }, 122 | "nbformat": 4, 123 | "nbformat_minor": 4 124 | } 125 | -------------------------------------------------------------------------------- /Ch1_CodeFormattingAndTool.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Chapter 1. 소개 및 코드 포매팅과 도구\n", 8 | "\n", 9 | "

\n", 10 | "## 클린 코드의 의미 \n", 11 | "+ 프로그래밍 언어라는 것은 인간의 아이디어를 컴퓨터에 전달하는 언어가 아니라 아이디어를 다른 개발자에게 전달하는 것\n", 12 | "+ 따라서 클린코드의 기준은 다른 엔지니어가 코드를 읽고 유지 관리할 수 있는지 여부\n", 13 | "\n", 14 | "

\n", 15 | "## 클린 코드의 중요성\n", 16 | "+ 기술부채는 나쁜 결정이나 적당한 타협의 결과로 생긴 소프트웨어적 결함을 의미\n", 17 | " + 과거의 방향(현재 문제가 과제의 타협에서 옴)과 미래의 방향(미래 문제의 원인을 제공)으로 생각해볼 수 있음\n", 18 | " + 지금 바꾸는 것보다 미래에 변경하는 것이 더 어렵기 때문에 부채라는 단어가 딱 어울림\n", 19 | " + 가장 안 좋은 점은 장기적이고 근본적인 문제를 내포하므로 언젠가는 깨어나 프로젝트의 돌발 변수가 될 수 있음\n", 20 | "\n", 21 | "#### 클린 코드에서 코드 포매팅의 역할\n", 22 | "+ 클린 코드는 코드 포매팅 그 이상의 것을 의미하며 유지보수성이나 소프트웨어 품질에 관한 것을 말함\n", 23 | "+ 그러나, PEP-8 준수와 같은 것과 않으나 작업의 효율화를 위해 중요\n", 24 | "+ PEP-8 : https://b.luavis.kr/python/python-convention\n", 25 | "\n", 26 | "#### 프로젝트 코딩 스타일 가이드 준수\n", 27 | "+ 좋은 코드 레이아웃에서 가장 필요한 특성은 일관성\n", 28 | "+ 파이썬은 코딩 스타일로 PEP-8을 사용\n", 29 | " + 검색의 효율성 : grep -nr \"local=\" 과 grep -nr \"local =\" 의 스페이스 차이\n", 30 | " + 일관성 : 코드의 가독성이 향상됨\n", 31 | " + 코드 품질 : 잠재적인 버그를 찾을 수 있음\n", 32 | "\n", 33 | "

\n", 34 | "## Docstring과 어노테이션\n", 35 | "+ 주석은 되도록 지양, 문서화(Docstring)로 대체\n", 36 | "+ 어노테이션은 파이썬의 동적 타입으로 인한 모호성을 개선하고, Mypy 같은 도구의 자동화 검증 수단으로도 활용 가능\n", 37 | "\n", 38 | "#### Docstring\n", 39 | "+ 소스 코드에 포함된 문서이며 코멘트가 아님\n", 40 | "+ 가능한 많은 docstring을 추가하는 것을 권장함\n", 41 | "+ 예상되는 함수의 입력과 출력을 문서화하면 사용할 때 이해하기 쉬움\n", 42 | "+ Sphinx(스핑크스)를 실행하면 프로젝트 문서화의 기본골격을 자동 생성하고 Docstring을 가져와 문서화된 페이지를 만들어 줌\n", 43 | " + https://kshyun87.tistory.com/63\n" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 10, 49 | "metadata": {}, 50 | "outputs": [ 51 | { 52 | "name": "stdout", 53 | "output_type": "stream", 54 | "text": [ 55 | "D.update([E, ]**F) -> None. Update D from dict/iterable E and F.\n", 56 | "If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]\n", 57 | "If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v\n", 58 | "In either case, this is followed by: for k in F: D[k] = F[k]\n" 59 | ] 60 | } 61 | ], 62 | "source": [ 63 | "\"\"\" dict.update의 dostring 내용 \"\"\"\n", 64 | "print(dict.update.__doc__) # __doc__로 Dostring 접근" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 14, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "name": "stdout", 74 | "output_type": "stream", 75 | "text": [ 76 | "{1: 'one', 2: 'two'}\n", 77 | "{1: 'one', 2: 'two', 3: 'three', 4: 'four'}\n" 78 | ] 79 | } 80 | ], 81 | "source": [ 82 | "\"\"\" dict.update의 docstring 활용 \"\"\"\n", 83 | "\n", 84 | "# dictionary를 통한 업데이트\n", 85 | "d = {}\n", 86 | "d.update({1:\"one\", 2:\"two\"})\n", 87 | "print(d)\n", 88 | "\n", 89 | "# 이터러블을 통한 업데이트\n", 90 | "d.update([(3, \"three\"), (4, \"four\")])\n", 91 | "print(d)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 15, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "data": { 101 | "text/plain": [ 102 | "'임의의 계산 수행'" 103 | ] 104 | }, 105 | "execution_count": 15, 106 | "metadata": {}, 107 | "output_type": "execute_result" 108 | } 109 | ], 110 | "source": [ 111 | "\"\"\" docstring 작성 \"\"\"\n", 112 | "\n", 113 | "def my_function():\n", 114 | " \"\"\"임의의 계산 수행\"\"\"\n", 115 | " return None\n", 116 | "\n", 117 | "my_function.__doc__" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "#### 어노테이션\n", 125 | "+ 코드 사용자에게 함수 인자로 어떤 값이 와야 하는지 힌트 제공\n", 126 | "+ 파이썬이 타입을 검사하거나 강제하지는 않음\n", 127 | "+ \\_\\_annotations\\_\\_ 이라는 특수 속성이 생김" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 16, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "{'lattitude': float, 'longitude': float, 'return': __main__.Point}" 139 | ] 140 | }, 141 | "execution_count": 16, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "\"\"\" 사용법 \"\"\"\n", 148 | "\n", 149 | "class Point:\n", 150 | " def __init__(self, lat, long):\n", 151 | " self.lat = lat\n", 152 | " self.long = long\n", 153 | "\n", 154 | "def locate(lattitude: float, longitude: float) -> Point:\n", 155 | " pass\n", 156 | "\n", 157 | "locate.__annotations__" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 17, 163 | "metadata": {}, 164 | "outputs": [ 165 | { 166 | "data": { 167 | "text/plain": [ 168 | "{'lat': float, 'long': float}" 169 | ] 170 | }, 171 | "execution_count": 17, 172 | "metadata": {}, 173 | "output_type": "execute_result" 174 | } 175 | ], 176 | "source": [ 177 | "\"\"\" 변수에 적용 \"\"\"\n", 178 | "\n", 179 | "class Point:\n", 180 | " lat: float\n", 181 | " long: float\n", 182 | "\n", 183 | "Point.__annotations__" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "metadata": {}, 189 | "source": [ 190 | "#### 어노테이션은 docstring을 대체하는 것일까?\n", 191 | "+ 일부 대체 가능하지만 보다 나은 문서화를 위한 여지를 남겨두어야 함\n", 192 | "+ 파라미터가 기댓값과 다를 경우 예외 등을 기술 할 수 있음(page 29 참고)\n", 193 | "\n", 194 | "#### 기본 품질 향상을 위한 도구 설정\n", 195 | "+ mypy : 프로젝트의 모든 파일을 분석하여 타입 불일치를 검사\n", 196 | "+ Pylint : PEP-8을 준수했는지 여부를 검사\n", 197 | " + .pylintrc 파일을 통해 설정 값을 바꿀 수 있음\n", 198 | "+ black : PEP-8을 준수했는지 여부를 검사하고 자동으로 바꿈\n", 199 | " + PEP-8 보다도 엄격한 기준을 적용(Uncompromising Code Formatter)\n", 200 | " + https://jonnung.dev/python/2019/11/10/python-black-uncompromising-code-formatter/\n", 201 | "※ makefile\n", 202 | "\n", 203 | "\n", 204 | "```\n", 205 | "Before running black formatter -\n", 206 | "def my_function(name):\n", 207 | " return 'received {0}'.format(name.title())\n", 208 | "```\n", 209 | "\n", 210 | "```\n", 211 | "Return modified source -\n", 212 | "def my_function(name):\n", 213 | " return \"received {0}\".format(name.title())\n", 214 | "```\n" 215 | ] 216 | } 217 | ], 218 | "metadata": { 219 | "file_extension": ".py", 220 | "kernelspec": { 221 | "display_name": "Python 3", 222 | "language": "python", 223 | "name": "python3" 224 | }, 225 | "language_info": { 226 | "codemirror_mode": { 227 | "name": "ipython", 228 | "version": 3 229 | }, 230 | "file_extension": ".py", 231 | "mimetype": "text/x-python", 232 | "name": "python", 233 | "nbconvert_exporter": "python", 234 | "pygments_lexer": "ipython3", 235 | "version": "3.7.6" 236 | }, 237 | "mimetype": "text/x-python", 238 | "name": "python", 239 | "npconvert_exporter": "python", 240 | "pygments_lexer": "ipython3", 241 | "version": 3 242 | }, 243 | "nbformat": 4, 244 | "nbformat_minor": 4 245 | } 246 | -------------------------------------------------------------------------------- /Ch5_Decorator.md: -------------------------------------------------------------------------------- 1 | # 파이썬의 데코레이터 2 | - Decorator: 장식하는 도구 3 | - clean code 4 | - syntax sugar (편리구문: 타이핑 감소 or 가독성 향상을 위해 다른 표현으로 코딩할 수 있게 해주는 기능) 5 | - 함수와 메서드 기능 쉽게 수정하기 위한 수단 6 | - 데코레이터 이후에 나오는 것을 데코레이터의 첫번째 파라미터로 하고, 데코레이터의 결과 값 반환 7 | - fluent python 8 | - 다른 함수를 인수로 받는 callable (callable: 호출할 수 있는 객체) 9 | - 메타프로그래밍(런타임에 프로그램 행위 변경) 편리 10 | - 모듈이 로딩될 때 바로 실행, import 타임에 실행 11 | 12 | - 적용범위 13 | - 호출 가능 객체(함수, 메서드, 제너레이터, 클래스)에 적용 14 | - 기능(effective python chapter 6) 15 | - **로그남기기** 16 | - **시간 측정** 17 | - 접근 제어와 인증 시행 18 | - 비율 제한 19 | - 캐싱 및 기타 추가기능 구현 20 | - 입력 인수와 반환 값을 접근하거나 수정 가능(ex: 재귀호출에서 함수 호출의 스택을 디버깅하는 경우) 21 | - 시맨틱 강조 22 | - 디버깅 23 | - 함수 등록 24 | - 라이브러리 25 | - 표준라이브러리 26 | - @staticmethod 27 | - @classmethod 28 | - @property (effective python chapter 4) 29 | - functools 30 | - **wraps** 31 | - lru_cache (memoization, Least Recently Used) 32 | - ... 33 | - numba 34 | - **@jit, @njit** 35 | 36 | 37 | ```python 38 | def original(): 39 | pass 40 | 41 | def modifier(func): 42 | pass 43 | 44 | original = modifier(original) 45 | ``` 46 | 47 | 48 | ```python 49 | @modifier # Decorator 50 | def original(): # 데코레이팅된(decorated) 함수 또는 래핑(wrapped)된 객체 51 | pass 52 | ``` 53 | 54 | - 보고서 생성 프로그램: 55 | - 비즈니스 로직 30개 56 | - 로직 간 단계에 입출력 로깅 추가해야 하는 경우 57 | - 로직함수 각각에 로깅호출 수작업 vs @audit_log 58 | 59 | ### 데코레이터: 감싸고 있는 함수를 호출하기 전/후, 추가로 코드 실행 가능 60 | 61 | #### null_decorator 62 | 63 | 64 | ```python 65 | def null_decorator(func): 66 | return func 67 | ``` 68 | 69 | 70 | ```python 71 | def greet(): 72 | return 'hello' 73 | 74 | greet = null_decorator(greet) 75 | greet() 76 | ``` 77 | 78 | 79 | 80 | 81 | 'hello' 82 | 83 | 84 | 85 | 86 | ```python 87 | @null_decorator 88 | def greet(): 89 | '''return greeting''' 90 | return 'hello' 91 | 92 | greet() 93 | ``` 94 | 95 | 96 | 97 | 98 | 'hello' 99 | 100 | 101 | 102 | 103 | ```python 104 | print(greet) 105 | print(greet.__name__) 106 | print(greet.__doc__) 107 | print(null_decorator(greet)) 108 | ``` 109 | 110 | 111 | greet 112 | return greeting 113 | 114 | 115 | 116 | 117 | ```python 118 | def uppercase(func): 119 | def wrapper(): 120 | result = func() 121 | return result.upper() 122 | return wrapper 123 | 124 | @uppercase 125 | def greet(): 126 | '''return greeting''' 127 | return 'hello' 128 | 129 | greet() 130 | ``` 131 | 132 | 133 | 134 | 135 | 'HELLO' 136 | 137 | 138 | 139 | 140 | ```python 141 | # 데코레이트된 함수 메타데이터에 접근하면, 대신 감싼 클로저(wrapper)의 메타데이터 표시 142 | print(uppercase(greet)) 143 | print(uppercase(greet).__name__) 144 | print(uppercase(greet).__doc__) 145 | ``` 146 | 147 | .wrapper at 0x7f3f23afa620> 148 | wrapper 149 | None 150 | 151 | 152 | 153 | ```python 154 | help(greet) 155 | ``` 156 | 157 | Help on function wrapper in module __main__: 158 | 159 | wrapper() 160 | 161 | 162 | 163 | - uppercase 함수는 그 안에 정의된 wrapper 반환 164 | - 데코레이터를 호출한 후 wrapper 함수가 greet 이름에 할당 165 | - 디버깅시 문제 발생 166 | - 해결책 167 | - 디버깅 가능한 데코레이터 작성 168 | - functools의 wraps 헬퍼 함수 사용 169 | 170 | #### wraps: 입력 함수의 docstring, metadata 전달 171 | 172 | 173 | ```python 174 | from functools import wraps 175 | 176 | def uppercase(func): 177 | @wraps(func) 178 | def wrapper(): 179 | result = func() 180 | return result.upper() 181 | return wrapper 182 | 183 | @uppercase 184 | def greet(): 185 | '''return greeting''' 186 | return 'hello' 187 | 188 | greet() 189 | ``` 190 | 191 | 192 | 193 | 194 | 'HELLO' 195 | 196 | 197 | 198 | 199 | ```python 200 | print(uppercase(greet)) 201 | print(uppercase(greet).__name__) 202 | print(uppercase(greet).__doc__) 203 | ``` 204 | 205 | 206 | greet 207 | return greeting 208 | 209 | 210 | 211 | ```python 212 | help(greet) 213 | ``` 214 | 215 | Help on function greet in module __main__: 216 | 217 | greet() 218 | return greeting 219 | 220 | 221 | 222 | #### trace 223 | 224 | 225 | ```python 226 | def trace(func): 227 | @wraps(func) 228 | def wrapper(*args, **kwargs): 229 | result = func(*args, **kwargs) 230 | print(f'{func.__name__}(args: {args}, kwargs: {kwargs}) -> result: {result}') 231 | return result 232 | return wrapper 233 | 234 | @trace # fibonacci = trace(fibonacci) 235 | def fibonacci(n): 236 | '''return the n-th fibonacci number''' 237 | if n in (0, 1): 238 | return n 239 | return (fibonacci(n-2) + fibonacci(n-1)) 240 | 241 | fibonacci(3) 242 | pass 243 | ``` 244 | 245 | fibonacci(args: (1,), kwargs: {}) -> result: 1 246 | fibonacci(args: (0,), kwargs: {}) -> result: 0 247 | fibonacci(args: (1,), kwargs: {}) -> result: 1 248 | fibonacci(args: (2,), kwargs: {}) -> result: 1 249 | fibonacci(args: (3,), kwargs: {}) -> result: 2 250 | 251 | 252 | #### 함수 실행 시간 측정 253 | 254 | 255 | ```python 256 | import time 257 | 258 | def clock(func): 259 | @wraps(func) 260 | def clocked(*args): 261 | t0 = time.perf_counter() 262 | result = func(*args) 263 | elapsed = time.perf_counter() - t0 264 | name = func.__name__ 265 | arg_str = ', '.join(repr(arg) for arg in args) 266 | print(f'{elapsed:.8f} {name}({arg_str}) -> {result}') 267 | return result 268 | return clocked 269 | 270 | @clock 271 | def snooze(seconds): 272 | time.sleep(seconds) 273 | 274 | @clock 275 | def factorial(n): 276 | return 1 if n < 2 else n*factorial(n-1) 277 | 278 | 279 | print(snooze(.123)) 280 | print(f'3! = {factorial(3)}') 281 | ``` 282 | 283 | 0.12350932 snooze(0.123) -> None 284 | None 285 | 0.00000124 factorial(1) -> 1 286 | 0.00006402 factorial(2) -> 2 287 | 0.00010787 factorial(3) -> 6 288 | 3! = 6 289 | 290 | 291 | #### numba, jit 292 | 293 | 294 | ```python 295 | def mandel(x, y, max_iters): 296 | """ 297 | Given the real and imaginary parts of a complex number, 298 | determine if it is a candidate for membership in the Mandelbrot 299 | set given a fixed number of iterations. 300 | """ 301 | i = 0 302 | c = complex(x,y) 303 | z = 0.0j 304 | for i in range(max_iters): 305 | z = z*z + c 306 | if (z.real*z.real + z.imag*z.imag) >= 4: 307 | return i 308 | 309 | return 255 310 | 311 | def create_fractal(min_x, max_x, min_y, max_y, image, iters): 312 | height = image.shape[0] 313 | width = image.shape[1] 314 | 315 | pixel_size_x = (max_x - min_x) / width 316 | pixel_size_y = (max_y - min_y) / height 317 | for x in range(width): 318 | real = min_x + x * pixel_size_x 319 | for y in range(height): 320 | imag = min_y + y * pixel_size_y 321 | color = mandel(real, imag, iters) 322 | image[y, x] = color 323 | 324 | return image 325 | 326 | image = np.zeros((500 * 2, 750 * 2), dtype=np.uint8) 327 | s = timer() 328 | create_fractal(-2.0, 1.0, -1.0, 1.0, image, 20) 329 | e = timer() 330 | print(f'{(e - s):.3f} sec') 331 | imshow(image) 332 | show() 333 | ``` 334 | 335 | 4.326 sec 336 | 337 | 338 | 339 | ![png](output_23_1.png) 340 | 341 | 342 | 343 | ```python 344 | from numba import jit 345 | 346 | @jit 347 | def mandel(x, y, max_iters): 348 | """ 349 | Given the real and imaginary parts of a complex number, 350 | determine if it is a candidate for membership in the Mandelbrot 351 | set given a fixed number of iterations. 352 | """ 353 | i = 0 354 | c = complex(x,y) 355 | z = 0.0j 356 | for i in range(max_iters): 357 | z = z*z + c 358 | if (z.real*z.real + z.imag*z.imag) >= 4: 359 | return i 360 | 361 | return 255 362 | 363 | @jit 364 | def create_fractal(min_x, max_x, min_y, max_y, image, iters): 365 | height = image.shape[0] 366 | width = image.shape[1] 367 | 368 | pixel_size_x = (max_x - min_x) / width 369 | pixel_size_y = (max_y - min_y) / height 370 | for x in range(width): 371 | real = min_x + x * pixel_size_x 372 | for y in range(height): 373 | imag = min_y + y * pixel_size_y 374 | color = mandel(real, imag, iters) 375 | image[y, x] = color 376 | 377 | return image 378 | 379 | image = np.zeros((500 * 2, 750 * 2), dtype=np.uint8) 380 | s = timer() 381 | create_fractal(-2.0, 1.0, -1.0, 1.0, image, 20) 382 | e = timer() 383 | print(f'{(e - s):.3f} sec') 384 | imshow(image) 385 | show() 386 | ``` 387 | 388 | 0.328 sec 389 | 390 | 391 | 392 | ![png](output_24_1.png) 393 | 394 | 395 | #### 다중 데코레이터 396 | 397 | 398 | ```python 399 | def strong(func): 400 | def wrapper(): 401 | return '' + func() + '' 402 | return wrapper 403 | 404 | def emphasis(func): 405 | def wrapper(): 406 | return '' + func() + '' 407 | return wrapper 408 | 409 | @strong 410 | @emphasis 411 | def greet(): 412 | return 'Hello!' 413 | 414 | greet() 415 | ``` 416 | 417 | 418 | 419 | 420 | 'Hello!' 421 | 422 | 423 | 424 | 425 | ```python 426 | def greet(): 427 | return 'Hello!' 428 | 429 | decorated_greet = strong(emphasis(greet)) 430 | decorated_greet() 431 | ``` 432 | 433 | 434 | 435 | 436 | 'Hello!' 437 | 438 | 439 | 440 | - 파이썬 함수: 일급 객체(first-class object) 441 | - 변수에 할당 442 | bark = yell 443 | - 데이터 구조에 저장 444 | funcs = [bark, str.lower] 445 | - 인자로 다른 함수에 전달 446 | def greet(func): 447 | print(func('hi')) 448 | - 다른 함수의 값에서 반환 449 | def speak(text): 450 | def whisper(t): 451 | return t.lower() + '...' 452 | return whisper(text) 453 | - 클로저 454 | 455 | #### 클로저: 함수를 정의할 때 존재하던 자유 변수에 대한 바인딩을 유지하는 함수 456 | - '비전역' 외부 변수를 다루는 경우는 그 함수가 다른 함수 안에 정의된 경우 한정 457 | 458 | 459 | ```python 460 | # 변수범위 461 | 462 | b = 6 463 | def f(a): 464 | print(a) 465 | print(b) 466 | b = 9 467 | 468 | f(3) 469 | ``` 470 | 471 | 3 472 | 473 | 474 | 475 | --------------------------------------------------------------------------- 476 | 477 | UnboundLocalError Traceback (most recent call last) 478 | 479 | in 480 | 7 b = 9 481 | 8 482 | ----> 9 f(3) 483 | 484 | 485 | in f(a) 486 | 4 def f(a): 487 | 5 print(a) 488 | ----> 6 print(b) 489 | 7 b = 9 490 | 8 491 | 492 | 493 | UnboundLocalError: local variable 'b' referenced before assignment 494 | 495 | 496 | 497 | ```python 498 | def make_averager(): 499 | series = [] 500 | 501 | def averager(new_value): 502 | series.append(new_value) 503 | total = sum(series) 504 | return total/len(series) 505 | return averager 506 | 507 | avg = make_averager() 508 | print(avg(10)) 509 | print(avg(11)) 510 | print(avg(12)) 511 | ``` 512 | 513 | 10.0 514 | 10.5 515 | 11.0 516 | 517 | 518 | ## 함수 데코레이터 519 | - 기능 520 | - 파라미터의 유효성 검사 521 | - 사전조건 검사 522 | - 기능 전체 재정의 523 | - 서명 변경 524 | - 원래 함수 결과 캐싱 525 | 526 | ## 클래스 데코레이터 527 | 528 | - 코드재사용, DRY (Don't Repeat Yourself) 원칙 이점 공유 529 | - 여러 클래스에 특정 인터페이스나 기준 강제 530 | - 여러 클래스에 적용할 검사를 데코레이터 한 번만 시행 531 | - 작고 간단한 클래스를 생성하고, 데코레이터로 기능 보강 (선지원 후고민?) 532 | - 유지보수시 기존 로직 쉽게 변경 533 | 534 | ## 다른 유형의 데코레이터 535 | - 스택 형태 536 | - 코루틴으로 사용되는 제너레이터 (clean code 7장에서...) 537 | 538 | ## 데코레이터에 인자 전달 539 | 540 | ### 중첩함수의 데코레이터 541 | - 고차함수(higher-order function): 함수를 파라미터로 받아서 함수를 반환하는 함수 542 | 543 | ### 데코레이터 객체 544 | - 클래스를 사용하여 데코레이터 정의 545 | __init__ : 파라미터 전달 546 | __call__ : 데코레이터 로직 구현 547 | 548 | ## 데코레이터 활용 우수 사례 549 | - 파라미터 변환 550 | - 코드 추적(trace): 파라미터, 함수 실행 로깅 551 | - 실제 함수의 실행 경로 추적(ex: 실행 함수 로깅) 552 | - 함수 지표 모니터링 553 | - 함수의 실행 시간 측정 554 | - 언제 함수가 실행되고 전달된 파라미터는 무엇인지 로깅 555 | - 파라미터 유효성 검사 556 | - 재시도 로직 구현 557 | - 반복작업을 데코레이터로 이동하여 클래스 단순화 558 | 559 | ## 데코레이터의 활용 - 흔한 실수 피하기 560 | 561 | ### 래핑된 원본 객체의 데이터 보존 562 | - wraps 563 | __qualname__ (python 3.3) 564 | 순수 함수명과 클래스 이름 구분 565 | 566 | ex: A클래스 b함수 567 | __name__: b 568 | --qualname__: A.b 569 | 570 | ### 데코레이터 부작용 처리 571 | - 데코레이터 부작용의 잘못된 처리 572 | 573 | - 데코레이터 부작용의 활용 574 | 575 | ### 어느곳에서나 동작하는 데코레이터 만들기 576 | 577 | ## 데코레이터와 DRY 원칙 578 | - 처음부터 데코레이터를 만들지 않는다. 패턴이 생기고 데코레이터에 대한 추상화가 명확해지면 리팩토링 진행 579 | - 데코레이터가 적어도 3회 이상 필요한 경우에만 구현 580 | - 데코레이터 코드를 최소한으로 유지 581 | 582 | ## 데코레이터와 관심사의 분리 583 | def trace_function(function): 584 | ... 585 | @trace_function 586 | def operation(): 587 | ... 588 | 589 | 590 | def log_execution(function): 591 | ... 592 | def measure_time(function): 593 | ... 594 | @measure_time 595 | @log_execution 596 | def operation(): 597 | ... 598 | 599 | ## 좋은 데코레이터 분석 600 | - 캡슐화와 관심사의 분리: 실제로 하는 일과 데코레이팅하는 일의 책임을 명확히 구분 601 | # Celery 602 | @app.task 603 | def mytask(): 604 | ... 605 | 606 | # 웹 프레임 워크 607 | @route('/', method = ['GET'] 608 | def view_handler(request): 609 | ... 610 | - 독립성: 데코레이터와 데코레이팅 되는 객체는 최대한 분리 611 | - 재사용성: 데코레이터는 여러 유형에 적용 가능한 충분히 범용적인 형태가 바람직 612 | 613 | ## 요약 614 | - 데코레이터는 런타임에 한 함수로 다른 함수를 수정할 수 있게 해주는 문법 615 | - 감싸고 있는 함수를 호출하기 전/후 추가로 코드 실행 가능 616 | - 데코레이터를 사용하면 디버거와 같이 객체 내부를 조사하는 도구가 이상하게 동작 -> 내장 모듈 functool의 wraps 데코레이터 사용 617 | -------------------------------------------------------------------------------- /Ch5_Decorator.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Chapter 5. 데코레이터를 활용한 코드 개선\n", 8 | "+ 데코레이터 패턴과 혼동하지 말 것\n", 9 | "\n", 10 | "## 함수 데코레이터" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "outputs": [ 18 | { 19 | "name": "stdout", 20 | "output_type": "stream", 21 | "text": [ 22 | "Completed a function\n" 23 | ] 24 | } 25 | ], 26 | "source": [ 27 | "\"\"\" 도메인의 특정 예외에 대해서 특정 횟수만큼 재시도하는 데코레이터 \"\"\"\n", 28 | "\n", 29 | "from functools import wraps\n", 30 | "from random import randint\n", 31 | "import logging\n", 32 | "\n", 33 | "class ControlledException(Exception):\n", 34 | " \"\"\" 일반적인 예외 가정 \"\"\"\n", 35 | "\n", 36 | "def retry(operation):\n", 37 | " @wraps(operation)\n", 38 | " def wrapped(*args, **kargs):\n", 39 | " last_raised = None\n", 40 | " RETRIES_LIMIT = 3\n", 41 | " for i in range(RETRIES_LIMIT):\n", 42 | " try:\n", 43 | " return operation(*args, **kargs)\n", 44 | " except ControlledException as e:\n", 45 | " logger.info(\"retrying %s\", operation.__qualname__)\n", 46 | " last_raised = e\n", 47 | " raise last_raised\n", 48 | " return wrapped\n", 49 | "\n", 50 | "\n", 51 | "@retry\n", 52 | "def run_operation(a:int, b:int):\n", 53 | " if randint(a, b) % 2 == 0:\n", 54 | " print(\"Completed a function\")\n", 55 | " else:\n", 56 | " raise ControlledException()\n", 57 | "\n", 58 | "if __name__ == \"__main__\":\n", 59 | " logger = logging.getLogger('example')\n", 60 | " logger.setLevel(logging.DEBUG)\n", 61 | " run_operation(0, 9)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## 클래스 데코레이터\n", 69 | "+ 파라미터로 함수가 아닌 클래스를 받는다는 점만 차이점이 있음\n", 70 | "+ 장점\n", 71 | " + 코드 재사용과 DRY 원칙의 모든 이점을 공유할 수 있음\n", 72 | " + 당장은 작고 간단한 클래스를 생성하고 나중에 데코레이터로 기능을 보강할 수 있음\n", 73 | " + 기존 로직을 쉽게 변경 가능하며, 메타클래스와 같은 방법보다 비교적 간단함" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 2, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "name": "stdout", 83 | "output_type": "stream", 84 | "text": [ 85 | "{'username': 'shyeon', 'password': '****', 'ip': '127.0.0.1'}\n" 86 | ] 87 | } 88 | ], 89 | "source": [ 90 | "\"\"\" 확장에 불리한 구조: 이벤트 클래스와 직렬화 클래스가 1대 1로 매팅됨 \"\"\"\n", 91 | "\n", 92 | "class LoginEventSerializer:\n", 93 | " def __init__(self, event):\n", 94 | " self.event = event\n", 95 | "\n", 96 | " def serialize(self) -> dict:\n", 97 | " return {\n", 98 | " \"username\": self.event.username,\n", 99 | " \"password\": len(self.event.password) * \"*\",\n", 100 | " \"ip\": self.event.ip,\n", 101 | " }\n", 102 | "\n", 103 | "\n", 104 | "class LoginEvent:\n", 105 | " SERIALIZER = LoginEventSerializer\n", 106 | "\n", 107 | " def __init__(self, username, password, ip):\n", 108 | " self.username = username\n", 109 | " self.password = password\n", 110 | " self.ip = ip\n", 111 | "\n", 112 | " def serialize(self) -> dict:\n", 113 | " return self.SERIALIZER(self).serialize()\n", 114 | "\n", 115 | "\n", 116 | "if __name__ == \"__main__\":\n", 117 | " event = LoginEvent(\"shyeon\", \"0524\", \"127.0.0.1\")\n", 118 | " print(event.serialize())" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 3, 124 | "metadata": {}, 125 | "outputs": [ 126 | { 127 | "name": "stdout", 128 | "output_type": "stream", 129 | "text": [ 130 | "{'username': 'shyeon', 'password': '****', 'ip': '127.0.0.1', 'timestamp': '2020-02-14 09:00'}\n" 131 | ] 132 | } 133 | ], 134 | "source": [ 135 | "\"\"\" 데코레이션을 활용한 구조 개선 \"\"\"\n", 136 | "\n", 137 | "from datetime import datetime\n", 138 | "\n", 139 | "def hide_field(field) -> str:\n", 140 | " return len(field) * \"*\"\n", 141 | "\n", 142 | "def format_time(field_timestamp: datetime) -> str:\n", 143 | " return field_timestamp.strftime(\"%Y-%m-%d %H:%M\")\n", 144 | "\n", 145 | "def show_original(event_field):\n", 146 | " return event_field\n", 147 | "\n", 148 | "\n", 149 | "class EventSerializer:\n", 150 | " def __init__(self, serialization_fields: dict) -> None:\n", 151 | " self.serialization_fields = serialization_fields\n", 152 | "\n", 153 | " def serialize(self, event) -> dict:\n", 154 | " return {\n", 155 | " field: transformation(getattr(event, field))\n", 156 | " for field, transformation in self.serialization_fields.items()\n", 157 | " }\n", 158 | "\n", 159 | "\n", 160 | "class Serialization:\n", 161 | " def __init__(self, **transformations):\n", 162 | " self.serializer = EventSerializer(transformations)\n", 163 | "\n", 164 | " def __call__(self, event_class):\n", 165 | " def serialize_method(event_instance):\n", 166 | " return self.serializer.serialize(event_instance)\n", 167 | " event_class.serialize = serialize_method\n", 168 | " return event_class\n", 169 | "\n", 170 | "\n", 171 | "@Serialization(\n", 172 | " username=show_original,\n", 173 | " password=hide_field,\n", 174 | " ip=show_original,\n", 175 | " timestamp=format_time)\n", 176 | "class LoginEvent:\n", 177 | " def __init__(self, username, password, ip, timestamp):\n", 178 | " self.username = username\n", 179 | " self.password = password\n", 180 | " self.ip = ip\n", 181 | " self.timestamp = timestamp\n", 182 | "\n", 183 | "if __name__ == \"__main__\":\n", 184 | " event = LoginEvent(\"shyeon\", \"0524\", \"127.0.0.1\", datetime(2020, 2, 14, 9, 00, 00))\n", 185 | " print(event.serialize())" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 4, 191 | "metadata": {}, 192 | "outputs": [ 193 | { 194 | "name": "stdout", 195 | "output_type": "stream", 196 | "text": [ 197 | "{'username': 'shyeon', 'password': '****', 'ip': '127.0.0.1', 'timestamp': '2020-02-14 09:00'}\n" 198 | ] 199 | } 200 | ], 201 | "source": [ 202 | "\"\"\" dataclass 데코레이터: init 함수의 단순코드 작성 간편화 \"\"\"\n", 203 | "\n", 204 | "from dataclasses import dataclass\n", 205 | "from datetime import datetime\n", 206 | "\n", 207 | "@Serialization(\n", 208 | " username=show_original,\n", 209 | " password=hide_field,\n", 210 | " ip=show_original,\n", 211 | " timestamp=format_time)\n", 212 | "@dataclass\n", 213 | "class LoginEvent:\n", 214 | " username: str\n", 215 | " password: str\n", 216 | " ip: str\n", 217 | " timestamp: datetime\n", 218 | "\n", 219 | "if __name__ == \"__main__\":\n", 220 | " event = LoginEvent(\"shyeon\", \"0524\", \"127.0.0.1\", datetime(2020, 2, 14, 9, 00, 00))\n", 221 | " print(event.serialize())" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "## 데코레이터에 인자 전달\n", 229 | "+ 간접 참조를 통해 새로운 레벨의 중첩 함수를 만들어 데코레이터의 모든 것을 한 단계 더 깊게 만듬\n", 230 | "+ 데코레이터를 위한 클래스를 생성(가독성이 더 좋음)\n", 231 | "\n", 232 | "### 중첩함수를 사용하는 방법" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": 14, 238 | "metadata": {}, 239 | "outputs": [ 240 | { 241 | "ename": "TypeError", 242 | "evalue": "'NoneType' object is not callable", 243 | "output_type": "error", 244 | "traceback": [ 245 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 246 | "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", 247 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0mwith_retry\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mretries_limit\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 21\u001b[0;31m \u001b[0;32mdef\u001b[0m \u001b[0mrun_operation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mint\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mint\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 22\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mrandint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0;36m2\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Completed a function\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 248 | "\u001b[0;31mTypeError\u001b[0m: 'NoneType' object is not callable" 249 | ] 250 | } 251 | ], 252 | "source": [ 253 | "RETRIES_LIMIT = 3\n", 254 | "\n", 255 | "def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None): # 1.인수를 전달\n", 256 | " allowed_exceptions = allowed_exceptions or (ControlledException,)\n", 257 | "\n", 258 | " def retry(operation): # 2.데코레이터가 될 함수\n", 259 | " @wraps(operation)\n", 260 | "\n", 261 | " def wrapped(*arg, **kwargs): # 3.결과를 반환\n", 262 | " last_raised = None\n", 263 | " for _ in range(retries_limit):\n", 264 | " try:\n", 265 | " return operation(*arg, **kwargs)\n", 266 | " except allowed_exceptions as e:\n", 267 | " logger.info(\"retrying %s due to %s\", operation, e)\n", 268 | " last_raised = e\n", 269 | " raise last_raised\n", 270 | "\n", 271 | "\n", 272 | "@with_retry(retries_limit=3)\n", 273 | "def run_operation(a:int, b:int):\n", 274 | " if randint(a, b) % 2 == 0:\n", 275 | " print(\"Completed a function\")\n", 276 | " else:\n", 277 | " raise ControlledException()\n", 278 | "\n", 279 | "\n", 280 | "if __name__ == \"__main__\":\n", 281 | " logger = logging.getLogger('example')\n", 282 | " logger.setLevel(logging.DEBUG)\n", 283 | " run_operation(0, 9)" 284 | ] 285 | }, 286 | { 287 | "cell_type": "markdown", 288 | "metadata": {}, 289 | "source": [ 290 | "### 데코레이터 객체" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 15, 296 | "metadata": {}, 297 | "outputs": [ 298 | { 299 | "ename": "TypeError", 300 | "evalue": "'NoneType' object is not callable", 301 | "output_type": "error", 302 | "traceback": [ 303 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 304 | "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", 305 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0mlogger\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlogging\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetLogger\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'example'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0mlogger\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msetLevel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlogging\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDEBUG\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 32\u001b[0;31m \u001b[0mrun_operation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m9\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 306 | "\u001b[0;31mTypeError\u001b[0m: 'NoneType' object is not callable" 307 | ] 308 | } 309 | ], 310 | "source": [ 311 | "RETRIES_LIMIT = 3\n", 312 | "\n", 313 | "\n", 314 | "class WithRetry:\n", 315 | " def __init__(self, retries_limit=RETRIES_LIMIT, allowed_exceptions=None):\n", 316 | " self.retries_limit = retries_limit\n", 317 | " self.allowed_exceptions = allowed_exceptions or (ControlledException,)\n", 318 | "\n", 319 | " def __call__(self, operation):\n", 320 | " @wraps(operation)\n", 321 | " def wrapped(*arg, **kwargs): # 3.결과를 반환\n", 322 | " last_raised = None\n", 323 | " for _ in range(self.retries_limit):\n", 324 | " try:\n", 325 | " return operation(*arg, **kwargs)\n", 326 | " except self.allowed_exceptions as e:\n", 327 | " logger.info(\"retrying %s due to %s\", operation, e)\n", 328 | " last_raised = e\n", 329 | " raise last_raised\n", 330 | "\n", 331 | "\n", 332 | "@WithRetry(retries_limit=RETRIES_LIMIT)\n", 333 | "def run_operation(a:int, b:int):\n", 334 | " if randint(a, b) % 2 == 0:\n", 335 | " print(\"Completed a function\")\n", 336 | " else:\n", 337 | " raise ControlledException()\n", 338 | " \n", 339 | "\n", 340 | "if __name__ == \"__main__\":\n", 341 | " logger = logging.getLogger('example')\n", 342 | " logger.setLevel(logging.DEBUG)\n", 343 | " run_operation(0, 9)" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": null, 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [] 352 | } 353 | ], 354 | "metadata": { 355 | "file_extension": ".py", 356 | "kernelspec": { 357 | "display_name": "Python 3", 358 | "language": "python", 359 | "name": "python3" 360 | }, 361 | "language_info": { 362 | "codemirror_mode": { 363 | "name": "ipython", 364 | "version": 3 365 | }, 366 | "file_extension": ".py", 367 | "mimetype": "text/x-python", 368 | "name": "python", 369 | "nbconvert_exporter": "python", 370 | "pygments_lexer": "ipython3", 371 | "version": "3.7.6" 372 | }, 373 | "mimetype": "text/x-python", 374 | "name": "python", 375 | "npconvert_exporter": "python", 376 | "pygments_lexer": "ipython3", 377 | "version": 3 378 | }, 379 | "nbformat": 4, 380 | "nbformat_minor": 4 381 | } 382 | -------------------------------------------------------------------------------- /Ch3_FeaturesOfGoodCodes.md: -------------------------------------------------------------------------------- 1 | # Chapter 3 좋은 코드의 일반적인 특징 2 | 3 | ## General Traits of Good Code 4 | 5 | ### 목표: 6 | 7 | - 견고한 SW의 개념을 이해 8 | - 작업 중 잘못된 데이터를 다루는 법 9 | - 새로운 요구 사항을 쉽게 받아들이고 확장할 수 잇는 유지보수가 쉬운 SW 설계 10 | - 재사용 가능한 SW설계 11 | - 개발팀의 생산성을 높이는 효율적인 코드 작성 12 | 13 | ## 1. 계약에 의한 디자인(Design by Contract) 14 | 15 | 계약에 의한 디자인이란, 관계자가 기대하는 바를 암묵적으로 코드에 삽입하는 대신 양측이 동의하는 계약을 먼저 한 다음, 계약을 어겼을 경우는 명시적으로 왜 계속할 수 없는지 예외를 발생시키는 것을 의미하며 구체적으로는 SW 컴포넌트간의 통신 중에 반드시 지켜져야 할 몇 가지 규칙을 강제하는 것을 말한다(i.e. input : int only -> output : str only) 16 | 17 | 계약은 주로 사전/사후조건을 명시하지만 때때로 불변식(함수가 실행되는 동안 일정하게 유지되는 것. 로직 확인용. doc-string에 언급)과 부작용(코드 부작용을 doc-string에 언급)을 기술함. 18 | 19 | --- 20 | 21 | - 사전조건 : 코드 실행전에 체크/처리해야 하는 것.i.e 데이터 유효성 검사. 22 | - 함수나 메서드가 정상 동작하기 위해 보장해야 하는 조건을 의미함. (i.e. 초기화된 객체, null 아닌 값등) —> Type checking 보다는 유효성 검사에 가까움. 23 | - 계약에 의한 디자인(DbC)에서는 함수가 자체적으로 로직을 실행하기전에 유효성 검사를 하는것이 널리 쓰임.(demanding approach) 24 | - 위에 반대되는 것은 "클라이언트가 함수를 호출하기 전에 모든 유효성 검사를 하는 것"임(tolerant approach) 25 | - 사후조건 : 함수 return값의 유효성 검사, 호출자가 기대하는 바를 return 받았는지 검사. 26 | - 파이썬스러운 계약 : PEP-316 in Deferred status 27 | - ***(저자) 디자인시 RuntimeError, ValueError등의 예외를 발생시키는 제어 매커니즘을 추가하는것이 바람직 함. 문제를 특정하기 어려울 경우, 사용자 정의 예외를 만드는 것이 바람직.*** 28 | - ***(저자) 데코레이터등을 사용하여 사전/사후검사 부분과 핵심 기능 부분의 코드를 격리된 상태로 유지하는 것이 바람직 함.*** 29 | - 계약에 의한 디자인(DbC) - 결론 : 이 디자인 규칙을 따름으로써... 30 | 1. 문제가 있는 부분을 효과적으로 식별할 수 있다. 31 | 2. 코드가 더욱 견고해 진다. 32 | 3. 프로그램의 구조를 명확히 하는데에도 도움이 된다. 33 | 4. 계약 작성에 대한 추가작업이 발생하며 단위 테스트를 상황에 따라 추가해야 할 수도 있다. 34 | - ***(저자) 이 방법을 더욱 효과적으로 하기 위해서는 무엇을 검증할 것인지 신중히 검토해야 함. Mypy와 같은 도구를 사용하면 쉽게 목적을 달성할 수 잇음.*** 35 | 36 | ## 2. 방어적 프로그래밍(Defensive programming) 37 | 38 | DbC는 계약에서 예외를 발생시키고 실패하는 모든 조건을 기술하는 대신, 방어적 프로그래밍은 객체, 함수 또는 메서드와 같은 코드의 부분을 유효하지 않은 것으로부터 보호한다는 점에서 그 접근 방식이 다르다. 방어적 프로그래밍은 특히 다른 디자인 원칙과 결합할 경우 유용하다. 39 | 40 | ### Error handling 41 | 42 | - 설명 : 예상할 수 있는 시나리오의 오류를 처리하는 방법 43 | - 주 목적 : 예상되는 에러에 대해서 실행이 계속 가능한지 또는 불가능 하여 프로그램을 중단할 것인지 결정하는 것. 44 | - 적용 상황 : 일반적으로 데이터 입력 확인 시 자주 사용됨. 45 | - 에러 처리방법 46 | - 값 대체: 기본값 또는 잘 알려진 상수, 초기값으로 결측값/결과값을 대체. 단, 오류 자체를 숨길 수 있으므로 매우 신중해야 함. 47 | - 예외 처리:예외적인 상황을 알려주고 원래의 비즈니스 로직 흐름을 유지. 단, 예외처리를 비즈니스 로직의 일부로 사용하지 말고 호출자가 알아야만 하는 실질적인 문제 발생에 대해서 사용해야 함. 48 | - 파이썬 예외처리 권장사항 49 | - 올바른 수준의 추상화 단계에서 예외처리 : 예외는 오직 한가지 일을 하는 함수의 한부분이어야 하며, 함수가 처리하는 예외는 캡슐화된 로직과 일치해야 함. 50 | 51 | class DataTransport: 52 | """다른 레벨에서 예외를 처리하는 예""" 53 | 54 | ... 55 | 56 | def deliver_event(self, event): 57 | try: 58 | self.connect() 59 | data = event.decode() 60 | self.send(data) 61 | 62 | # 관계가 없는 두 유형의 오류. 63 | 64 | # connect 메서드 내에서 처리되어야 함. 65 | # 만약 재시도 지원 시, 여기서 처리 가능 66 | except ConnectionError as e: 67 | logger.info("connection error detected: %s", e) 68 | raise 69 | 70 | # event.decode 메서드에 속한 에러. 71 | except ValueError as e: 72 | logger.error("%r contains incorrect data: %s", event, e) 73 | raise 74 | 75 | def connect(self): 76 | for _ in range(self.retry_n_times): 77 | try: 78 | self.connection = self._connector.connect() 79 | except ConnectionError as e: 80 | logger.info( 81 | "%s: attempting new connection in %is", 82 | e, 83 | self.retry_threshold, 84 | ) 85 | time.sleep(self.retry_threshold) 86 | else: 87 | return self.connection 88 | raise ConnectionError( 89 | f"Couldn't connect after {self.retry_n_times} times" 90 | ) 91 | 92 | def send(self, data): 93 | return self.connection.send(data) 94 | 95 | ... 96 | 97 | def connect_with_retry(connector, retry_n_times, retry_threshold=5): 98 | for _ in range(retry_n_times): 99 | try: 100 | return connector.connect() 101 | except ConnectionError as e: 102 | logger.info( 103 | "%s: attempting new connection in %is", e, retry_threshold 104 | ) 105 | time.sleep(retry_threshold) 106 | 107 | # ConnectionError를 따로 분리함 108 | exc = ConnectionError(f"Couldn't connect after {retry_n_times} times") 109 | logger.exception(exc) 110 | raise exc 111 | 112 | 113 | class DataTransport: 114 | retry_threshold: int = 5 115 | retry_n_times: int = 3 116 | 117 | def __init__(self, connector): 118 | self._connector = connector 119 | self.connection = None 120 | 121 | def deliver_event(self, event): 122 | self.connection = connect_with_retry( 123 | self._connector, self.retry_n_times, self.retry_threshold 124 | ) 125 | self.send(event) 126 | 127 | def send(self, event): 128 | try: 129 | return self.connection.send(event.decode()) 130 | 131 | # ValueError를 따로 분리함. 132 | except ValueError as e: 133 | logger.error("%r contains incorrect data: %s", event, e) 134 | raise 135 | 136 | - Trackback 노출금지 : 파이썬의 trackback은 많은 디버깅 정보를 포함하기 때문에 역으로 악의적인 사용자에게도 유용한 정보이다. 예외가 전파되도록 할 때, 중요한 정보를 공개하지 않도록 주의해야 한다. 137 | - 비어있는 except 블록 지양 : 에러에 대한 지나지게 방어적인 프로그래밍은 더 큰 문제를 야기한다. 이 경우 1. 보다 구체적인 예외(i.e. AttributeError, KeyError)를 사용하거나 2. except 블록에서 실제 오류 처리를 한다.(i.e. 예외상황 로깅, 기본값 반환) 138 | 139 | try: 140 | process_data() 141 | except: 142 | pass 143 | 144 | - 원본 예외 포함: 예외의 타입을 변경할 때는 항상 raise ~ from 구문을 사용한다. 145 | 146 | class InternalDataError(Exception): 147 | """업무 도메인 데이터의 예외""" 148 | 149 | 150 | def process(data_dictionary, record_id): 151 | try: 152 | return data_dictionary[record_id] 153 | except KeyError as e: 154 | raise InternalDataError("Record not present") from e 155 | 156 | ### 파이썬에서 assertion 사용하기 157 | 158 | - 발생하지 않아야 하는 오류를 처리하기 위해 사용하며 에러 발생시 바로 프로그램이 중단된다. 그렇기 때문에 assertion을 비즈니스 로직과 섞어나 제어 흐름 매커니즘으로 사용하지 않아야 한다. 159 | 160 | # assertion 사용의 안좋은 예 161 | try: 162 | assert contition.holds(), "조건에 맞지 않음" 163 | except AssertionError: 164 | alternative_procedure() 165 | 166 | # 개선 167 | result = condition.holds() 168 | assert result > 0, "에러 {0}".format(result) 169 | 170 | ## 3. 관심사의 분리(Separation of concerns) 171 | 172 | 여기서 말하는 관심사란, 프로그램內 기능의 일부분을 의미한다. 173 | 174 | - SW에서 "관심사"를 분리하는 이유 175 | - 파급효과(코드 수정에 의한 체인리액션)를 최소화 하여 코드의 유지 보수성을 향상시키기 위함. 176 | - 책임(담당하고 있는 기능)에 따라 컴포넌트/계층/모듈등 적절하게 분리. 여기서 말하는 적절하게란 각 부분이 일부의 기능에 대해서만 책임을 진다는 의미. 177 | - (4번에서 논의)적절한 캡슐화를 통해 극복 178 | 179 | --- 180 | 181 | ### 응집력 182 | 183 | : 객체가 작고 잘 정의된 목적을 가져야 함을 의미. 응집력이 높을 수록 좋음. 184 | 185 | ### 결합력 186 | 187 | : 두 개 이상의 객체간의 의존성을 나타내며 서로간의 의존성/종속성이 클수록 바람직 하지 않음. 188 | 189 | - 낮은 재사용성 : 다른 상황에서는 이 함수를 사용하기 매우 어려움. 190 | - 파급효과 : 한 군데의 수정이 다른 부분에도 영향을 크게 미침 191 | - 낮은 수준의 추상화 : 밀접한 두 함수는 서로 다른 추상화 레벨에서 문제를 해결하기 어렵기 때문에 관심사가 분리되어 있다고 보기 어려움. 192 | 193 | ***저자) 높은 응집력과 낮은 결합력을 갖는 것이 좋은 소프트웨어이다.*** 194 | 195 | ## 4. 개발 지침 약어(Acronyms to live by) 196 | 197 | 좋은 소프트웨어 관행을 약어를 통해 쉽게 기억하자 198 | 199 | --- 200 | 201 | ### DRY/OAOO 202 | 203 | : Do not repeat yourself, Once and only once를 의미하며 코드에 있는 지식이 단 한번, 단 한곳에 정의되어야 하고 또한 코드를 변경하려고 할때 수정이 필요한 곳이 한군데에만 있어야 한다는 SW best practice. 코드 중복은 아래와 같은 리스트를 발생시킴 204 | 205 | - 오류가 발생하기 쉽다 : 여러번 반복된 코드가 있다면 수정과정에서 오류 발생 가능성이 높아진다. 206 | - 비용이 비싸다. : 변경시 더 많은 시간이 들며, 팀 전체의 개발 속도를 지연시킨다. 207 | - 신뢰성이 떨어진다. : 문맥상 여러 코드를 변경하는 경우, 사람이 모든 인스턴스의 위치를 기억해야 하며, 단일 데이터 소스가 아니므로 데이터 완결성도 저하된다. 208 | 209 | ***저자) 중복을 제거하는 방법은 간단하게는 함수 생성 기법부터 컨텍스트 관리자를 이용하거나 이터레이터, 제너레이터, 데코레이터등을 활용할 수도 있다.*** 210 | 211 | ### YAGNI 212 | 213 | : You ain't gonna need it를 의미하며 과잉 엔지니어링(굳이 필요없는 개발)을 자제하라는 의미이다. 유지보수가 가능한 소프트웨어는 미래 요구 사항을 예측하는것이 아닌 오직 현재의 요구사항을 잘 해결하기위한 소프트웨어를 수정하기 쉽게 작성하는 것이다. 214 | 215 | ### KIS 216 | 217 | : Keep it simple을 의미하며 YAGNI와 유사하다. 문제를 올바르게 해결하는 최소한의 기능을 구현하고 필요 이상으로 솔루션을 복잡하게 만들지 않는다. 단순하게 유지하여 관리를 용이하게 한다. 218 | 219 | ### EAFP/LBYL 220 | 221 | : Easier to ask forgiveness than permission, look before you leap를 의미한다. 전자는 일단 코드를 실행하고 실제 동작하지 않을 경우에 대항한다는 뜻이며, 후자는 사전에 미리 확인하고 실행하라는 의미이다. 222 | 223 | ***저자) 파이썬은 EAFP방식으로 이루어졌으며 그 방식을 권장한다.*** 224 | 225 | # LBYL 226 | if os.path.exists(filename): 227 | with open(filename) as f: 228 | ... 229 | 230 | # EAFP 231 | try: 232 | with open(filename) as f: 233 | ... 234 | except: 235 | FileNotFoundError as e: 236 | logger.error(e) 237 | 238 | ## 5. 컴포지션과 상속(Composition and inheritance) 239 | 240 | OOP의 핵심개념인 상속의 장점과 단점을 알아보고 적절한 방법을 논한다. 코드 상속을 통한 코드 재사용에 있어서 올바른 방법은, 여러 상황에서 동작 가능하고 쉽게 조합할수 잇는 **응집력 높은 객체**를 개발/사용하는 것이다.(동시에 결합력은 낮은!) 241 | 242 | --- 243 | 244 | ### 상속이 좋은 선택인 경우 245 | 246 | : 상속이 양날의 검인 이유는부모 클래스를 쉽게 전수 받을수 있다는 장점과 동시에 너무 많은 기능을 가질 수 있어 사용자의 혼란을 가중시킬 수 있다는 단점도 동시에 존재하기 때문이다. 247 | 248 | - 상속의 적용이 적절하지 않은 경우 249 | - 부모 클래스가 막연한 정의와 너무 많은 책임을 가짐. 250 | - 자식 클래스는 부모 클래스의 적절한 구체화가 아님. 251 | - 상속의 적용이 잘 된 경우(일반적 클래스를 전문화/세부 추상화) 252 | - 부모 클래스의 기능을 모두 필요로 하면서 구체적인 몇가지 기능을 자식 클래스에서 추가하는 경우. i.e) (부모)BaseHTTPRequestHandler (자식)SimpleHTTPRequestHandler 253 | - 객체에 인터페이스 방식을 강제하고자 할때 구현 하지 않은 추상적인 부모 클래스를 만들고, 이 클래스를 상속하는 자식 클래스에서 적절한 구현을 하는 경우. 254 | - 파이썬의 모든 예외는 표준 예외 Exception에서 파생됨. i.e.) (조부모)IOError (부모)RequestException (자식)HTTPError 255 | 256 | ### 상속 안티패턴 257 | 258 | :도메인 문제를 해결하기 위해 적절한 데이터 구조를 만든 다음에 이 데이터 구조를 사용하는 객체를 만들지 않고 데이터 구조 자체를 객체로 만드는 경우 259 | 260 | class TransactionalPolicy(collections.UserDict): 261 | """잘못된 상속의 예""" 262 | 263 | def change_in_policy(self, customer_id, **new_policy_data): 264 | self[customer_id].update(**new_policy_data) 265 | 266 | >>> policy = TransactionalPolicy({ 267 | "client001":{ 268 | "fee":1000.0, 269 | "expiration_date":datetime(2020,1,3), 270 | } 271 | }) 272 | >>> policy["client001"] 273 | {"fee":1000.0, "expiration_date":datetime.datetime(2020,1,3,0,0)} 274 | 275 | >>> policy.change_in_policy("client001", expiration_date=datetime(2020,1,4)) 276 | >>> policy["client001"] 277 | {"fee":1000.0, "expiration_date":datetime.datetime(2020,1,4,0,0)} 278 | 279 | >>>dir(policy) 280 | [# 모든 매직 메서드는 생략... 281 | 'change_in_policy','clear','copy','data','fromkeys','get','items','keys','pop', 282 | 'popitem','setdefault','update','values'] 283 | 284 | - 위 코드의 문제점 285 | 1. TransactionalPolicy 이름을 통해 사전 타입이라는 것을 파악하기 어렵고, 노출된 public 메서드를 통해 부적절하게 전문화된 이상한 계층 구조라고 사용자들이 생각 할 것임. 286 | 2. 불필요해보이는 pop, items와 같은 public 메서드들이 그대로 노출되어 잇어서 사용자들이 임의로 호출할 수 있음. 287 | - 컴포지션을 활용한 개선 288 | 289 | : dictionary를 private 속성에 저장하고 __getitem**__**()으로 딕셔너리의 프록시를 만들고 추가 필요 public 메서드를 구현함. 290 | 291 | class TransactionalPolicy: 292 | """컴포지션을 사용한 리팩토링""" 293 | 294 | def __init__(self, policy_data, **extra_data): 295 | self._data = {**policy_data, **extra_data} 296 | 297 | def change_in_policy(self, customer_id, **new_policy_data): 298 | self._data[customer_id].update(**new_policy_data) 299 | 300 | def __getitem__(self, customer_id): 301 | return self._data[customer_id] 302 | 303 | def __len__(self): 304 | return len(self._data) 305 | 306 | ### 파이썬의 다중상속 307 | 308 | 다중 상속을 지원하는 파이썬은 자칫 잘못 구현하면 여러가지 디자인 문제를 유발한다. 309 | 310 | - 메서드 결정 순서(MRO) 311 | 312 | :아래 다이어 그램을 통해 다중 상속 구조를 이해해보자 313 | 314 | ![Figures/MRO.png](Figures/MRO.png) 315 | 316 | - 최상위 속성은 module_name 속성을 가지며 __str__메서드를 구현한다. 중간 1, 2 모듈중에 어떤 것이 하단 A12클래스의 메서드가 되는지 아래의 코드로 확인해보자. 317 | 318 | class BaseModule: 319 | module_name = "top" 320 | 321 | def __init__(self, module_name): 322 | self.name = module_name 323 | 324 | def __str__(self): 325 | return f"{self.module_name}:{self.name}" 326 | 327 | 328 | class BaseModule1(BaseModule): 329 | module_name = "module-1" 330 | 331 | 332 | class BaseModule2(BaseModule): 333 | module_name = "module-2" 334 | 335 | 336 | class BaseModule3(BaseModule): 337 | module_name = "module-3" 338 | 339 | 340 | class ConcreteModuleA12(BaseModule1, BaseModule2): 341 | """1과 2 확장""" 342 | 343 | 344 | class ConcreteModuleB23(BaseModule2, BaseModule3): 345 | """2와 3 확장""" 346 | 347 | >>> str(ConcreteModuleA12("test")) 348 | 'module-1:test' 349 | # 충돌발생X, Python에서는 메서드가 호출되는 방식을 정의하는 350 | # MRO 또는 C3 Linearization 알고리즘을 활용하여 문제를 해결함. 351 | # https://en.wikipedia.org/wiki/C3_linearization 352 | 353 | # 직접 클래스 결정 순서 확인 354 | >>> [cls.__name__ for cls in ConcreteModuleA12.mro()] 355 | ['ConcreteModuleA12','BaseModule1','BaseModule2','BaseModule','object'] 356 | 357 | - 믹스인(mixin) 358 | - 정의: 코드를 재사용하기 위해 일반적인 행동을 캡슐화해놓은 기본 클래스를 말한다. 359 | - 특징: 믹스인 클래스는 그 자체로 유용하지 않으며 대부분 클래스에 정의된 메서드나 속성에 의존한다. 360 | - 용법 : 다른 클래스와 함께 믹스인 클래스를 다중 상속하여 믹스인 내부 메서드/속성을 사용함. 361 | 362 | class BaseTokenizer: 363 | """하이픈으로 구분된 값을 반환하는 parser""" 364 | 365 | def __init__(self, str_token): 366 | self.str_token = str_token 367 | 368 | def __iter__(self): 369 | yield from self.str_token.split("-") 370 | 371 | >>> tk = BaseTokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0") 372 | >>> list(tk) 373 | ['28a2320b', 'fd3f', '4627', '9792', 'a2b38e3c46b0'] 374 | 375 | - 많은 클래스가 이미 해당 클래스를 확장했고 자식 클래스를 일일이 수정하고 싶지 않다고 가정한 상황에서 값을 대문자로 변환해 보자. 376 | 377 | class UpperIterableMixin: 378 | def __iter__(self): 379 | return map(str.upper, super().__iter__()) 380 | 381 | class Tokenizer(UpperIterableMixin, BaseTokenizer): 382 | """ 383 | 1. Tokenizer는 UpperIterableMixin에서 __iter__를 호출 384 | 2. super()를 호출하여 다음 클래스 BaseTokenizer에 위임 385 | * 이때 이미 대문자를 전달 함. 386 | """ 387 | pass 388 | 389 | >>> tk = Tokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0") 390 | >>> list(tk) 391 | ['28A2320B', 'FD3F', '4627', '9792', 'A2B38E3C46B0'] 392 | 393 | 394 | - 이러한 유형의 혼합은 일종의 데코레이터 역할을 함. 395 | 396 | ## 6. 함수와 메서드의 인자(Arguments in functions and methods) 397 | 398 | 함수의 인자 전달 매커니즘과 SW 개발 모범사례에서 발견되는 일반적인 원칙을 살펴본다. 399 | 400 | ### 파이썬의 함수 인자 동작방식 401 | 402 | - 인자는 함수에 어떻게 복사되는가 403 | - 파이썬에서는 모든 인자가 값에 의해 전달된다. 즉 함수에 값을 전달하면 함수의 변수에 할당되고 나중에 사용된다. 404 | - 만약 변경가능한 객체를 전달하고 함수 내부에서 값을 변경하면 결과에서 실제 값이 변경될 수 있다. 405 | 406 | >>> def f(arg): 407 | >>> arg += " in function" 408 | >>> print(arg) 409 | 410 | >>> immu = "hello" 411 | >>> f(immu) 412 | hello in function 413 | 414 | >>> mut = list("hello") 415 | >>> immu 416 | 'hello' 417 | 418 | >>> f(mut) 419 | ['h','e','l','l',...,'n'] 420 | 421 | >>> mut 422 | ['h','e','l','l',...,'n'] 423 | 424 | ***저자) 함수 인자를 변경하지 말아라. 최대한 함수에서 발생할 수 있는 부작용을 줄여야 한다.*** 425 | 426 | - 가변인자 427 | - 별표(*, **)를 통해 여러 인자를 패킹하여 함수에 전달하고 부분적으로 언패킹 할 수 있다. 428 | 429 | >>> l = [1,2,3] 430 | >>> f(*l) 431 | 1 432 | 2 433 | 3 434 | 435 | >>> def show(e, rest): 436 | print("요소 : {0} - 나머지: {1}".format(e, rest)) 437 | 438 | >>> first, *rest = [1,2,3,4,5] 439 | >>> show(first, rest) 440 | 요소: 1 - 나머지: [2,3,4,5] 441 | 442 | >>> *rest, last = range(6) 443 | >>> show(last, rest) 444 | 요소: 5 - 나머지: [0,1,2,3,4] 445 | 446 | >>> first, *middle, last = range(6) 447 | >>> first 448 | 0 449 | >>> middle 450 | [1,2,3,4] 451 | >>> last 452 | 5 453 | 454 | >>> first, last, *empty = (1, 2) 455 | >>> first 456 | 1 457 | >>> last 458 | 2 459 | >>> empty 460 | [] 461 | 462 | - "반복"은 변수 언패킹을 사용하기에 적절한 기능이다. 아래는 데이터베이스 결과를 리스트로 받는 함수를 가정할때, 언패킹을 사용/미사용 할때의 코드의 차이를 보여준다. 463 | 464 | USERS = [(i, f"first_name_{i}", "last_name_{i}") for i in range(1_000)] 465 | 466 | 467 | class User: 468 | def __init__(self, user_id, first_name, last_name): 469 | self.user_id = user_id 470 | self.first_name = first_name 471 | self.last_name = last_name 472 | 473 | 474 | def bad_users_from_rows(dbrows) -> list: 475 | """DB 결과에서 사용자를 생성하는 파이썬스럽지 않은 예시""" 476 | return [User(row[0], row[1], row[2]) for row in dbrows] 477 | 478 | 479 | def users_from_rows(dbrows) -> list: 480 | """DB행에서 사용자 생성: 가독성이 좋음""" 481 | return [ 482 | User(user_id, first_name, last_name) 483 | for (user_id, first_name, last_name) in dbrows 484 | ] 485 | 486 | ### 함수 인자의 개수 487 | 488 | : 함수가 너무 많은 인자를 가진다면, 나쁜 디자인일 가능성이 높다. 이런 경우 일반적으로... 489 | 490 | 1. 구체화(전달하는 모든 인자를 포함하는 새로운 객체를 만듦)를 통해 일반적 SW 디자인의 원칙을 사용한다. 491 | 2. 가변인자나 키워드 인자를 사용하여 동적 서명을 가진 함수를 만든다. 단, 매우 동적이어서 유지보수가 어렵기 때문에 남용하지 않도록 주의한다. 492 | 3. 여러 작은 (한가지 기능을 담당하는)함수로 분리한다. 493 | - 함수 인자와 결합력 494 | - 함수의 인자가 많을수록 호출하기 위한 정보를 수집하기가 점점 더 어려워 진다. 495 | - 함수의 추상화가 너무 적으면 다른 환경에서 사용하기 어렵거나(재사용성↓), 다른 함수에 지나치게 의존적이 된다. 496 | - 함수가 너무 많은 인자를 가진다면, 나쁜 디자인일 가능성이 높다. 497 | - 많은 인자를 취하는 작은 함수의 서명 498 | - 공통 객체에 파라미터 대부분이 포함되어 있는 경우 리팩토링 하기 499 | 500 | track_request(request.headers, request.ip_addr, request.request_id) 501 | # 모든 파라미터가 request와 관련이 있으므로 그냥 request를 파라미터로 502 | # 전달하는 것으로 변경한다. 단, 함수가 전달받은 객체를 변경하지 않도록 한다. 503 | 504 | - 하나의 객체에 파라미터를 담아 전달한다. 파라미터 그룹핑이라고 한다. 505 | - 함수의 서명을 변경하여 다양한 인자를 허용한다. 단, 인터페이스에 대한 문서화를 하고 정확하게 사용했는지 확인해야 한다. 506 | - *arg, **kwarg를 사용한다. 단, 이 경우 서명을 잃어버리고 가독성을 거의 상실하므로 사용에 주의한다. 507 | 508 | ## 7. 소프트웨어 디자인 우수 사례 결론(Final remarks on good practices for software design) 509 | 510 | ### 소프트웨어의 독립성(Orthogonality) 511 | 512 | : SW의 런타임 구조 측면에서 직교란, 변경(또는 부작용)을 내부 문제로 만드는 것이다. 예를들어 어떤 객체의 메서드를 호출하는 것이 다른 관련없는 객체의 내부 상태를 변경해서는 안된다. 513 | 514 | - 아래의 코드에서, 가격 계산함수와 표시함수는 서로 독립이다. 이를 알고 있다면, 둘중 하나의 함수를 쉽게 변경할 수 있으며 두 함수에 대한 단위 테스트가 성공하면 전체 테스트도 필요치 않다. 515 | 516 | # 가격 계산함수 517 | def calculate_price(base_price: float, tax: float, discount: float) -> float: 518 | return (base_price * (1 + tax)) * (1 - discount) 519 | 520 | # 가격 표시함수 521 | def show_price(price: float) -> str: 522 | return "$ {0:,.2f}".format(price) 523 | 524 | 525 | def str_final_price( 526 | base_price: float, tax: float, discount: float, fmt_function=str 527 | ) -> str: 528 | return fmt_function(calculate_price(base_price, tax, discount)) 529 | 530 | >>> str_final_price(10, 0.2, 0.5) 531 | '0.6' 532 | 533 | >>> str_final_price(1000, 0.2, 0) 534 | '1200.0' 535 | 536 | >>> str_final_price(1000, 0.2, 0.1, fmt_function=show_price) 537 | '$ 1,080.00' 538 | 539 | ### 코드 구조 540 | 541 | : 팀의 작업 효율성과 유지보수성을 위해 코드를 구조화 한다. 542 | 543 | - 유사한 컴포넌트끼리 정리하여 구조화 한다. 544 | - __init__.py를 가진 디렉토리를 통해 파이썬 패키지를 만들면, 코드간의 종속성이 있어도 전체적인 호환성을 유지한다.(__init__.py 파일에 다른 파일에 있던 모든 정의를 가져올 수 있다) 545 | - *모듈을 import할때 구문을 분석하고 메모리에 로드할 객체가 줄어든다.* 546 | - *의존성이 줄었기 때문에 더 적은 모듈만 가져오면 된다.* 547 | - config에 정의가 필요한 상수를 정의하고 일괄 import하여 정보를 중앙화 한다. 548 | 549 | 자세한 내용은 10장에서 다룰예정. 550 | 551 | ## 요약 552 | 553 | 1. 고품질 SW를 만들기 위한 핵심은 코드도 디자인의 일부라는 것을 이해하는 것이다. 554 | 2. DbC(계약에 의한 디자인)을 적용하면 주어진 조건 하에서 동작이 보장되는 컴포넌트를 만들수 있다. 555 | 1. 오류가 발생하면 원인이 무엇이며 어떤 부분이 계약을 파기했는지 명확히 알수 있다. 556 | 2. 위와 같은 역할분담은 효과적인 디버깅에 유리하다. 557 | 3. 방어적 프로그래밍을 통해 악의적인 의도를 가진 또는 잘못된 입력으로부터 스스로를 보호하면 개별 컴포넌트를 더욱 강력하게 만들수 있다. 558 | 4. 2, 3번 모두 Assertion을 올바르게 사용해야 한다. 프로그램 흐름제어 용도나 예외에서 처리하기 위한 용도로 쓰는것은 부적절하다. 559 | 5. 예외는 언제 어떻게 사용해야 하는지는 아는것이 중요하다. 제어흐룸 수단으로 쓰는것은 부적절하다. 560 | 6. OOP디자인에서는 부모 자식클래스간의 적절한 옵션을 사용하는 것이 중요하다. 또한 파이썬의 높은 동적 특성으로 발생하는 안티 패턴을 피해야 한다. 561 | 7. 파이썬의 특수성을 고려한 파라미터 갯수 관리가 필요하다. -------------------------------------------------------------------------------- /Ch7_Generator.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Chapter 7. 제너레이터 사용하기\n", 8 | "\n", 9 | "## 제너레이터 만들기\n", 10 | "\n", 11 | "+ 제너레이터는 한번에 하나씩 구성요소를 반환해주는 이터러블을 생성해주는 객체\n", 12 | "+ 메모리를 적게 사용하는 반복을 위한 방법으로 2001년에 소개됨\n", 13 | "+ yield 키워드를 사용하면 제너레이터 함수가 됨\n", 14 | "+ gettemdir 설명 " 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import os\n", 24 | "from tempfile import gettempdir\n", 25 | "\n", 26 | "PURCHASES_FILE = os.path.join(gettempdir(), \"purchases.csv\")\n", 27 | "\n", 28 | "\n", 29 | "def create_purchases_file(filename, entries=1_000_000):\n", 30 | " if os.path.exists(PURCHASES_FILE):\n", 31 | " return\n", 32 | "\n", 33 | " with open(filename, \"w+\") as f:\n", 34 | " for i in range(entries):\n", 35 | " line = f\"2018-01-01,{i}\\n\"\n", 36 | " f.write(line)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 2, 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "name": "stderr", 46 | "output_type": "stream", 47 | "text": [ 48 | "Results: PurchasesStats(0.0, 999999.0, 499999.5)\n" 49 | ] 50 | } 51 | ], 52 | "source": [ 53 | "import logging\n", 54 | "\n", 55 | "logging.basicConfig(level=logging.INFO, format=\"%(message)s\")\n", 56 | "logger = logging.getLogger(__name__)\n", 57 | "\n", 58 | "class PurchasesStats:\n", 59 | " def __init__(self, purchases):\n", 60 | " self.purchases = iter(purchases)\n", 61 | " self.min_price: float = None\n", 62 | " self.max_price: float = None\n", 63 | " self._total_purchases_price: float = 0.0\n", 64 | " self._total_purchases = 0\n", 65 | " self._initialize()\n", 66 | "\n", 67 | " def _initialize(self):\n", 68 | " try:\n", 69 | " first_value = next(self.purchases)\n", 70 | " except StopIteration:\n", 71 | " raise ValueError(\"no values provided\")\n", 72 | "\n", 73 | " self.min_price = self.max_price = first_value\n", 74 | " self._update_avg(first_value)\n", 75 | "\n", 76 | " def process(self):\n", 77 | " for purchase_value in self.purchases:\n", 78 | " self._update_min(purchase_value)\n", 79 | " self._update_max(purchase_value)\n", 80 | " self._update_avg(purchase_value)\n", 81 | " return self\n", 82 | "\n", 83 | " def _update_min(self, new_value: float):\n", 84 | " if new_value < self.min_price:\n", 85 | " self.min_price = new_value\n", 86 | "\n", 87 | " def _update_max(self, new_value: float):\n", 88 | " if new_value > self.max_price:\n", 89 | " self.max_price = new_value\n", 90 | "\n", 91 | " @property\n", 92 | " def avg_price(self):\n", 93 | " return self._total_purchases_price / self._total_purchases\n", 94 | "\n", 95 | " def _update_avg(self, new_value: float):\n", 96 | " self._total_purchases_price += new_value\n", 97 | " self._total_purchases += 1\n", 98 | "\n", 99 | " def __str__(self):\n", 100 | " return (\n", 101 | " f\"{self.__class__.__name__}({self.min_price}, \"\n", 102 | " f\"{self.max_price}, {self.avg_price})\"\n", 103 | " )\n", 104 | "\n", 105 | "\n", 106 | "def _load_purchases(filename):\n", 107 | " \"\"\"\n", 108 | " list 사용\n", 109 | " \"\"\"\n", 110 | " purchases = []\n", 111 | " with open(filename) as f:\n", 112 | " for line in f:\n", 113 | " *_, price_raw = line.partition(\",\")\n", 114 | " purchases.append(float(price_raw))\n", 115 | "\n", 116 | " return purchases\n", 117 | "\n", 118 | "\n", 119 | "def load_purchases(filename):\n", 120 | " \"\"\"\n", 121 | " 제너레이터\n", 122 | " \"\"\"\n", 123 | " with open(filename) as f:\n", 124 | " for line in f:\n", 125 | " *_, price_raw = line.partition(\",\")\n", 126 | " yield float(price_raw)\n", 127 | "\n", 128 | "\n", 129 | "def main():\n", 130 | " create_purchases_file(PURCHASES_FILE)\n", 131 | " purchases = load_purchases(PURCHASES_FILE)\n", 132 | " stats = PurchasesStats(purchases).process()\n", 133 | " logger.info(\"Results: %s\", stats)\n", 134 | "\n", 135 | "\n", 136 | "if __name__ == \"__main__\":\n", 137 | " main()" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "### 제너레이터 표현식\n", 145 | "```\n", 146 | "sum(x**2 for x in range(10)\n", 147 | "```" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "## 이상적인 반복\n", 155 | "### 관용적인 반복코드" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 3, 161 | "metadata": {}, 162 | "outputs": [ 163 | { 164 | "data": { 165 | "text/plain": [ 166 | "[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'), (5, 'f')]" 167 | ] 168 | }, 169 | "execution_count": 3, 170 | "metadata": {}, 171 | "output_type": "execute_result" 172 | } 173 | ], 174 | "source": [ 175 | "class SequenceOfNumber():\n", 176 | "\n", 177 | " def __init__(self, start=0):\n", 178 | " self.current = start\n", 179 | " \n", 180 | " def __next__(self):\n", 181 | " current, self.current = self.current, self.current + 1\n", 182 | " return current\n", 183 | " \n", 184 | " def __iter__(self):\n", 185 | " return self\n", 186 | " \n", 187 | "list(zip(SequenceOfNumber(0), \"abcdef\"))" 188 | ] 189 | }, 190 | { 191 | "cell_type": "markdown", 192 | "metadata": {}, 193 | "source": [ 194 | "#### next함수\n", 195 | "+ 다음 요소로 이동시키고 기존의 값을 반환\n", 196 | "+ 이터레이터가 더 이상의 값을 가지고 있지 않다면 StopIteration 예외가 발생함\n", 197 | "+ StopIteration 예외 대신 기본값을 반환할 수 있음" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 4, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "data": { 207 | "text/plain": [ 208 | "'c'" 209 | ] 210 | }, 211 | "execution_count": 4, 212 | "metadata": {}, 213 | "output_type": "execute_result" 214 | } 215 | ], 216 | "source": [ 217 | "word = iter(\"abc\")\n", 218 | "next(word)\n", 219 | "next(word)\n", 220 | "next(word)" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 5, 226 | "metadata": {}, 227 | "outputs": [ 228 | { 229 | "ename": "StopIteration", 230 | "evalue": "", 231 | "output_type": "error", 232 | "traceback": [ 233 | "\u001b[0;31m\u001b[0m", 234 | "\u001b[0;31mStopIteration\u001b[0mTraceback (most recent call last)", 235 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnext\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mword\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 236 | "\u001b[0;31mStopIteration\u001b[0m: " 237 | ] 238 | } 239 | ], 240 | "source": [ 241 | "next(word)" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": null, 247 | "metadata": {}, 248 | "outputs": [], 249 | "source": [ 250 | "next(word, \"default value\")" 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | "#### 제너레이터 사용하기\n", 258 | "+ yield를 활용하여 클래스 사용없이 간단하게 구현 가능" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "def sequnce(num):\n", 268 | " curr = 0\n", 269 | " while(True):\n", 270 | " retrun_val, curr = curr, curr + 1\n", 271 | " yield retrun_val\n", 272 | " \n", 273 | "list(zip(sequnce(0), \"abcdef\")) " 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "#### itertools\n", 281 | "+ islice : 원하는 갯수만큼 잘라서 반환\n", 282 | "+ tee : 이터러블을 원하는 수만큼 복제" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "\"\"\" 특정 조건의 가격만 계산하기 \"\"\"\n", 292 | "from itertools import islice, tee\n", 293 | "\n", 294 | "purchases = islice(filter(lambda x: x > 1000.0, load_purchases(PURCHASES_FILE)), 10) # 처음 10개만 처리\n", 295 | "stats = PurchasesStats(purchases).process()\n", 296 | "logger.info(\"Results: %s\", stats)\n", 297 | "\n", 298 | "\n", 299 | "\"\"\" 이터러블 복제하기 \"\"\"\n", 300 | "def process_purchases(purchases):\n", 301 | " min_, max_ = tee(purchases, 2)\n", 302 | " return min(min_), max(max_)\n", 303 | "\n", 304 | "purchases = load_purchases(PURCHASES_FILE)\n", 305 | "logger.info(\"Results: %d, %d\", *process_purchases(purchases))" 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": {}, 311 | "source": [ 312 | "#### 중첩 루프" 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": 6, 318 | "metadata": {}, 319 | "outputs": [ 320 | { 321 | "name": "stderr", 322 | "output_type": "stream", 323 | "text": [ 324 | "value 12 found at [2, 3]\n" 325 | ] 326 | } 327 | ], 328 | "source": [ 329 | "def search_nested_bad(array, desired_value):\n", 330 | " \"\"\"Example of an iteration in a nested loop.\"\"\"\n", 331 | " coords = None\n", 332 | " for i, row in enumerate(array):\n", 333 | " for j, cell in enumerate(row):\n", 334 | " if cell == desired_value:\n", 335 | " coords = (i, j)\n", 336 | " break\n", 337 | "\n", 338 | " if coords is not None:\n", 339 | " break\n", 340 | "\n", 341 | " if coords is None:\n", 342 | " raise ValueError(f\"{desired_value} not found\")\n", 343 | "\n", 344 | " logger.info(\"value %r found at [%i, %i]\", desired_value, *coords)\n", 345 | " return coords\n", 346 | "\n", 347 | "\n", 348 | "def _iterate_array2d(array2d):\n", 349 | " for i, row in enumerate(array2d):\n", 350 | " for j, cell in enumerate(row):\n", 351 | " yield (i, j), cell\n", 352 | "\n", 353 | "\n", 354 | "def search_nested(array, desired_value):\n", 355 | " \"\"\"\"Searching in multiple dimensions with a single loop.\"\"\"\n", 356 | " try:\n", 357 | " coord = next(\n", 358 | " coord\n", 359 | " for (coord, cell) in _iterate_array2d(array)\n", 360 | " if cell == desired_value\n", 361 | " )\n", 362 | " except StopIteration:\n", 363 | " raise ValueError(f\"{desired_value} not found\")\n", 364 | "\n", 365 | " logger.info(\"value %r found at [%i, %i]\", desired_value, *coord)\n", 366 | " return coord\n", 367 | "\n", 368 | "\n", 369 | "if __name__ == \"__main__\":\n", 370 | " array2d = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]\n", 371 | " # search_nested_bad(array2d, 13) 같은 결과이나 코드가 복잡\n", 372 | " search_nested(array2d, 12)" 373 | ] 374 | }, 375 | { 376 | "cell_type": "markdown", 377 | "metadata": {}, 378 | "source": [ 379 | "### 파이썬의 이터레이터 패턴\n", 380 | "+ \\_\\_iter\\_\\_ 매직 메서드를 통해 이터레이터를 반환하고, \\_\\_next\\_\\_ 매직 메서드를 통해 반복 로직을 구현\n", 381 | "+ 모든 제너레이터는 이터레이터임\n", 382 | "+ \\_\\_len()\\_\\_, \\_\\_getitem\\_\\_()를 구현한 시퀀스 객체도 반복 가능함" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": 7, 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "class SequenceWrapper:\n", 392 | " def __init__(self, original_sequence):\n", 393 | " self.seq = original_sequence\n", 394 | "\n", 395 | " def __getitem__(self, item):\n", 396 | " value = self.seq[item]\n", 397 | " logger.info(\"%s getting %s\", self.__class__.__name__, item)\n", 398 | " return value\n", 399 | "\n", 400 | " def __len__(self):\n", 401 | " return len(self.seq)\n", 402 | "\n", 403 | "\n", 404 | "class MappedRange:\n", 405 | " \"\"\"Apply a transformation to a range of numbers.\"\"\"\n", 406 | "\n", 407 | " def __init__(self, transformation, start, end):\n", 408 | " self._transformation = transformation\n", 409 | " self._wrapped = range(start, end)\n", 410 | "\n", 411 | " def __getitem__(self, index):\n", 412 | " value = self._wrapped.__getitem__(index)\n", 413 | " result = self._transformation(value)\n", 414 | " logger.info(\"Index %d: %s\", index, result)\n", 415 | " return result\n", 416 | "\n", 417 | " def __len__(self):\n", 418 | " return len(self._wrapped)" 419 | ] 420 | }, 421 | { 422 | "cell_type": "code", 423 | "execution_count": 8, 424 | "metadata": {}, 425 | "outputs": [ 426 | { 427 | "name": "stderr", 428 | "output_type": "stream", 429 | "text": [ 430 | "Index 0: 2\n", 431 | "Index 0: 2\n", 432 | "Index 1: 3\n", 433 | "Index 2: 4\n" 434 | ] 435 | }, 436 | { 437 | "name": "stdout", 438 | "output_type": "stream", 439 | "text": [ 440 | "2\n" 441 | ] 442 | }, 443 | { 444 | "data": { 445 | "text/plain": [ 446 | "[2, 3, 4]" 447 | ] 448 | }, 449 | "execution_count": 8, 450 | "metadata": {}, 451 | "output_type": "execute_result" 452 | } 453 | ], 454 | "source": [ 455 | "seq = SequenceWrapper((0,1,2,3,4,5,6,7,8,9))\n", 456 | "mr = MappedRange(abs, 2, 5)\n", 457 | "print(mr[0])\n", 458 | "list(mr)" 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "## 코루틴(coroutine)" 466 | ] 467 | }, 468 | { 469 | "cell_type": "markdown", 470 | "metadata": {}, 471 | "source": [ 472 | "+ 코루틴의 인터페이스\n", 473 | " + close()\n", 474 | " + 제너레이터에서 GeneratorExit 예외가 발생하며, 이 예외를 따로 처리하지 않으면 제너레이터 더 이상 값을 생성하지 않으며 반복이 중지됨\n", 475 | " + throw(ex_type[, ex_value[, ex_traceback]])\n", 476 | " + 현재 제너레이터가 중단된 위치에서 예외를 던짐\n", 477 | " + send(value)\n", 478 | " + 제너레이터와 코루틴을 구분하는 기준으로 yield 키워드가 할당 구문의 오른쪽에 나오게 되고 인자 값을 받아서 다른 곳에서 할당할 수 있음을 뜻함\n", 479 | " + next()를 먼저 호출해야 함\n", 480 | " + next()를 자동 호출해주는 @prepare_coroutine을 사용하면 편리함" 481 | ] 482 | }, 483 | { 484 | "cell_type": "code", 485 | "execution_count": 9, 486 | "metadata": {}, 487 | "outputs": [ 488 | { 489 | "name": "stdout", 490 | "output_type": "stream", 491 | "text": [ 492 | ">>> returned value 3\n" 493 | ] 494 | } 495 | ], 496 | "source": [ 497 | "\"\"\" Stop exception을 활용한 리턴 방법(실제로 쓰이진 않음) \"\"\"\n", 498 | "def generator():\n", 499 | " yield 1\n", 500 | " yield 2\n", 501 | " return 3\n", 502 | "\n", 503 | "gen = generator()\n", 504 | "\n", 505 | "while(True):\n", 506 | " try:\n", 507 | " next(gen)\n", 508 | " except StopIteration as e:\n", 509 | " print(\">>> returned value\", e.value)\n", 510 | " break" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": 10, 516 | "metadata": {}, 517 | "outputs": [], 518 | "source": [ 519 | "\"\"\" 간단한 yield from 사례 \"\"\" \n", 520 | "def chain(*iterables):\n", 521 | " for it in iterables:\n", 522 | " for values in it:\n", 523 | " yield value" 524 | ] 525 | }, 526 | { 527 | "cell_type": "code", 528 | "execution_count": 11, 529 | "metadata": {}, 530 | "outputs": [], 531 | "source": [ 532 | "def chain(*iterables):\n", 533 | " for it in iterables:\n", 534 | " yield from it # " 535 | ] 536 | }, 537 | { 538 | "cell_type": "code", 539 | "execution_count": 12, 540 | "metadata": {}, 541 | "outputs": [ 542 | { 543 | "data": { 544 | "text/plain": [ 545 | "['h', 'e', 'l', 'l', 'o', 'world', 'tuple', 'value']" 546 | ] 547 | }, 548 | "execution_count": 12, 549 | "metadata": {}, 550 | "output_type": "execute_result" 551 | } 552 | ], 553 | "source": [ 554 | "list(chain(\"hello\", [\"world\"], (\"tuple\", \"value\")))" 555 | ] 556 | }, 557 | { 558 | "cell_type": "code", 559 | "execution_count": 13, 560 | "metadata": {}, 561 | "outputs": [ 562 | { 563 | "name": "stderr", 564 | "output_type": "stream", 565 | "text": [ 566 | "first started at 0\n", 567 | "first finished at 5\n", 568 | "second started at 5\n", 569 | "second finished at 10\n" 570 | ] 571 | }, 572 | { 573 | "name": "stdout", 574 | "output_type": "stream", 575 | "text": [ 576 | ">>> returned value 15\n" 577 | ] 578 | } 579 | ], 580 | "source": [ 581 | "\"\"\" 서브 제너레이터에서 반환된 값 구하기\n", 582 | "yield from을 사용하면 코루틴의 종료 시 최종 반환 값을 구할 수 있음\n", 583 | "\"\"\"\n", 584 | "\n", 585 | "def sequence(name, start, end):\n", 586 | " logger.info(\"%s started at %i\", name, start)\n", 587 | " yield from range(start, end)\n", 588 | " logger.info(\"%s finished at %i\", name, end)\n", 589 | " return end\n", 590 | "\n", 591 | "def main():\n", 592 | " step1 = yield from sequence(\"first\", 0, 5)\n", 593 | " step2 = yield from sequence(\"second\", step1, 10)\n", 594 | " return step1 + step2\n", 595 | "\n", 596 | "g = main()\n", 597 | "\n", 598 | "while(True): # yield from을 처음부터 쓸 수는 없음\n", 599 | " try:\n", 600 | " next(g)\n", 601 | " except StopIteration as e:\n", 602 | " print(\">>> returned value\", e.value) # 리턴값을 반환받을 수 있음\n", 603 | " break" 604 | ] 605 | }, 606 | { 607 | "cell_type": "code", 608 | "execution_count": 14, 609 | "metadata": { 610 | "jupyter": { 611 | "source_hidden": true 612 | } 613 | }, 614 | "outputs": [ 615 | { 616 | "name": "stderr", 617 | "output_type": "stream", 618 | "text": [ 619 | "first started at 0\n", 620 | "first received None\n", 621 | "first received None\n", 622 | "first received None\n", 623 | "first received None\n", 624 | "first received None\n", 625 | "second started at 5\n", 626 | "second received None\n", 627 | "second received None\n", 628 | "second received None\n", 629 | "second received None\n", 630 | "second received None\n" 631 | ] 632 | }, 633 | { 634 | "name": "stdout", 635 | "output_type": "stream", 636 | "text": [ 637 | "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n" 638 | ] 639 | } 640 | ], 641 | "source": [ 642 | "\"\"\" 서브 제너레이터와 테이터 송수신하기 \"\"\"\n", 643 | "\n", 644 | "class CustomException(Exception):\n", 645 | " \"\"\"A type of exception that is under control.\"\"\"\n", 646 | "\n", 647 | "\n", 648 | "def sequence(name, start, end):\n", 649 | " value = start\n", 650 | " logger.info(\"%s started at %i\", name, value)\n", 651 | " while value < end:\n", 652 | " try:\n", 653 | " received = yield value\n", 654 | " logger.info(\"%s received %r\", name, received)\n", 655 | " value += 1\n", 656 | " except CustomException as e:\n", 657 | " logger.info(\"%s is handling %s\", name, e)\n", 658 | " received = yield \"OK\"\n", 659 | " return end\n", 660 | "\n", 661 | "\n", 662 | "def main():\n", 663 | " step1 = yield from sequence(\"first\", 0, 5)\n", 664 | " step2 = yield from sequence(\"second\", step1, 10)\n", 665 | "\n", 666 | "if __name__ == \"__main__\":\n", 667 | " print(list(main()))" 668 | ] 669 | } 670 | ], 671 | "metadata": { 672 | "kernelspec": { 673 | "display_name": "Python 3", 674 | "language": "python", 675 | "name": "python3" 676 | }, 677 | "language_info": { 678 | "codemirror_mode": { 679 | "name": "ipython", 680 | "version": 3 681 | }, 682 | "file_extension": ".py", 683 | "mimetype": "text/x-python", 684 | "name": "python", 685 | "nbconvert_exporter": "python", 686 | "pygments_lexer": "ipython3", 687 | "version": "3.7.6" 688 | } 689 | }, 690 | "nbformat": 4, 691 | "nbformat_minor": 4 692 | } 693 | -------------------------------------------------------------------------------- /Ch7-1_Concurrency.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Async programming" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## I/O 대기\n", 15 | "+ CPU 클럭의 속도가 1초라고 가정했을 때\n", 16 | "+ RAM은 20초, SSD는 4일, HDD 6개월, 네트워크 전송은?" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## 동시성\n", 24 | "+ 여러 개의 요청을 동시에 다룰 수 있는 시스템을 구현하는 방식\n", 25 | "+ 아래는 동기식으로 구현된 기본 함수" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 1, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import time \n", 35 | "import functools\n", 36 | "\n", 37 | "\n", 38 | "DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}, {kwargs}) -> {result}'\n", 39 | "\n", 40 | "def clock(fmt=DEFAULT_FMT): \n", 41 | " def decorate(func): \n", 42 | " @functools.wraps(func)\n", 43 | " def clocked(*_args, **_kwargs): # clocked에서 *, ** 키워드를 통해 설정된 인수를 변수화\n", 44 | " t0 = time.time()\n", 45 | " _result = func(*_args)\n", 46 | " elapsed = time.time() - t0\n", 47 | " name = func.__name__\n", 48 | " args = ', '.join(repr(arg) for arg in _args)\n", 49 | " pairs = ['%s=%r' % (k, w) for k, w in sorted(_kwargs.items())]\n", 50 | " kwargs = ', '.join(pairs)\n", 51 | " result = repr(_result)\n", 52 | " print(fmt.format(**locals()))\n", 53 | " return _result # clocked()는 데커레이트된 함수를 대체하므로, 원래 함수가 반환하는 값을 반환해야 한다.\n", 54 | " return clocked # decorate()는 clocked()를 반환한다. \n", 55 | " return decorate # clock()은 decorate()를 반환한다. " 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 2, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "name": "stdout", 65 | "output_type": "stream", 66 | "text": [ 67 | "[1.00051713s] fetch_square(0, ) -> 0\n", 68 | "[1.00127792s] fetch_square(1, ) -> 1\n", 69 | "[1.00123954s] fetch_square(2, ) -> 4\n", 70 | "[1.00055385s] fetch_square(3, ) -> 9\n", 71 | "[1.00105548s] fetch_square(4, ) -> 16\n", 72 | "[1.00137401s] fetch_square(5, ) -> 25\n", 73 | "[1.00112104s] fetch_square(6, ) -> 36\n", 74 | "[1.00127673s] fetch_square(7, ) -> 49\n", 75 | "[1.00112653s] fetch_square(8, ) -> 64\n", 76 | "[1.00130653s] fetch_square(9, ) -> 81\n", 77 | "Total: 10.016206741333008 sec\n" 78 | ] 79 | } 80 | ], 81 | "source": [ 82 | "def network_request(number):\n", 83 | " time.sleep(1.0)\n", 84 | " return {\"success\": True, \"result\": number ** 2}\n", 85 | "\n", 86 | "\n", 87 | "@clock()\n", 88 | "def fetch_square(number):\n", 89 | " response = network_request(number)\n", 90 | " if response[\"success\"]:\n", 91 | " return response[\"result\"]\n", 92 | "\n", 93 | " \n", 94 | "if __name__ == \"__main__\":\n", 95 | " t0 = time.time()\n", 96 | "\n", 97 | " returns = [fetch_square(i) for i in range(10)]\n", 98 | " \n", 99 | " elapsed = time.time() - t0\n", 100 | " print(\"Total:\", elapsed, \"sec\")" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "### Callback\n", 108 | "\n", 109 | "+ 원리만 이해하고 복잡하니 쓰지 말자" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 3, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "import threading\n", 119 | "\n", 120 | "\n", 121 | "def network_request_async(number, on_done):\n", 122 | " \n", 123 | " def timer_done():\n", 124 | " time.sleep(1.0)\n", 125 | " on_done({\"success\": True, \"result\": number ** 2})\n", 126 | "\n", 127 | " timer = threading.Thread(target=timer_done, args=[])\n", 128 | " timer.start()\n", 129 | " \n", 130 | " \n", 131 | "@clock()\n", 132 | "def fetch_square_async(number):\n", 133 | " \n", 134 | " def on_done(response):\n", 135 | " if response[\"success\"]:\n", 136 | " return response[\"result\"]\n", 137 | " \n", 138 | " network_request_async(number, on_done)" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 4, 144 | "metadata": {}, 145 | "outputs": [ 146 | { 147 | "name": "stdout", 148 | "output_type": "stream", 149 | "text": [ 150 | "[0.00027442s] fetch_square_async(0, ) -> None\n", 151 | "[0.00050879s] fetch_square_async(1, ) -> None\n", 152 | "[0.00332260s] fetch_square_async(2, ) -> None\n", 153 | "[0.00051570s] fetch_square_async(3, ) -> None\n", 154 | "[0.00053382s] fetch_square_async(4, ) -> None\n", 155 | "[0.00039411s] fetch_square_async(5, ) -> None\n", 156 | "[0.00046110s] fetch_square_async(6, ) -> None\n", 157 | "[0.00057125s] fetch_square_async(7, ) -> None\n", 158 | "[0.00052476s] fetch_square_async(8, ) -> None\n", 159 | "[0.00072122s] fetch_square_async(9, ) -> None\n", 160 | "Total: 0.009291410446166992 sec\n" 161 | ] 162 | } 163 | ], 164 | "source": [ 165 | "if __name__ == \"__main__\":\n", 166 | " t0 = time.time()\n", 167 | "\n", 168 | " returns = [fetch_square_async(i) for i in range(10)]\n", 169 | " \n", 170 | " elapsed = time.time() - t0\n", 171 | " print(\"Total:\", elapsed, \"sec\")" 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "### Future\n", 179 | "\n", 180 | "+ 요청한 자원을 추적하고 가용하게 될 때 까지 대기하는 데 도움이 되는 추상화" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 5, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "from concurrent.futures import Future\n", 190 | "\n", 191 | "def callback(future):\n", 192 | " print(future.result()[::-1])\n", 193 | "\n", 194 | "fut = Future()\n", 195 | "fut.add_done_callback(callback)" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 6, 201 | "metadata": {}, 202 | "outputs": [ 203 | { 204 | "data": { 205 | "text/plain": [ 206 | "{'_condition': , 0)>,\n", 207 | " '_state': 'PENDING',\n", 208 | " '_result': None,\n", 209 | " '_exception': None,\n", 210 | " '_waiters': [],\n", 211 | " '_done_callbacks': []}" 212 | ] 213 | }, 214 | "execution_count": 6, 215 | "metadata": {}, 216 | "output_type": "execute_result" 217 | } 218 | ], 219 | "source": [ 220 | "vars(fut)" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": 7, 226 | "metadata": {}, 227 | "outputs": [ 228 | { 229 | "name": "stdout", 230 | "output_type": "stream", 231 | "text": [ 232 | "OLLEH\n" 233 | ] 234 | } 235 | ], 236 | "source": [ 237 | "fut.set_result(\"HELLO\") ## 퓨쳐를 리턴함" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 8, 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "data": { 247 | "text/plain": [ 248 | "{'_condition': , 0)>,\n", 249 | " '_state': 'FINISHED',\n", 250 | " '_result': 'HELLO',\n", 251 | " '_exception': None,\n", 252 | " '_waiters': [],\n", 253 | " '_done_callbacks': []}" 254 | ] 255 | }, 256 | "execution_count": 8, 257 | "metadata": {}, 258 | "output_type": "execute_result" 259 | } 260 | ], 261 | "source": [ 262 | "vars(fut)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "+ Network_request_async 를 쓰레드 방식으로 변환" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 9, 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "def network_reqeust_async(number):\n", 279 | " future = Future()\n", 280 | " result = {\"success\": True, \"result\": number ** 2}\n", 281 | " timer = threading.Timer(1.0, lambda: future.set_result(result))\n", 282 | " timer.start()\n", 283 | " return future\n", 284 | "\n", 285 | "\n", 286 | "@clock()\n", 287 | "def fetch_square_async(number):\n", 288 | " fut = network_reqeust_async(number)\n", 289 | "\n", 290 | " def on_done_future(future):\n", 291 | " response = future.result()\n", 292 | " if response[\"success\"]:\n", 293 | "# return response[\"result\"]\n", 294 | " print(response[\"result\"], flush=True) \n", 295 | " \n", 296 | " fut.add_done_callback(on_done_future)" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 10, 302 | "metadata": {}, 303 | "outputs": [ 304 | { 305 | "name": "stdout", 306 | "output_type": "stream", 307 | "text": [ 308 | "[0.00061083s] fetch_square_async(0, ) -> None\n", 309 | "[0.00058317s] fetch_square_async(1, ) -> None\n", 310 | "[0.00023699s] fetch_square_async(2, ) -> None\n", 311 | "[0.00046134s] fetch_square_async(3, ) -> None\n", 312 | "[0.00120807s] fetch_square_async(4, ) -> None\n", 313 | "[0.00201869s] fetch_square_async(5, ) -> None\n", 314 | "[0.00043750s] fetch_square_async(6, ) -> None\n", 315 | "[0.00081587s] fetch_square_async(7, ) -> None\n", 316 | "[0.00057101s] fetch_square_async(8, ) -> None\n", 317 | "[0.00039887s] fetch_square_async(9, ) -> None\n", 318 | "Total: 0.008257627487182617 sec\n", 319 | "0\n", 320 | "1\n", 321 | "49\n", 322 | "\n", 323 | "16\n", 324 | "2536\n", 325 | "\n", 326 | "4964\n", 327 | "\n", 328 | "81\n" 329 | ] 330 | } 331 | ], 332 | "source": [ 333 | "if __name__ == \"__main__\":\n", 334 | " t0 = time.time()\n", 335 | "\n", 336 | " returns = [fetch_square_async(i) for i in range(10)]\n", 337 | " \n", 338 | " elapsed = time.time() - t0\n", 339 | " print(\"Total:\", elapsed, \"sec\")" 340 | ] 341 | }, 342 | { 343 | "cell_type": "markdown", 344 | "metadata": {}, 345 | "source": [ 346 | "## asyncio 프레임워크\n", 347 | "+ loop = asyncio.get_event_loop() 함수를 호출해 asyncio 루프를 얻을 수 있음\n", 348 | "+ loop.call_later() 를 사용해 콜백 실행을 예약할 수 있음\n", 349 | "+ loop.stop 메소드를 사용해서 루프 종료\n", 350 | "+ loop.run_forever() 로 루프 시작 가능\n", 351 | "+ loop.run_until_complete()을 통해 개별적인 실행 가능\n", 352 | "+ ensure_future()는 Task 인스턴스를 반환해서 실행하고 싶은 코루틴을 이벤트 루프로 전달 가능" 353 | ] 354 | }, 355 | { 356 | "cell_type": "code", 357 | "execution_count": 11, 358 | "metadata": {}, 359 | "outputs": [], 360 | "source": [ 361 | "import asyncio\n", 362 | "import nest_asyncio\n", 363 | "nest_asyncio.apply() # This module patches asyncio to allow nested (due to jupyter notebook)\n", 364 | "\n", 365 | "async def network_request(number):\n", 366 | " await asyncio.sleep(1.0)\n", 367 | " return {\"success\": True, \"result\": number ** 2}\n", 368 | "\n", 369 | "@clock()\n", 370 | "async def fetch_square(number):\n", 371 | " response = await network_request(number)\n", 372 | " if response[\"success\"]:\n", 373 | " print(response[\"result\"], flush=True)" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": 12, 379 | "metadata": {}, 380 | "outputs": [ 381 | { 382 | "name": "stdout", 383 | "output_type": "stream", 384 | "text": [ 385 | "[0.00000095s] fetch_square(0, ) -> \n", 386 | "0\n", 387 | "[0.00000262s] fetch_square(1, ) -> \n", 388 | "1\n", 389 | "[0.00000310s] fetch_square(2, ) -> \n", 390 | "4\n", 391 | "Total: 3.0104825496673584 sec\n" 392 | ] 393 | } 394 | ], 395 | "source": [ 396 | "if __name__ == \"__main__\":\n", 397 | " loop = asyncio.get_event_loop()\n", 398 | " t0 = time.time()\n", 399 | "\n", 400 | " loop.run_until_complete(fetch_square(0))\n", 401 | " loop.run_until_complete(fetch_square(1))\n", 402 | " loop.run_until_complete(fetch_square(2))\n", 403 | " \n", 404 | " elapsed = time.time() - t0\n", 405 | " print(\"Total:\", elapsed, \"sec\")" 406 | ] 407 | }, 408 | { 409 | "cell_type": "code", 410 | "execution_count": 13, 411 | "metadata": {}, 412 | "outputs": [ 413 | { 414 | "name": "stdout", 415 | "output_type": "stream", 416 | "text": [ 417 | "[0.00000095s] fetch_square(0, ) -> \n", 418 | "[0.00000119s] fetch_square(1, ) -> \n", 419 | "[0.00000048s] fetch_square(2, ) -> \n", 420 | "0\n", 421 | "1\n", 422 | "4\n", 423 | "Total: 1.0085382461547852 sec\n" 424 | ] 425 | } 426 | ], 427 | "source": [ 428 | "if __name__ == \"__main__\":\n", 429 | " loop = asyncio.get_event_loop()\n", 430 | " t0 = time.time()\n", 431 | "\n", 432 | " loop.run_until_complete(asyncio.gather(fetch_square(0), fetch_square(1), fetch_square(2)))\n", 433 | " \n", 434 | " elapsed = time.time() - t0\n", 435 | " print(\"Total:\", elapsed, \"sec\")" 436 | ] 437 | }, 438 | { 439 | "cell_type": "code", 440 | "execution_count": 14, 441 | "metadata": {}, 442 | "outputs": [ 443 | { 444 | "name": "stdout", 445 | "output_type": "stream", 446 | "text": [ 447 | "[0.00000143s] fetch_square(0, ) -> \n", 448 | "[0.00000191s] fetch_square(1, ) -> \n", 449 | "[0.00002360s] fetch_square(2, ) -> \n", 450 | "Total: 0.0013251304626464844 sec\n", 451 | "0\n", 452 | "1\n", 453 | "4\n" 454 | ] 455 | } 456 | ], 457 | "source": [ 458 | "if __name__ == \"__main__\":\n", 459 | " loop = asyncio.get_event_loop()\n", 460 | " t0 = time.time()\n", 461 | "\n", 462 | " asyncio.ensure_future(fetch_square(0))\n", 463 | " asyncio.ensure_future(fetch_square(1))\n", 464 | " asyncio.ensure_future(fetch_square(2))\n", 465 | " \n", 466 | "# loop.run_forever()\n", 467 | "\n", 468 | " elapsed = time.time() - t0\n", 469 | " print(\"Total:\", elapsed, \"sec\")" 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 15, 475 | "metadata": {}, 476 | "outputs": [ 477 | { 478 | "name": "stderr", 479 | "output_type": "stream", 480 | "text": [ 481 | "0it [00:00, ?it/s]\n", 482 | "0it [00:00, ?it/s]\u001b[A\n", 483 | "0it [00:00, ?it/s]" 484 | ] 485 | }, 486 | { 487 | "name": "stdout", 488 | "output_type": "stream", 489 | "text": [ 490 | "Empty DataFrame\n", 491 | "Columns: []\n", 492 | "Index: []\n", 493 | "[0.00909424s] main(, ) -> None\n" 494 | ] 495 | }, 496 | { 497 | "name": "stderr", 498 | "output_type": "stream", 499 | "text": [ 500 | "\n" 501 | ] 502 | } 503 | ], 504 | "source": [ 505 | "import asyncio\n", 506 | "import json\n", 507 | "from pathlib import Path\n", 508 | "\n", 509 | "from collections import defaultdict\n", 510 | "import pandas as pd\n", 511 | "from tqdm import tqdm\n", 512 | "\n", 513 | "import time\n", 514 | "\n", 515 | "\n", 516 | "path = Path(\"/home/shyeon/workspace/python/SparkDefinitiveGuide/data/activity-data/\")\n", 517 | "\n", 518 | "\n", 519 | "def load_json_byline(path):\n", 520 | " with open(path, \"r\", encoding=\"UTF-8\") as f:\n", 521 | " for line in f:\n", 522 | " yield line\n", 523 | "\n", 524 | "\n", 525 | "def load_json_byfile(fps):\n", 526 | " for fp in tqdm(fps):\n", 527 | " yield from load_json_byline(fp)\n", 528 | "\n", 529 | "\n", 530 | "@clock()\n", 531 | "def main():\n", 532 | " dd = defaultdict(list)\n", 533 | " files = path.glob(\"*.json\")\n", 534 | " \n", 535 | " for lines in tqdm(load_json_byfile(files)):\n", 536 | " for key, value in json.loads(lines).items():\n", 537 | " dd[key].append(value)\n", 538 | " \n", 539 | " result = pd.DataFrame(dd)\n", 540 | " print(result.head()) \n", 541 | " \n", 542 | " \n", 543 | "if __name__ == \"__main__\":\n", 544 | " main()" 545 | ] 546 | }, 547 | { 548 | "cell_type": "code", 549 | "execution_count": 17, 550 | "metadata": {}, 551 | "outputs": [ 552 | { 553 | "name": "stderr", 554 | "output_type": "stream", 555 | "text": [ 556 | "0it [00:00, ?it/s]" 557 | ] 558 | }, 559 | { 560 | "name": "stdout", 561 | "output_type": "stream", 562 | "text": [ 563 | "Empty DataFrame\n", 564 | "Columns: []\n", 565 | "Index: []\n", 566 | "[0.00340915s] main(, ) -> None\n" 567 | ] 568 | }, 569 | { 570 | "name": "stderr", 571 | "output_type": "stream", 572 | "text": [ 573 | "\n" 574 | ] 575 | } 576 | ], 577 | "source": [ 578 | "import asyncio\n", 579 | "import json\n", 580 | "from pathlib import Path\n", 581 | "\n", 582 | "from collections import defaultdict\n", 583 | "import pandas as pd\n", 584 | "from tqdm import tqdm\n", 585 | "\n", 586 | "import time\n", 587 | "\n", 588 | "\n", 589 | "path = Path(\"/home/shyeon/workspace/python/SparkDefinitiveGuide/data/activity-data/\")\n", 590 | "\n", 591 | "\n", 592 | "@asyncio.coroutine\n", 593 | "def load_json_byline(path):\n", 594 | " with open(path, \"r\", encoding=\"UTF-8\") as fp:\n", 595 | " yield from fp\n", 596 | "\n", 597 | "\n", 598 | "@asyncio.coroutine \n", 599 | "def load_json_byfile(fps):\n", 600 | " for fp in fps:\n", 601 | " yield from load_json_byline(fp)\n", 602 | "\n", 603 | "\n", 604 | "@clock()\n", 605 | "def main():\n", 606 | " dd = defaultdict(list)\n", 607 | " files = path.glob(\"*.json\")\n", 608 | " \n", 609 | " for lines in tqdm(load_json_byfile(files)):\n", 610 | " for key, value in json.loads(lines).items():\n", 611 | " dd[key].append(value)\n", 612 | " \n", 613 | " result = pd.DataFrame(dd)\n", 614 | " print(result.head()) \n", 615 | " \n", 616 | " \n", 617 | "if __name__ == \"__main__\":\n", 618 | " main()" 619 | ] 620 | }, 621 | { 622 | "cell_type": "code", 623 | "execution_count": 18, 624 | "metadata": {}, 625 | "outputs": [ 626 | { 627 | "name": "stderr", 628 | "output_type": "stream", 629 | "text": [ 630 | "0it [00:00, ?it/s]" 631 | ] 632 | }, 633 | { 634 | "name": "stdout", 635 | "output_type": "stream", 636 | "text": [ 637 | "[0.00000119s] main(, ) -> \n" 638 | ] 639 | }, 640 | { 641 | "name": "stderr", 642 | "output_type": "stream", 643 | "text": [ 644 | "\n" 645 | ] 646 | } 647 | ], 648 | "source": [ 649 | "import time\n", 650 | "\n", 651 | "import asyncio\n", 652 | "import nest_asyncio\n", 653 | "nest_asyncio.apply() # This module patches asyncio to allow nested (due to jupyter notebook)\n", 654 | "import aiofile\n", 655 | "\n", 656 | "import json\n", 657 | "import pandas as pd\n", 658 | "from collections import defaultdict\n", 659 | "from pathlib import Path\n", 660 | "from tqdm import tqdm\n", 661 | "\n", 662 | "\n", 663 | "path = Path(\"/home/shyeon/workspace/python/SparkDefinitiveGuide/data/activity-data/\")\n", 664 | "\n", 665 | "\n", 666 | "@asyncio.coroutine\n", 667 | "def load_json_byline(path):\n", 668 | " with open(path, \"r\", encoding=\"UTF-8\") as fp:\n", 669 | " yield from fp\n", 670 | "\n", 671 | "\n", 672 | "@asyncio.coroutine \n", 673 | "def load_json_byfile(fps):\n", 674 | " for fp in fps:\n", 675 | " yield from load_json_byline(fp)\n", 676 | "\n", 677 | "\n", 678 | "def convert_json2dict(json_str:str, dic:dict):\n", 679 | " for key, value in json.loads(json_str).items():\n", 680 | " dic[key].append(value)\n", 681 | " \n", 682 | " \n", 683 | "@clock()\n", 684 | "@asyncio.coroutine \n", 685 | "def main():\n", 686 | " dd = defaultdict(list)\n", 687 | " files = path.glob(\"*.json\")\n", 688 | " \n", 689 | " task = [convert_json2dict(lines, dd) for lines in tqdm(load_json_byfile(files))]\n", 690 | " \n", 691 | " return task\n", 692 | " \n", 693 | "# result = pd.DataFrame(dd)\n", 694 | "# print(result.head())\n", 695 | " \n", 696 | " \n", 697 | "if __name__ == \"__main__\":\n", 698 | " \n", 699 | " loop = asyncio.get_event_loop()\n", 700 | " loop.run_until_complete(asyncio.gather(*main()))\n", 701 | "# loop.close() " 702 | ] 703 | }, 704 | { 705 | "cell_type": "code", 706 | "execution_count": 19, 707 | "metadata": {}, 708 | "outputs": [ 709 | { 710 | "name": "stdout", 711 | "output_type": "stream", 712 | "text": [ 713 | "[0.00005937s] main(, ) -> []\n" 714 | ] 715 | } 716 | ], 717 | "source": [ 718 | "import time\n", 719 | "\n", 720 | "import asyncio\n", 721 | "import nest_asyncio\n", 722 | "nest_asyncio.apply() # This module patches asyncio to allow nested (due to jupyter notebook)\n", 723 | "from aiofile import AIOFile, Reader, Writer\n", 724 | "\n", 725 | "import json\n", 726 | "import pandas as pd\n", 727 | "from collections import defaultdict\n", 728 | "from pathlib import Path\n", 729 | "from tqdm import tqdm\n", 730 | "\n", 731 | "\n", 732 | "path = Path(\"/home/shyeon/workspace/python/SparkDefinitiveGuide/data/activity-data/\")\n", 733 | "\n", 734 | "\n", 735 | "async def load_json(path):\n", 736 | " async with AIOFile(path, \"r\", encoding=\"UTF-8\") as afp:\n", 737 | " print(await afp.read())\n", 738 | "\n", 739 | "\n", 740 | "\n", 741 | "def convert_json2dict(json_str:str, dic:dict):\n", 742 | " for key, value in json.loads(json_str).items():\n", 743 | " dic[key].append(value)\n", 744 | " \n", 745 | " \n", 746 | "@clock()\n", 747 | "def main():\n", 748 | " dd = defaultdict(list)\n", 749 | " files = path.glob(\"*.json\")\n", 750 | " \n", 751 | " tasks = [load_json(file) for file in files]\n", 752 | " \n", 753 | " return tasks\n", 754 | " \n", 755 | "# result = pd.DataFrame(dd)\n", 756 | "# print(result.head())\n", 757 | " \n", 758 | " \n", 759 | "if __name__ == \"__main__\":\n", 760 | " \n", 761 | " loop = asyncio.get_event_loop()\n", 762 | " loop.run_until_complete(asyncio.gather(*main()))\n", 763 | "# loop.close() " 764 | ] 765 | } 766 | ], 767 | "metadata": { 768 | "kernelspec": { 769 | "display_name": "Python 3", 770 | "language": "python", 771 | "name": "python3" 772 | }, 773 | "language_info": { 774 | "codemirror_mode": { 775 | "name": "ipython", 776 | "version": 3 777 | }, 778 | "file_extension": ".py", 779 | "mimetype": "text/x-python", 780 | "name": "python", 781 | "nbconvert_exporter": "python", 782 | "pygments_lexer": "ipython3", 783 | "version": "3.7.6" 784 | } 785 | }, 786 | "nbformat": 4, 787 | "nbformat_minor": 4 788 | } 789 | -------------------------------------------------------------------------------- /Ch9_Pattern.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# \n", 8 | "## 실전 속의 디자인\n", 9 | "\n" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "### 팩토리 패턴\n", 17 | "+ 파이썬은 클래스, 함수, 사용자 정의 객체 등의 역할이 구분되지 않으므로 필요하지 않음\n", 18 | "\n", 19 | "### 생성 패턴\n", 20 | "+ 객체 초기화를 위한 파라미터를 결정하거나 초기화에 필요한 관련 객체를 준비하는 등의 모든 관련 작업을 단순화하려는 것\n", 21 | "\n", 22 | "#### 싱글턴과 공유 상태\n", 23 | "+ 일반적인 상태에서 싱글턴은 사용하지 않은 것이 좋음\n", 24 | "+ 객체 지향 소프트웨어를 위한 전역 변수의 한 형태이며 나쁜 습관일 확률이 높음\n", 25 | "+ 파이썬에서 이를 해결하는 가장 쉬운 방법은 모듈을 사용하는 것. 여러 번 임포트하더라도 sys.modules에 로딩되는 것은 항상 하나임\n", 26 | "+ 클래스 변수의 게터나 세트를 활용하거나 디스크립터 사용할 수도 있음" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 21, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "name": "stderr", 36 | "output_type": "stream", 37 | "text": [ 38 | "INFO:__main__:Pull at 0.3\n", 39 | "INFO:__main__:Pull at 0.3\n" 40 | ] 41 | } 42 | ], 43 | "source": [ 44 | "import logging\n", 45 | "\n", 46 | "logging.basicConfig()\n", 47 | "logger = logging.getLogger(__name__)\n", 48 | "\n", 49 | "\n", 50 | "\"\"\" 게터/세터 구현 \"\"\"\n", 51 | "class GitFetcher:\n", 52 | " _current_tag = None\n", 53 | "\n", 54 | " def __init__(self, tag):\n", 55 | " self.current_tag = tag # 프로퍼티를 통해 클래스 변수와 연결되어 있음\n", 56 | "\n", 57 | " @property\n", 58 | " def current_tag(self):\n", 59 | " if self._current_tag is None:\n", 60 | " raise AttributeError(\"Tag is not initialized yet.\")\n", 61 | " return self._current_tag \n", 62 | " \n", 63 | " @current_tag.setter\n", 64 | " def current_tag(self, new_tag):\n", 65 | " self.__class__._current_tag = new_tag \n", 66 | " \n", 67 | " def pull(self):\n", 68 | " logger.info(f\"Pull at {self.current_tag}\")\n", 69 | "\n", 70 | " \n", 71 | "if __name__ == \"__main__\":\n", 72 | " f1 = GitFetcher(0.1)\n", 73 | " f2 = GitFetcher(0.2)\n", 74 | " f1.current_tag = 0.3\n", 75 | " \n", 76 | " f2.pull()\n", 77 | " f1.pull()" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 24, 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "name": "stderr", 87 | "output_type": "stream", 88 | "text": [ 89 | "INFO:__main__:calling __set__\n", 90 | "INFO:__main__:calling __set__\n", 91 | "INFO:__main__:calling __set__\n", 92 | "INFO:__main__:Pull at 0.3\n", 93 | "INFO:__main__:Pull at 0.3\n" 94 | ] 95 | } 96 | ], 97 | "source": [ 98 | "import logging\n", 99 | "\n", 100 | "logging.basicConfig()\n", 101 | "logger = logging.getLogger(__name__)\n", 102 | "\n", 103 | "\n", 104 | "\"\"\" 디스크립터 구현 \"\"\"\n", 105 | "class SharedArrtibute:\n", 106 | " def __init__(self, initial_value=None):\n", 107 | " self.value = initial_value\n", 108 | " self._name = None\n", 109 | " \n", 110 | " def __get__(self, instance, owner):\n", 111 | " if instance is None:\n", 112 | " return self\n", 113 | " if self.value is None:\n", 114 | " raise AttributeError(f\"{self._name} was never set\")\n", 115 | " return self.value\n", 116 | " \n", 117 | " def __set__(self, instance, new_value):\n", 118 | " logger.info(\"calling __set__\")\n", 119 | " self.value = new_value\n", 120 | " \n", 121 | " def __set_name__(self, owner, name):\n", 122 | " self._name = name\n", 123 | " \n", 124 | " \n", 125 | "class GitFetcher:\n", 126 | " current_tag = SharedArrtibute()\n", 127 | " current_branch = SharedArrtibute()\n", 128 | "\n", 129 | " def __init__(self, tag, branch=None):\n", 130 | " self.current_tag = tag \n", 131 | " self.__class__.current_branch = branch # __class__를 사용하면 디스크립터가 동작하지 않음\n", 132 | "\n", 133 | " def pull(self):\n", 134 | " logger.info(f\"Pull at {self.current_tag}\")\n", 135 | " return self.current_tag\n", 136 | " \n", 137 | "if __name__ == \"__main__\":\n", 138 | " f1 = GitFetcher(0.1)\n", 139 | " f2 = GitFetcher(0.2)\n", 140 | " f1.current_tag = 0.3\n", 141 | " \n", 142 | " f2.pull()\n", 143 | " f1.pull()" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "### borg 패턴\n", 151 | "\n", 152 | "+ 어쩔 수 없이 꼭 싱글턴을 사용해야 하는 상태에서의 대안\n", 153 | "+ 같은 클래스의 모든 인스턴스가 모든 속성을 복제하는 객체를 만드는 것" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 26, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "class BaseFetcher:\n", 163 | " def __init__(self, source):\n", 164 | " self.source = source\n", 165 | "\n", 166 | " \n", 167 | "class TagFetcher(BaseFetcher):\n", 168 | " _attributes = {} # borg 패턴의 특징,\n", 169 | " \n", 170 | " def __init__(self, source):\n", 171 | " self.__dict__ = self.__class__._attributes\n", 172 | " super().__init__(source)\n", 173 | " \n", 174 | " def pull(self):\n", 175 | " logger.info(f\"Pull at tag {self.source}\")\n", 176 | " return f\"Tag = {self.source}\"\n", 177 | " \n", 178 | "\n", 179 | "class BranchFetcher(BaseFetcher):\n", 180 | " _attributes = {} # 사전은 레퍼런스 형태로 전달되는 변경 가능한 mutable 객체이므로\n", 181 | " # 사전을 업데이트하면 모든 객체에 동일하게 업데이트 됨\n", 182 | " \n", 183 | " def __init__(self, source):\n", 184 | " self.__dict__ = self.__class__._attributes\n", 185 | " super().__init__(source)\n", 186 | " \n", 187 | " def pull(self):\n", 188 | " logger.info(f\"Pull at branch {self.source}\")\n", 189 | " return f\"Branch = {self.source}\" " 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "+ 실수로 사전과 관련된 로직을 추가하는 것을 방지하기 위해 믹스인 클래스를 사용해서 사전을 만드는 방법" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 69, 202 | "metadata": {}, 203 | "outputs": [ 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "{'source': 0.2}\n", 209 | "[, , , ]\n" 210 | ] 211 | }, 212 | { 213 | "ename": "AttributeError", 214 | "evalue": "'TagFetcher' object has no attribute '__subclasses__'", 215 | "output_type": "error", 216 | "traceback": [ 217 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 218 | "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", 219 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 34\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mf2\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__dict__\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mTagFetcher\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmro\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[0;32m---> 36\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mf2\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__subclasses__\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 37\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 38\u001b[0m \u001b[0mf2\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpull\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", 220 | "\u001b[0;31mAttributeError\u001b[0m: 'TagFetcher' object has no attribute '__subclasses__'" 221 | ] 222 | } 223 | ], 224 | "source": [ 225 | "## 공통 부분 시작 ##\n", 226 | "class SharedAllMixin:\n", 227 | " def __init__(self, *args, **kwargs):\n", 228 | " try:\n", 229 | " self.__class__._attributes\n", 230 | " except AttributeError:\n", 231 | " self.__class__._attributes = {}\n", 232 | "\n", 233 | " self.__dict__ = self.__class__._attributes\n", 234 | " super().__init__(*args, **kwargs)\n", 235 | "\n", 236 | "\n", 237 | "class BaseFetcher:\n", 238 | " def __init__(self, source):\n", 239 | " self.source = source\n", 240 | "## 공통 부분 끝 ##\n", 241 | "\n", 242 | "class TagFetcher(SharedAllMixin, BaseFetcher):\n", 243 | " def pull(self):\n", 244 | " logger.info(\"pulling from tag %s\", self.source)\n", 245 | " return f\"Tag = {self.source}\"\n", 246 | "\n", 247 | "\n", 248 | "class BranchFetcher(SharedAllMixin, BaseFetcher):\n", 249 | " def pull(self):\n", 250 | " logger.info(\"pulling from branch %s\", self.source)\n", 251 | " return f\"Branch = {self.source}\"\n", 252 | "\n", 253 | " \n", 254 | "if __name__ == \"__main__\":\n", 255 | " f1 = TagFetcher(0.1)\n", 256 | " f2 = TagFetcher(0.2)\n", 257 | " \n", 258 | " print(f2.__dict__)\n", 259 | " print(TagFetcher.mro())\n", 260 | " print(SharedAllMixin.__subclasses__())\n", 261 | " \n", 262 | " f2.pull()\n", 263 | " f1.pull()" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "### 빌더\n", 271 | "\n", 272 | "+ 필요로 하는 모든 객체를 직접 생성해주는 하나의 복잡한 객체를 만들어야 함\n", 273 | "+ 한 번에 모든 것을 처리해주는 추상화를 해야 함\n", 274 | "\n", 275 | "https://hoony-gunputer.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-Builder-Pattern-by-python-java\n", 276 | "https://medium.com/modern-nlp/10-great-ml-practices-for-python-developers-b089eefc18fc" 277 | ] 278 | }, 279 | { 280 | "cell_type": "markdown", 281 | "metadata": {}, 282 | "source": [ 283 | "## 구조패턴\n", 284 | "\n", 285 | "+ 여러 개의 객체를 조합하여 반복을 제거" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "metadata": {}, 291 | "source": [ 292 | "### 어댑터 패턴\n", 293 | "+ 두 가지 방법으로 구현 가능 \n", 294 | "+ 첫 번째는 사용하려는 클래스를 상속 받는 클래스를 만드는 것\n", 295 | " + 상속은 얼마나 많은 외부 라이브러리를 가져올지 정확히 할기 어려우므로 is a 관계에 한정해서 적용하는 것이 바람직 함" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": 41, 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "class UsernameLookup:\n", 305 | " def search(self, user_namespace):\n", 306 | " logger.info(\"looking for %s\", user_namespace)" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": 42, 312 | "metadata": {}, 313 | "outputs": [], 314 | "source": [ 315 | "class UserSource(UsernameLookup):\n", 316 | " def fetch(self, user_id, username):\n", 317 | " user_namespace = self._adapt_arguments(user_id, username)\n", 318 | " return self.search(user_namespace)\n", 319 | "\n", 320 | " @staticmethod\n", 321 | " def _adapt_arguments(user_id, username):\n", 322 | " return f\"{user_id}:{username}\"" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "+ 두 번째 방법은 컴포지션을 사용 \n", 330 | " + 더 나은 방법으로 소개함" 331 | ] 332 | }, 333 | { 334 | "cell_type": "code", 335 | "execution_count": 45, 336 | "metadata": {}, 337 | "outputs": [], 338 | "source": [ 339 | "class UserSource:\n", 340 | " def __init__(self, username_lookup: UsernameLookup) -> None:\n", 341 | " self.username_lookup = username_lookup\n", 342 | "\n", 343 | " def fetch(self, user_id, username):\n", 344 | " user_namespace = self._adapt_arguments(user_id, username)\n", 345 | " return self.username_lookup.search(user_namespace)\n", 346 | "\n", 347 | " @staticmethod\n", 348 | " def _adapt_arguments(user_id, username):\n", 349 | " return f\"{user_id}:{username}\"" 350 | ] 351 | }, 352 | { 353 | "cell_type": "markdown", 354 | "metadata": {}, 355 | "source": [ 356 | "### 컴포지트\n", 357 | "\n", 358 | "+ 객체는 구조화된 트리 형태로 볼 수 있으며, 기본 객체는 리프 노드이고 컨테이너 객체는 중간 노드라 볼 수 있음\n", 359 | "+ 클라이언트는 이 중 아무거나 호출하여 결과를 얻으려고 할 것임\n", 360 | "+ 또한 컴포지트 객체도 클라이언트 처럼 동작함\n", 361 | "+ 즉 리프 노드인지 중간 노드인지 상관없이 해당 요청을 관련노드가 처리할 수 있을 때 까지 계속 전달" 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": 54, 367 | "metadata": {}, 368 | "outputs": [ 369 | { 370 | "name": "stderr", 371 | "output_type": "stream", 372 | "text": [ 373 | "INFO:__main__:2807.5\n" 374 | ] 375 | } 376 | ], 377 | "source": [ 378 | "from typing import Iterable, Union\n", 379 | "\n", 380 | "\n", 381 | "class Product:\n", 382 | " def __init__(self, name, price):\n", 383 | " self._name = name\n", 384 | " self._price = price\n", 385 | "\n", 386 | " @property\n", 387 | " def price(self):\n", 388 | " return self._price\n", 389 | "\n", 390 | "\n", 391 | "class ProductBundle:\n", 392 | " def __init__(\n", 393 | " self,\n", 394 | " name,\n", 395 | " perc_discount,\n", 396 | " *products: Iterable[Union[Product, \"ProductBundle\"]]\n", 397 | " ) -> None:\n", 398 | " self._name = name\n", 399 | " self._perc_discount = perc_discount\n", 400 | " self._products = products\n", 401 | "\n", 402 | " @property\n", 403 | " def price(self):\n", 404 | " total = sum(p.price for p in self._products)\n", 405 | " return total * (1 - self._perc_discount)\n", 406 | " \n", 407 | "if __name__ == \"__main__\":\n", 408 | "\n", 409 | " electronics = ProductBundle(\n", 410 | " \"electronics\",\n", 411 | " 0,\n", 412 | " ProductBundle(\n", 413 | " \"smartphones\",\n", 414 | " 0.15,\n", 415 | " Product(\"smartphone1\", 200),\n", 416 | " Product(\"smartphone2\", 700),\n", 417 | " ),\n", 418 | " ProductBundle(\n", 419 | " \"laptops\",\n", 420 | " 0.05,\n", 421 | " Product(\"laptop1\", 700),\n", 422 | " Product(\"laptop2\", 950),\n", 423 | " ),\n", 424 | " )\n", 425 | " tablets = ProductBundle(\n", 426 | " \"tablets\", 0.05, Product(\"tablet1\", 200), Product(\"tablet2\", 300)\n", 427 | " )\n", 428 | " total = ProductBundle(\"total\", 0, electronics, tablets)\n", 429 | " logger.info(total.price)" 430 | ] 431 | }, 432 | { 433 | "cell_type": "markdown", 434 | "metadata": {}, 435 | "source": [ 436 | "### 데코레이터 패턴\n", 437 | "\n", 438 | "+ 파이선 데코레이터와는 다른 개념\n", 439 | "+ 상속을 하지 않고도 객체의 기능을 동적으로 확장 할 수 있음\n", 440 | "+ 동일한 인터페이스를 가지고 여러 단계를 거쳐 결과를 장식하거나 결합도 할 수 있는 또 다른 객체를 만드는 것\n", 441 | "+ 이 객체들은 연결되어 있으며 각각의 객체는 본래 의도에 더해 새로운 기능이 추가될 수 있음" 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": 55, 447 | "metadata": {}, 448 | "outputs": [], 449 | "source": [ 450 | "class DictQuery:\n", 451 | " def __init__(self, **kwargs):\n", 452 | " self._raw_query = kwargs\n", 453 | "\n", 454 | " def render(self) -> dict:\n", 455 | " return self._raw_query" 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": 56, 461 | "metadata": {}, 462 | "outputs": [], 463 | "source": [ 464 | "class QueryEnhancer:\n", 465 | " def __init__(self, query: DictQuery):\n", 466 | " self.decorated = query\n", 467 | "\n", 468 | " def render(self):\n", 469 | " return self.decorated.render()\n", 470 | "\n", 471 | "\n", 472 | "class RemoveEmpty(QueryEnhancer):\n", 473 | " def render(self):\n", 474 | " original = super().render() # 오리지널을 받아서\n", 475 | " return {k: v for k, v in original.items() if v} # 새로운 형식으로 리턴함\n", 476 | "\n", 477 | "\n", 478 | "class CaseInsensitive(QueryEnhancer):\n", 479 | " def render(self):\n", 480 | " original = super().render()\n", 481 | " return {k: v.lower() for k, v in original.items()}" 482 | ] 483 | }, 484 | { 485 | "cell_type": "code", 486 | "execution_count": 57, 487 | "metadata": {}, 488 | "outputs": [], 489 | "source": [ 490 | "original = DictQuery(foo=\"bar\", empty=\"\", none=None, upper=\"UPPERCASE\", title=\"Title\")\n", 491 | "new_query = CaseInsensitive(RemoveEmpty(original))" 492 | ] 493 | }, 494 | { 495 | "cell_type": "code", 496 | "execution_count": 58, 497 | "metadata": {}, 498 | "outputs": [ 499 | { 500 | "data": { 501 | "text/plain": [ 502 | "{'foo': 'bar', 'upper': 'uppercase', 'title': 'title'}" 503 | ] 504 | }, 505 | "execution_count": 58, 506 | "metadata": {}, 507 | "output_type": "execute_result" 508 | } 509 | ], 510 | "source": [ 511 | "new_query.render()" 512 | ] 513 | }, 514 | { 515 | "cell_type": "markdown", 516 | "metadata": {}, 517 | "source": [ 518 | "+ 파이썬의 동적인 특성을 활용해 다른 방법으로 구현한 사례" 519 | ] 520 | }, 521 | { 522 | "cell_type": "code", 523 | "execution_count": 65, 524 | "metadata": {}, 525 | "outputs": [], 526 | "source": [ 527 | "from typing import Callable, Dict, Iterable\n", 528 | "\n", 529 | "class QueryEnhancer:\n", 530 | " def __init__(\n", 531 | " self,\n", 532 | " query: DictQuery,\n", 533 | " *decorators: Iterable[Callable[[Dict[str, str]], Dict[str, str]]] # Callable[[input], return type]\n", 534 | " ) -> None:\n", 535 | " self._decorated = query\n", 536 | " self._decorators = decorators\n", 537 | "\n", 538 | " def render(self):\n", 539 | " current_result = self._decorated.render()\n", 540 | " for deco in self._decorators:\n", 541 | " current_result = deco(current_result)\n", 542 | " return current_result\n", 543 | "\n", 544 | "\n", 545 | "def remove_empty(original: dict) -> dict:\n", 546 | " return {k: v for k, v in original.items() if v}\n", 547 | "\n", 548 | "\n", 549 | "def case_insensitive(original: dict) -> dict:\n", 550 | " return {k: v.lower() for k, v in original.items()}" 551 | ] 552 | }, 553 | { 554 | "cell_type": "code", 555 | "execution_count": 62, 556 | "metadata": {}, 557 | "outputs": [ 558 | { 559 | "data": { 560 | "text/plain": [ 561 | "{'foo': 'bar', 'upper': 'uppercase', 'title': 'title'}" 562 | ] 563 | }, 564 | "execution_count": 62, 565 | "metadata": {}, 566 | "output_type": "execute_result" 567 | } 568 | ], 569 | "source": [ 570 | "QueryEnhancer(original, remove_empty, case_insensitive).render()" 571 | ] 572 | }, 573 | { 574 | "cell_type": "markdown", 575 | "metadata": {}, 576 | "source": [ 577 | "### 파사드 패턴\n", 578 | "+ 여러 객체가 다대다 관계를 이루며 상화작용을 하는 경우 사용됨\n", 579 | "+ 각각의 객체에 대한 모든 연결을 만드는 대신 파사드 역할을 하는 중간 객체를 만들어 해결\n", 580 | "+ 디렉토리의 패키지 빌드할 때 \\_\\_init\\_\\_.py 파일을 나머지 파일들과 함께 두는 것은 모듈의 루트오서 파사드와 같은 역할을 함\n", 581 | " + \\_\\_init\\_\\_.py 파일의 API가 유지되는 한 클라이언트에 영향을 주지 않게 됨" 582 | ] 583 | }, 584 | { 585 | "cell_type": "markdown", 586 | "metadata": {}, 587 | "source": [ 588 | "## 행동 패턴\n", 589 | "+ 어떤 패턴을 사용하든지 간에 결국에는 중복을 피하거나 행동을 캡슐화하는 추상화를 통해 모델 간 결합력을 낮추는 방법" 590 | ] 591 | }, 592 | { 593 | "cell_type": "markdown", 594 | "metadata": {}, 595 | "source": [ 596 | "### 책임 연쇄 패턴\n", 597 | "\n", 598 | "+ 후계자라는 개념이 추가됨, 현재 초리할 수 없을 때 대비한 다음 이벤트 객체를 의미\n", 599 | "+ 직접 처리가 가능한 경우 결과를 반환하지만 처리가 불가능하면 후계자에게 전달하고 이 과정을 반복함" 600 | ] 601 | }, 602 | { 603 | "cell_type": "code", 604 | "execution_count": 76, 605 | "metadata": {}, 606 | "outputs": [], 607 | "source": [ 608 | "import re\n", 609 | "\n", 610 | "class Event:\n", 611 | " pattern = None\n", 612 | "\n", 613 | " def __init__(self, next_event=None):\n", 614 | " self.successor = next_event\n", 615 | "\n", 616 | " def process(self, logline: str):\n", 617 | " if self.can_process(logline):\n", 618 | " return self._process(logline)\n", 619 | "\n", 620 | " if self.successor is not None:\n", 621 | " return self.successor.process(logline)\n", 622 | "\n", 623 | " def _process(self, logline: str) -> dict:\n", 624 | " parsed_data = self._parse_data(logline)\n", 625 | " return {\n", 626 | " \"type\": self.__class__.__name__,\n", 627 | " \"id\": parsed_data[\"id\"],\n", 628 | " \"value\": parsed_data[\"value\"],\n", 629 | " }\n", 630 | "\n", 631 | " @classmethod\n", 632 | " def can_process(cls, logline: str) -> bool:\n", 633 | " return cls.pattern.match(logline) is not None\n", 634 | "\n", 635 | " @classmethod\n", 636 | " def _parse_data(cls, logline: str) -> dict:\n", 637 | " return cls.pattern.match(logline).groupdict()\n", 638 | "\n", 639 | "\n", 640 | "class LoginEvent(Event):\n", 641 | " pattern = re.compile(r\"(?P\\d+):\\s+login\\s+(?P\\S+)\")\n", 642 | "\n", 643 | "\n", 644 | "class LogoutEvent(Event):\n", 645 | " pattern = re.compile(r\"(?P\\d+):\\s+logout\\s+(?P\\S+)\")\n", 646 | "\n", 647 | "\n", 648 | "class SessionEvent(Event):\n", 649 | " pattern = re.compile(r\"(?P\\d+):\\s+log(in|out)\\s+(?P\\S+)\")" 650 | ] 651 | }, 652 | { 653 | "cell_type": "code", 654 | "execution_count": 77, 655 | "metadata": {}, 656 | "outputs": [], 657 | "source": [ 658 | "chain = LogoutEvent(LoginEvent())" 659 | ] 660 | }, 661 | { 662 | "cell_type": "code", 663 | "execution_count": 78, 664 | "metadata": {}, 665 | "outputs": [ 666 | { 667 | "data": { 668 | "text/plain": [ 669 | "{'type': 'LoginEvent', 'id': '567', 'value': 'User'}" 670 | ] 671 | }, 672 | "execution_count": 78, 673 | "metadata": {}, 674 | "output_type": "execute_result" 675 | } 676 | ], 677 | "source": [ 678 | "chain.process(\"567: login User\") # 로그 아웃이 먼저 실행됨" 679 | ] 680 | }, 681 | { 682 | "cell_type": "markdown", 683 | "metadata": {}, 684 | "source": [ 685 | "### 커맨트 패턴\n", 686 | "<순서>\n", 687 | "+ 실행될 명령의 파라미터들을 저장하는 객체를 만드는 것\n", 688 | "+ 명령에 필요한 파라미터에 필터를 더하거나 제거하는 것처럼 상호작용할 수 잇는 메서드 제공\n", 689 | "+ 마지막으로 실제로 작업을 수행할 객체를 만듬" 690 | ] 691 | }, 692 | { 693 | "cell_type": "markdown", 694 | "metadata": {}, 695 | "source": [ 696 | "### 상태 패턴\n", 697 | "+ 상태별로 작은 객체를 만들어 각각의 객체가 적은 책임을 갖게 하도록 구현" 698 | ] 699 | }, 700 | { 701 | "cell_type": "code", 702 | "execution_count": 96, 703 | "metadata": {}, 704 | "outputs": [], 705 | "source": [ 706 | "import abc\n", 707 | "\n", 708 | "class InvalidTransitionError(Exception):\n", 709 | " \"\"\"Raised when trying to move to a target state from an unreachable source\n", 710 | " state.\n", 711 | " \"\"\"\n", 712 | "\n", 713 | "\n", 714 | "class MergeRequestState(abc.ABC):\n", 715 | " def __init__(self, merge_request):\n", 716 | " self._merge_request = merge_request\n", 717 | "\n", 718 | " @abc.abstractmethod\n", 719 | " def open(self):\n", 720 | " ...\n", 721 | "\n", 722 | " @abc.abstractmethod\n", 723 | " def close(self):\n", 724 | " ...\n", 725 | "\n", 726 | " @abc.abstractmethod\n", 727 | " def merge(self):\n", 728 | " ...\n", 729 | "\n", 730 | " def __str__(self):\n", 731 | " return self.__class__.__name__\n", 732 | "\n", 733 | "\n", 734 | "class Open(MergeRequestState):\n", 735 | " def open(self):\n", 736 | " self._merge_request.approvals = 0\n", 737 | "\n", 738 | " def close(self):\n", 739 | " self._merge_request.approvals = 0\n", 740 | " self._merge_request.state = Closed # 상태를 전환하는 코드\n", 741 | "\n", 742 | " def merge(self):\n", 743 | " logger.info(\"merging %s\", self._merge_request)\n", 744 | " logger.info(\"deleting branch %s\", self._merge_request.source_branch)\n", 745 | " self._merge_request.state = Merged # 상태를 전환하는 코드\n", 746 | "\n", 747 | "\n", 748 | "class Closed(MergeRequestState):\n", 749 | " def open(self):\n", 750 | " logger.info(\"reopening closed merge request %s\", self._merge_request)\n", 751 | " self._merge_request.state = Open # 상태를 전환하는 코드\n", 752 | "\n", 753 | " def close(self):\n", 754 | " \"\"\"Current state.\"\"\"\n", 755 | "\n", 756 | " def merge(self):\n", 757 | " raise InvalidTransitionError(\"can't merge a closed request\")\n", 758 | "\n", 759 | "\n", 760 | "class Merged(MergeRequestState):\n", 761 | " def open(self):\n", 762 | " raise InvalidTransitionError(\"already merged request\")\n", 763 | "\n", 764 | " def close(self):\n", 765 | " raise InvalidTransitionError(\"already merged request\")\n", 766 | "\n", 767 | " def merge(self):\n", 768 | " \"\"\"Current state.\"\"\"\n", 769 | "\n", 770 | "\n", 771 | "class MergeRequest:\n", 772 | " def __init__(self, source_branch: str, target_branch: str) -> None:\n", 773 | " self.source_branch = source_branch\n", 774 | " self.target_branch = target_branch\n", 775 | " self._state = None\n", 776 | " self.approvals = 0\n", 777 | " self.state = Open # 상태를 전환하는 코드\n", 778 | "\n", 779 | " @property\n", 780 | " def state(self):\n", 781 | " return self._state\n", 782 | "\n", 783 | " @state.setter\n", 784 | " def state(self, new_state_cls):\n", 785 | " print(\"calling setter\", new_state_cls)\n", 786 | " self._state = new_state_cls(self)\n", 787 | "\n", 788 | " def open(self):\n", 789 | " return self.state.open()\n", 790 | "\n", 791 | " def close(self):\n", 792 | " return self.state.close()\n", 793 | "\n", 794 | " def merge(self):\n", 795 | " return self.state.merge()\n", 796 | "\n", 797 | " def __str__(self):\n", 798 | " return f\"{self.target_branch}:{self.source_branch}\"\n" 799 | ] 800 | }, 801 | { 802 | "cell_type": "code", 803 | "execution_count": 97, 804 | "metadata": {}, 805 | "outputs": [ 806 | { 807 | "name": "stdout", 808 | "output_type": "stream", 809 | "text": [ 810 | "calling setter \n" 811 | ] 812 | }, 813 | { 814 | "data": { 815 | "text/plain": [ 816 | "0" 817 | ] 818 | }, 819 | "execution_count": 97, 820 | "metadata": {}, 821 | "output_type": "execute_result" 822 | } 823 | ], 824 | "source": [ 825 | "mr = MergeRequest(\"develop\", \"master\")\n", 826 | "mr.open()\n", 827 | "mr.approvals" 828 | ] 829 | }, 830 | { 831 | "cell_type": "code", 832 | "execution_count": 98, 833 | "metadata": {}, 834 | "outputs": [ 835 | { 836 | "name": "stdout", 837 | "output_type": "stream", 838 | "text": [ 839 | "calling setter \n" 840 | ] 841 | }, 842 | { 843 | "data": { 844 | "text/plain": [ 845 | "0" 846 | ] 847 | }, 848 | "execution_count": 98, 849 | "metadata": {}, 850 | "output_type": "execute_result" 851 | } 852 | ], 853 | "source": [ 854 | "mr.approvals = 3\n", 855 | "mr.close()\n", 856 | "mr.approvals" 857 | ] 858 | }, 859 | { 860 | "cell_type": "code", 861 | "execution_count": 99, 862 | "metadata": {}, 863 | "outputs": [ 864 | { 865 | "name": "stderr", 866 | "output_type": "stream", 867 | "text": [ 868 | "INFO:__main__:reopening closed merge request master:develop\n" 869 | ] 870 | }, 871 | { 872 | "name": "stdout", 873 | "output_type": "stream", 874 | "text": [ 875 | "calling setter \n" 876 | ] 877 | } 878 | ], 879 | "source": [ 880 | "mr.open()" 881 | ] 882 | }, 883 | { 884 | "cell_type": "code", 885 | "execution_count": 100, 886 | "metadata": {}, 887 | "outputs": [ 888 | { 889 | "name": "stderr", 890 | "output_type": "stream", 891 | "text": [ 892 | "INFO:__main__:merging master:develop\n", 893 | "INFO:__main__:deleting branch develop\n" 894 | ] 895 | }, 896 | { 897 | "name": "stdout", 898 | "output_type": "stream", 899 | "text": [ 900 | "calling setter \n" 901 | ] 902 | } 903 | ], 904 | "source": [ 905 | "mr.merge()" 906 | ] 907 | }, 908 | { 909 | "cell_type": "code", 910 | "execution_count": 101, 911 | "metadata": {}, 912 | "outputs": [ 913 | { 914 | "ename": "InvalidTransitionError", 915 | "evalue": "already merged request", 916 | "output_type": "error", 917 | "traceback": [ 918 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 919 | "\u001b[0;31mInvalidTransitionError\u001b[0m Traceback (most recent call last)", 920 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\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", 921 | "\u001b[0;32m\u001b[0m in \u001b[0;36mclose\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 85\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 86\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\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---> 87\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\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 88\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 89\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmerge\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\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", 922 | "\u001b[0;32m\u001b[0m in \u001b[0;36mclose\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\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---> 60\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mInvalidTransitionError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"already merged request\"\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 61\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 62\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmerge\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\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", 923 | "\u001b[0;31mInvalidTransitionError\u001b[0m: already merged request" 924 | ] 925 | } 926 | ], 927 | "source": [ 928 | "mr.close()" 929 | ] 930 | }, 931 | { 932 | "cell_type": "markdown", 933 | "metadata": {}, 934 | "source": [ 935 | "### Null 객체 패턴\n", 936 | "\n", 937 | "+ 함수나 메서드는 일관된 타입을 잔환해야 한다는 것\n", 938 | "+ 검색하는 사용자가 없다고 가정했을 때\n", 939 | " + 예외를 발생하거나\n", 940 | " + UserUnknow 타입을 반환\n", 941 | " + 어떤 경우에도 None을 반환하면 안됨\n", 942 | " + None이라는 문구는 방금 일어난 일에 대한 아무것도 설명해주지 않으며 호출자는 특별한 공지가 없으면 아무 생각없이 반환 객체에 대해 메서드를 호출할 것이므로 결국 AttributeError가 발생하게 된다." 943 | ] 944 | } 945 | ], 946 | "metadata": { 947 | "kernelspec": { 948 | "display_name": "Python 3.7.6 64-bit ('cleancode': venv)", 949 | "language": "python", 950 | "name": "python37664bitcleancodevenv75c55895aceb487e9e5693697f2a0d36" 951 | }, 952 | "language_info": { 953 | "codemirror_mode": { 954 | "name": "ipython", 955 | "version": 3 956 | }, 957 | "file_extension": ".py", 958 | "mimetype": "text/x-python", 959 | "name": "python", 960 | "nbconvert_exporter": "python", 961 | "pygments_lexer": "ipython3", 962 | "version": "3.7.6" 963 | } 964 | }, 965 | "nbformat": 4, 966 | "nbformat_minor": 4 967 | } 968 | -------------------------------------------------------------------------------- /Ch6_Descriptor.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Chapter 6. 디스크립터" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## 개요\n", 15 | "### 디스크립터 메커니즘\n", 16 | "+ 최소 두 개의 클래스가 필요\n", 17 | "+ 디스크립터 프로토콜 중 하나를 구현한 클래스 인트턴스를 클래스 속성으로 포함해야 함\n", 18 | " + \\_\\_get\\_\\_\n", 19 | " + \\_\\_set\\_\\_\n", 20 | " + \\_\\_delete\\_\\_\n", 21 | " + \\_\\_set_name\\_\\_" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 1, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "\"\"\" 디스크립터는 __get__ 매직 메서드의 결과를 반환함 \"\"\"\n", 31 | "\n", 32 | "import logging\n", 33 | "\n", 34 | "logging.basicConfig(level=logging.INFO)\n", 35 | "logger = logging.getLogger(__name__)\n", 36 | "\n", 37 | "class DescriptorClass:\n", 38 | " def __get__(self, instance, owner): # owner의 인스턴스와 클래스를 받음\n", 39 | " if instance is None:\n", 40 | " return self\n", 41 | " logger.info(\n", 42 | " \"Call: %s.__get__(%r, %r)\", \n", 43 | " self.__class__.__name__,\n", 44 | " instance,\n", 45 | " owner,\n", 46 | " )\n", 47 | " return instance\n", 48 | "\n", 49 | "\n", 50 | "class ClientClass:\n", 51 | " descriptor = DescriptorClass()" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 2, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "name": "stderr", 61 | "output_type": "stream", 62 | "text": [ 63 | "INFO:__main__:Call: DescriptorClass.__get__(<__main__.ClientClass object at 0x7fdd740bb2d0>, )\n" 64 | ] 65 | }, 66 | { 67 | "data": { 68 | "text/plain": [ 69 | "<__main__.ClientClass at 0x7fdd740bb2d0>" 70 | ] 71 | }, 72 | "execution_count": 2, 73 | "metadata": {}, 74 | "output_type": "execute_result" 75 | } 76 | ], 77 | "source": [ 78 | "client = ClientClass()\n", 79 | "client.descriptor" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 3, 85 | "metadata": {}, 86 | "outputs": [ 87 | { 88 | "name": "stderr", 89 | "output_type": "stream", 90 | "text": [ 91 | "INFO:__main__:Call: DescriptorClass.__get__(<__main__.ClientClass object at 0x7fdd740bb2d0>, )\n" 92 | ] 93 | }, 94 | { 95 | "data": { 96 | "text/plain": [ 97 | "True" 98 | ] 99 | }, 100 | "execution_count": 3, 101 | "metadata": {}, 102 | "output_type": "execute_result" 103 | } 104 | ], 105 | "source": [ 106 | "client.descriptor is client" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "### 디스크립터 프로토콜의 매서드 \n", 114 | "#### \\_\\_get\\_\\_(self, instance, owner)\n", 115 | "+ owner가 있는 경우는 클래스에서 직접 호출하는 경우 instance가 None이기 때문" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 4, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "\"\"\" 디스크립터를 인스턴스로 호출할 때와 클래스 속성으로 호출할 때의 차이점 \"\"\"\n", 125 | "\n", 126 | "class DescriptorClass:\n", 127 | " def __get__(self, instance, owner):\n", 128 | " if instance is None:\n", 129 | " return self, owner\n", 130 | " logger.info(\"Call: %s.__get__(%r, %r)\",\n", 131 | " self.__class__.__name__, instance, owner)\n", 132 | " return instance\n", 133 | " \n", 134 | "class ClientClass:\n", 135 | " descriptor = DescriptorClass()" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 5, 141 | "metadata": {}, 142 | "outputs": [ 143 | { 144 | "name": "stderr", 145 | "output_type": "stream", 146 | "text": [ 147 | "INFO:__main__:Call: DescriptorClass.__get__(<__main__.ClientClass object at 0x7fdd7404f1d0>, )\n" 148 | ] 149 | }, 150 | { 151 | "data": { 152 | "text/plain": [ 153 | "<__main__.ClientClass at 0x7fdd7404f1d0>" 154 | ] 155 | }, 156 | "execution_count": 5, 157 | "metadata": {}, 158 | "output_type": "execute_result" 159 | } 160 | ], 161 | "source": [ 162 | "ClientClass().descriptor" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": 6, 168 | "metadata": {}, 169 | "outputs": [ 170 | { 171 | "data": { 172 | "text/plain": [ 173 | "(<__main__.DescriptorClass at 0x7fdd7404f490>, __main__.ClientClass)" 174 | ] 175 | }, 176 | "execution_count": 6, 177 | "metadata": {}, 178 | "output_type": "execute_result" 179 | } 180 | ], 181 | "source": [ 182 | "ClientClass.descriptor # instance가 None이므로 self를 반환" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "#### \\_\\_set\\_\\_(self, instance, value)\n", 190 | "+ 디스크립터에 값을 할당할 때 호출되며, \\_\\_set\\_\\_( ) 메서드를 구현한 디스크립터에 대해서만 활성화\n", 191 | "+ \\_\\_set\\_\\_이 구현되지 않은 경우 client.descriptor = \"value\" 에서 descriptor자체를 덮어쓸 수 있으므로 주의 필요" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 7, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "from typing import Callable, Any # 형 힌트 지원\n", 201 | " # Callable[[Arg1Type, Arg2Type], ReturnType] \n", 202 | "\n", 203 | "class Validation: # 검증함수와 에러 메세지만을 가지고 있고 value는 __set__ 호출 시 전달받음\n", 204 | " \"\"\"A configurable validation callable.\"\"\"\n", 205 | "\n", 206 | " def __init__(\n", 207 | " self, validation_function: Callable[[Any], bool], error_msg: str \n", 208 | " ) -> None:\n", 209 | " self.validation_function = validation_function\n", 210 | " self.error_msg = error_msg\n", 211 | "\n", 212 | " def __call__(self, value): # client.descriptor = 42 에서 42\n", 213 | " if not self.validation_function(value):\n", 214 | " raise ValueError(f\"{value!r} {self.error_msg}\")\n", 215 | "\n", 216 | "\n", 217 | "class Field:\n", 218 | " \"\"\"A class attribute with validation functions configured over it.\"\"\"\n", 219 | "\n", 220 | " def __init__(self, *validations):\n", 221 | " self._name = None\n", 222 | " self.validations = validations\n", 223 | "\n", 224 | " def __set_name__(self, owner, name):\n", 225 | " self._name = name\n", 226 | "\n", 227 | " def __get__(self, instance, owner):\n", 228 | " if instance is None:\n", 229 | " return self\n", 230 | " return instance.__dict__[self._name]\n", 231 | "\n", 232 | " def validate(self, value):\n", 233 | " for validation in self.validations:\n", 234 | " validation(value)\n", 235 | "\n", 236 | " def __set__(self, instance, value): # client.descriptor = 42 처럼 대입 시 실행됨\n", 237 | " self.validate(value) # 여기서 raise ValueError 안된다면 다음 줄에서 대입됨\n", 238 | " instance.__dict__[self._name] = value\n", 239 | "\n", 240 | "\n", 241 | "class ClientClass:\n", 242 | " descriptor = Field(\n", 243 | " Validation(lambda x: isinstance(x, (int, float)), \"is not a number\"),\n", 244 | " Validation(lambda x: x >= 0, \"is not >= 0\"),\n", 245 | " )" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": 8, 251 | "metadata": {}, 252 | "outputs": [ 253 | { 254 | "name": "stdout", 255 | "output_type": "stream", 256 | "text": [ 257 | "42\n" 258 | ] 259 | }, 260 | { 261 | "ename": "ValueError", 262 | "evalue": "-42 is not >= 0", 263 | "output_type": "error", 264 | "traceback": [ 265 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 266 | "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", 267 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mclient\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdescriptor\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0mclient\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdescriptor\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m-\u001b[0m\u001b[0;36m42\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 268 | "\u001b[0;32m\u001b[0m in \u001b[0;36m__set__\u001b[0;34m(self, instance, value)\u001b[0m\n\u001b[1;32m 36\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__set__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minstance\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# client.descriptor = 42 처럼 대입 시 실행됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 38\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# 여기서 raise ValueError 안된다면 다음 줄에서 대입됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 39\u001b[0m \u001b[0minstance\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__dict__\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_name\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 269 | "\u001b[0;32m\u001b[0m in \u001b[0;36mvalidate\u001b[0;34m(self, value)\u001b[0m\n\u001b[1;32m 33\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mvalidate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\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 34\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvalidation\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidations\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 35\u001b[0;31m \u001b[0mvalidation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\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 36\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__set__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minstance\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# client.descriptor = 42 처럼 대입 시 실행됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 270 | "\u001b[0;32m\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, value)\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# client.descriptor = 42 에서 42\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidation_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\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---> 15\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"{value!r} {self.error_msg}\"\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 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 271 | "\u001b[0;31mValueError\u001b[0m: -42 is not >= 0" 272 | ] 273 | } 274 | ], 275 | "source": [ 276 | "client = ClientClass()\n", 277 | "client.descriptor = 42\n", 278 | "print(client.descriptor)\n", 279 | "\n", 280 | "client.descriptor = -42" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 9, 286 | "metadata": {}, 287 | "outputs": [ 288 | { 289 | "ename": "ValueError", 290 | "evalue": "'string' is not a number", 291 | "output_type": "error", 292 | "traceback": [ 293 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 294 | "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", 295 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mclient\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdescriptor\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"string\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 296 | "\u001b[0;32m\u001b[0m in \u001b[0;36m__set__\u001b[0;34m(self, instance, value)\u001b[0m\n\u001b[1;32m 36\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__set__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minstance\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# client.descriptor = 42 처럼 대입 시 실행됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 38\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# 여기서 raise ValueError 안된다면 다음 줄에서 대입됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 39\u001b[0m \u001b[0minstance\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__dict__\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_name\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 297 | "\u001b[0;32m\u001b[0m in \u001b[0;36mvalidate\u001b[0;34m(self, value)\u001b[0m\n\u001b[1;32m 33\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mvalidate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\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 34\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mvalidation\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidations\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 35\u001b[0;31m \u001b[0mvalidation\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\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 36\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 37\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__set__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minstance\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# client.descriptor = 42 처럼 대입 시 실행됨\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 298 | "\u001b[0;32m\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, value)\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;31m# client.descriptor = 42 에서 42\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidation_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\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---> 15\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"{value!r} {self.error_msg}\"\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 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 299 | "\u001b[0;31mValueError\u001b[0m: 'string' is not a number" 300 | ] 301 | } 302 | ], 303 | "source": [ 304 | "client.descriptor = \"string\"" 305 | ] 306 | }, 307 | { 308 | "cell_type": "markdown", 309 | "metadata": {}, 310 | "source": [ 311 | "#### \\_\\_delete\\_\\_(self, instance)\n", 312 | "+ name과 email을 None으로 직접 설정하지 못하도록 \\_\\_set\\_\\_ 함수 구현\n", 313 | "+ \\_\\_ delete\\_\\_ 를 통해서만 email의 None설정 가능" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": 10, 319 | "metadata": {}, 320 | "outputs": [], 321 | "source": [ 322 | "class ProtectedAttribute:\n", 323 | " def __init__(self, requires_role=None) -> None:\n", 324 | " self.permission_required = requires_role\n", 325 | " self._name = None\n", 326 | "\n", 327 | " def __set_name__(self, owner, name):\n", 328 | " self._name = name\n", 329 | "\n", 330 | " def __set__(self, user, value):\n", 331 | " if value is None:\n", 332 | " raise ValueError(f\"{self._name} can't be set to None\")\n", 333 | " user.__dict__[self._name] = value\n", 334 | "\n", 335 | " def __delete__(self, user):\n", 336 | " if self.permission_required in user.permissions:\n", 337 | " user.__dict__[self._name] = None\n", 338 | " else:\n", 339 | " raise ValueError(\n", 340 | " f\"User {user!s} doesn't have {self.permission_required} \"\n", 341 | " \"permission\"\n", 342 | " )\n", 343 | "\n", 344 | "\n", 345 | "class User:\n", 346 | " \"\"\"Only users with \"admin\" privileges can remove their email address.\"\"\"\n", 347 | "\n", 348 | " email = ProtectedAttribute(requires_role=\"admin\")\n", 349 | "\n", 350 | " def __init__(\n", 351 | " self, username: str, email: str, permission_list: list = None\n", 352 | " ) -> None:\n", 353 | " self.username = username\n", 354 | " self.email = email\n", 355 | " self.permissions = permission_list or []\n", 356 | "\n", 357 | " def __str__(self):\n", 358 | " return self.username" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": 11, 364 | "metadata": {}, 365 | "outputs": [], 366 | "source": [ 367 | "admin = User(\"root\", \"root@d.com\", [\"admin\"])\n", 368 | "user = User(\"user\", \"user@d.com\", [\"email\", \"helpdesk\"])" 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": 12, 374 | "metadata": {}, 375 | "outputs": [ 376 | { 377 | "data": { 378 | "text/plain": [ 379 | "True" 380 | ] 381 | }, 382 | "execution_count": 12, 383 | "metadata": {}, 384 | "output_type": "execute_result" 385 | } 386 | ], 387 | "source": [ 388 | "del admin.email\n", 389 | "admin.email is None" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": 13, 395 | "metadata": {}, 396 | "outputs": [ 397 | { 398 | "ename": "ValueError", 399 | "evalue": "User user doesn't have admin permission", 400 | "output_type": "error", 401 | "traceback": [ 402 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 403 | "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", 404 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mdel\u001b[0m \u001b[0muser\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0memail\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 405 | "\u001b[0;32m\u001b[0m in \u001b[0;36m__delete__\u001b[0;34m(self, user)\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m raise ValueError(\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0;34mf\"User {user!s} doesn't have {self.permission_required} \"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 20\u001b[0m \u001b[0;34m\"permission\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 21\u001b[0m )\n", 406 | "\u001b[0;31mValueError\u001b[0m: User user doesn't have admin permission" 407 | ] 408 | } 409 | ], 410 | "source": [ 411 | "del user.email" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "#### \\_\\_set_name\\_\\_(self, ower, name)\n", 419 | "+ 디스크립터가 처리하려는 속성의 이름을 알려줌\n", 420 | "+ 클래스 데코레이터, 메타클래스를 사용해야하는 종전의 방법을 개선함(3.6 부터)" 421 | ] 422 | }, 423 | { 424 | "cell_type": "markdown", 425 | "metadata": {}, 426 | "source": [ 427 | "## 디스크립터의 유형\n", 428 | "+ \\_\\_set\\_\\__, \\_\\_delete\\_\\__ 메서드를 구현했다면 데이터 디스크립터로 구분됨\n", 429 | "+ \\_\\_get\\_\\__만 구현했다면 비데이터 디스크립터로 구분됨" 430 | ] 431 | }, 432 | { 433 | "cell_type": "markdown", 434 | "metadata": {}, 435 | "source": [ 436 | "### 비데이터 디스크립터\n", 437 | "+ set을 구현하지 않았으므로 객체의 사전이 우선 순위를 갖고 그 다음에 디스크립터가 실행됨" 438 | ] 439 | }, 440 | { 441 | "cell_type": "code", 442 | "execution_count": 14, 443 | "metadata": {}, 444 | "outputs": [], 445 | "source": [ 446 | "class NonDataDesciptor:\n", 447 | " def __get__(self, instance, owner):\n", 448 | " if instance is None:\n", 449 | " return self\n", 450 | " return 42\n", 451 | " \n", 452 | "\n", 453 | "class ClientClass:\n", 454 | " descriptor = NonDataDesciptor() " 455 | ] 456 | }, 457 | { 458 | "cell_type": "code", 459 | "execution_count": 15, 460 | "metadata": {}, 461 | "outputs": [ 462 | { 463 | "data": { 464 | "text/plain": [ 465 | "42" 466 | ] 467 | }, 468 | "execution_count": 15, 469 | "metadata": {}, 470 | "output_type": "execute_result" 471 | } 472 | ], 473 | "source": [ 474 | "client = ClientClass()\n", 475 | "client.descriptor" 476 | ] 477 | }, 478 | { 479 | "cell_type": "code", 480 | "execution_count": 16, 481 | "metadata": {}, 482 | "outputs": [ 483 | { 484 | "data": { 485 | "text/plain": [ 486 | "{}" 487 | ] 488 | }, 489 | "execution_count": 16, 490 | "metadata": {}, 491 | "output_type": "execute_result" 492 | } 493 | ], 494 | "source": [ 495 | "vars(client) # 내부 딕셔너리에 아무것도 등록되지 않음" 496 | ] 497 | }, 498 | { 499 | "cell_type": "code", 500 | "execution_count": 17, 501 | "metadata": {}, 502 | "outputs": [ 503 | { 504 | "data": { 505 | "text/plain": [ 506 | "{'descriptor': 99}" 507 | ] 508 | }, 509 | "execution_count": 17, 510 | "metadata": {}, 511 | "output_type": "execute_result" 512 | } 513 | ], 514 | "source": [ 515 | "client.descriptor = 99\n", 516 | "vars(client)" 517 | ] 518 | }, 519 | { 520 | "cell_type": "code", 521 | "execution_count": 18, 522 | "metadata": {}, 523 | "outputs": [ 524 | { 525 | "data": { 526 | "text/plain": [ 527 | "99" 528 | ] 529 | }, 530 | "execution_count": 18, 531 | "metadata": {}, 532 | "output_type": "execute_result" 533 | } 534 | ], 535 | "source": [ 536 | "client.descriptor " 537 | ] 538 | }, 539 | { 540 | "cell_type": "code", 541 | "execution_count": 19, 542 | "metadata": {}, 543 | "outputs": [ 544 | { 545 | "data": { 546 | "text/plain": [ 547 | "42" 548 | ] 549 | }, 550 | "execution_count": 19, 551 | "metadata": {}, 552 | "output_type": "execute_result" 553 | } 554 | ], 555 | "source": [ 556 | "del client.descriptor\n", 557 | "client.descriptor # 비데이터 디스크립터의 경우 내부 딕셔너리보다 후순위로 검색되는 것을 알 수 있음" 558 | ] 559 | }, 560 | { 561 | "cell_type": "markdown", 562 | "metadata": {}, 563 | "source": [ 564 | "### 데이터 디스크립터\n", 565 | "+ set을 구현했으므로 객체 사전보다 높은 우선순위를 갖게 됨\n", 566 | "+ 디스크립터의 set에서 setattr()이나 할당 표현식을 직접 사용하면 무한루프가 발생하므로 주의가 필요함" 567 | ] 568 | }, 569 | { 570 | "cell_type": "code", 571 | "execution_count": 20, 572 | "metadata": {}, 573 | "outputs": [], 574 | "source": [ 575 | "class DataDesciptor:\n", 576 | " \n", 577 | " def __init__(self):\n", 578 | " self._name = None\n", 579 | " \n", 580 | " def __get__(self, instance, owner):\n", 581 | " if instance is None:\n", 582 | " return self\n", 583 | " return 42\n", 584 | " \n", 585 | " def __set__(self, instance, value):\n", 586 | " instance.__dict__[self._name] = value\n", 587 | "\n", 588 | " def __set_name__(self, ower, name):\n", 589 | " self._name = name \n", 590 | "\n", 591 | " \n", 592 | "class ClientClass:\n", 593 | " descriptor = DataDesciptor()" 594 | ] 595 | }, 596 | { 597 | "cell_type": "code", 598 | "execution_count": 21, 599 | "metadata": {}, 600 | "outputs": [ 601 | { 602 | "data": { 603 | "text/plain": [ 604 | "42" 605 | ] 606 | }, 607 | "execution_count": 21, 608 | "metadata": {}, 609 | "output_type": "execute_result" 610 | } 611 | ], 612 | "source": [ 613 | "client = ClientClass()\n", 614 | "client.descriptor" 615 | ] 616 | }, 617 | { 618 | "cell_type": "code", 619 | "execution_count": 22, 620 | "metadata": {}, 621 | "outputs": [ 622 | { 623 | "data": { 624 | "text/plain": [ 625 | "{}" 626 | ] 627 | }, 628 | "execution_count": 22, 629 | "metadata": {}, 630 | "output_type": "execute_result" 631 | } 632 | ], 633 | "source": [ 634 | "vars(client) # 내부 딕셔너리에 아무것도 등록되지 않음" 635 | ] 636 | }, 637 | { 638 | "cell_type": "code", 639 | "execution_count": 23, 640 | "metadata": {}, 641 | "outputs": [ 642 | { 643 | "data": { 644 | "text/plain": [ 645 | "{'descriptor': 99}" 646 | ] 647 | }, 648 | "execution_count": 23, 649 | "metadata": {}, 650 | "output_type": "execute_result" 651 | } 652 | ], 653 | "source": [ 654 | "client.descriptor = 99\n", 655 | "vars(client)" 656 | ] 657 | }, 658 | { 659 | "cell_type": "code", 660 | "execution_count": 24, 661 | "metadata": {}, 662 | "outputs": [ 663 | { 664 | "data": { 665 | "text/plain": [ 666 | "42" 667 | ] 668 | }, 669 | "execution_count": 24, 670 | "metadata": {}, 671 | "output_type": "execute_result" 672 | } 673 | ], 674 | "source": [ 675 | "client.descriptor # 내부 딕셔너리 보다도 우선순위가 높음, delete가 구현되어 있지 않으므로 del을 사용할 수 없음" 676 | ] 677 | }, 678 | { 679 | "cell_type": "code", 680 | "execution_count": 25, 681 | "metadata": {}, 682 | "outputs": [ 683 | { 684 | "data": { 685 | "text/plain": [ 686 | "99" 687 | ] 688 | }, 689 | "execution_count": 25, 690 | "metadata": {}, 691 | "output_type": "execute_result" 692 | } 693 | ], 694 | "source": [ 695 | "client.__dict__[\"descriptor\"]" 696 | ] 697 | }, 698 | { 699 | "cell_type": "markdown", 700 | "metadata": {}, 701 | "source": [ 702 | "## 디스크립터 실전\n", 703 | "+ 디스크립터는 클라이언트 클래스와 완전히 독립적이여야 함\n", 704 | "+ 어떠한 비즈니스 로직도 포함되어 있지 않아야 함\n", 705 | "+ 라이브러리, 프레임워크 또는 내부 API를 정의해야 함" 706 | ] 707 | }, 708 | { 709 | "cell_type": "code", 710 | "execution_count": 26, 711 | "metadata": {}, 712 | "outputs": [], 713 | "source": [ 714 | "\"\"\" 여행지를 누적하는 디스크립터 \"\"\"\n", 715 | "\n", 716 | "class HistoryTracedAttribute:\n", 717 | " \"\"\"Trace the values of this attribute into another one given by the name at\n", 718 | " ``trace_attribute_name``.\n", 719 | " \"\"\"\n", 720 | "\n", 721 | " def __init__(self, trace_attribute_name: str) -> None:\n", 722 | " self.trace_attribute_name = trace_attribute_name # cities_visited\n", 723 | " self._name = None\n", 724 | "\n", 725 | " def __set_name__(self, owner, name):\n", 726 | " self._name = name\n", 727 | "\n", 728 | " def __get__(self, instance, owner):\n", 729 | " if instance is None:\n", 730 | " return self\n", 731 | " return instance.__dict__[self._name] # current_city\n", 732 | "\n", 733 | " def __set__(self, instance, value):\n", 734 | " self._track_change_in_value_for_instance(instance, value)\n", 735 | " instance.__dict__[self._name] = value # current_city\n", 736 | "\n", 737 | " def _track_change_in_value_for_instance(self, instance, value):\n", 738 | " self._set_default(instance)\n", 739 | " if self._needs_to_track_change(instance, value):\n", 740 | " instance.__dict__[self.trace_attribute_name].append(value) # 키값의 리스트에 여행을 추가\n", 741 | "\n", 742 | " def _needs_to_track_change(self, instance, value) -> bool:\n", 743 | " \"\"\"Determine if the value change needs to be traced or not.\n", 744 | " Rules for adding a value to the trace:\n", 745 | " * If the value is not previously set (it's the first one).\n", 746 | " * If the new value is != than the current one.\n", 747 | " \"\"\"\n", 748 | " try:\n", 749 | " current_value = instance.__dict__[self._name]\n", 750 | " except KeyError: # 키가 없으면 발생\n", 751 | " return True\n", 752 | " return value != current_value # 현재 키값도 새로운 키값이 다른 경우에만 리스트에 추가하도록 True를 리턴함\n", 753 | "\n", 754 | " def _set_default(self, instance):\n", 755 | " instance.__dict__.setdefault(self.trace_attribute_name, []) # 키가 없으면 기본값으로 리스트를 반환\n", 756 | "\n", 757 | "\n", 758 | "class Traveller:\n", 759 | " \"\"\"A person visiting several cities.\n", 760 | " We wish to track the path of the traveller, as he or she is visiting each new city.\n", 761 | " \"\"\"\n", 762 | "\n", 763 | " current_city = HistoryTracedAttribute(\"cities_visited\")\n", 764 | "\n", 765 | " def __init__(self, name, current_city):\n", 766 | " self.name = name\n", 767 | " self.current_city = current_city" 768 | ] 769 | }, 770 | { 771 | "cell_type": "code", 772 | "execution_count": 27, 773 | "metadata": {}, 774 | "outputs": [ 775 | { 776 | "data": { 777 | "text/plain": [ 778 | "['Barcelona', 'Paris', 'Brussels', 'Amsterdam']" 779 | ] 780 | }, 781 | "execution_count": 27, 782 | "metadata": {}, 783 | "output_type": "execute_result" 784 | } 785 | ], 786 | "source": [ 787 | "alice = Traveller(\"Alice\", \"Barcelona\")\n", 788 | "alice.current_city = \"Paris\"\n", 789 | "alice.current_city = \"Brussels\"\n", 790 | "alice.current_city = \"Brussels\"\n", 791 | "alice.current_city = \"Amsterdam\"\n", 792 | "alice.cities_visited" 793 | ] 794 | }, 795 | { 796 | "cell_type": "code", 797 | "execution_count": 28, 798 | "metadata": {}, 799 | "outputs": [ 800 | { 801 | "data": { 802 | "text/plain": [ 803 | "'Amsterdam'" 804 | ] 805 | }, 806 | "execution_count": 28, 807 | "metadata": {}, 808 | "output_type": "execute_result" 809 | } 810 | ], 811 | "source": [ 812 | "alice.current_city" 813 | ] 814 | }, 815 | { 816 | "cell_type": "markdown", 817 | "metadata": {}, 818 | "source": [ 819 | "#### 클래스 데코레이터 피하기" 820 | ] 821 | }, 822 | { 823 | "cell_type": "code", 824 | "execution_count": 29, 825 | "metadata": {}, 826 | "outputs": [], 827 | "source": [ 828 | "from datetime import datetime\n", 829 | "from functools import partial\n", 830 | "from typing import Any, Callable\n", 831 | "\n", 832 | "\n", 833 | "class BaseFieldTransformation:\n", 834 | " \"\"\"Base class to define descriptors that convert values.\"\"\"\n", 835 | "\n", 836 | " def __init__(self, transformation: Callable[[Any, str], str]) -> None:\n", 837 | " self._name = None\n", 838 | " self.transformation = transformation\n", 839 | "\n", 840 | " def __get__(self, instance, owner):\n", 841 | " if instance is None:\n", 842 | " return self\n", 843 | " raw_value = instance.__dict__[self._name]\n", 844 | " return self.transformation(raw_value)\n", 845 | "\n", 846 | " def __set_name__(self, owner, name):\n", 847 | " self._name = name\n", 848 | "\n", 849 | " def __set__(self, instance, value):\n", 850 | " instance.__dict__[self._name] = value\n", 851 | "\n", 852 | "\n", 853 | "ShowOriginal = partial(BaseFieldTransformation, transformation=lambda x: x) # transformation이 자동 설정됨\n", 854 | "HideField = partial(BaseFieldTransformation, transformation=lambda x: \"**redacted**\")\n", 855 | "FormatTime = partial(\n", 856 | " BaseFieldTransformation,\n", 857 | " transformation=lambda ft: ft.strftime(\"%Y-%m-%d %H:%M\"),\n", 858 | ")\n", 859 | "\n", 860 | "\n", 861 | "class LoginEvent:\n", 862 | " username = ShowOriginal()\n", 863 | " password = HideField()\n", 864 | " ip = ShowOriginal()\n", 865 | " timestamp = FormatTime()\n", 866 | "\n", 867 | " def __init__(self, username, password, ip, timestamp):\n", 868 | " self.username = username\n", 869 | " self.password = password\n", 870 | " self.ip = ip\n", 871 | " self.timestamp = timestamp\n", 872 | "\n", 873 | " def serialize(self):\n", 874 | " return {\n", 875 | " \"username\": self.username,\n", 876 | " \"password\": self.password,\n", 877 | " \"ip\": self.ip,\n", 878 | " \"timestamp\": self.timestamp,\n", 879 | " }\n", 880 | "\n", 881 | "\n", 882 | "class BaseEvent:\n", 883 | " \"\"\"Abstract the serialization and the __init__\"\"\"\n", 884 | "\n", 885 | " def __init__(self, **kwargs):\n", 886 | " self.__dict__.update(kwargs)\n", 887 | "\n", 888 | " def serialize(self):\n", 889 | " return {\n", 890 | " attr: getattr(self, attr) for attr in self._fields_to_serialize()\n", 891 | " }\n", 892 | "\n", 893 | " def _fields_to_serialize(self):\n", 894 | " for attr_name, value in vars(self.__class__).items():\n", 895 | " if isinstance(value, BaseFieldTransformation): # '__module__, __doc__ 등은 제외하고 변환함\n", 896 | " yield attr_name\n", 897 | "\n", 898 | "\n", 899 | "class NewLoginEvent(BaseEvent):\n", 900 | " \"\"\"A class that takes advantage of the base to only define the fields.\"\"\"\n", 901 | "\n", 902 | " username = ShowOriginal()\n", 903 | " password = HideField()\n", 904 | " ip = ShowOriginal()\n", 905 | " timestamp = FormatTime()" 906 | ] 907 | }, 908 | { 909 | "cell_type": "code", 910 | "execution_count": 30, 911 | "metadata": {}, 912 | "outputs": [], 913 | "source": [ 914 | "le = NewLoginEvent(username = \"usr\",\n", 915 | " password = \"secret password\",\n", 916 | " ip = \"127.0.0.1\",\n", 917 | " timestamp = datetime(2016, 7, 20, 15, 45))" 918 | ] 919 | }, 920 | { 921 | "cell_type": "code", 922 | "execution_count": 31, 923 | "metadata": {}, 924 | "outputs": [ 925 | { 926 | "data": { 927 | "text/plain": [ 928 | "{'username': 'usr',\n", 929 | " 'password': '**redacted**',\n", 930 | " 'ip': '127.0.0.1',\n", 931 | " 'timestamp': '2016-07-20 15:45'}" 932 | ] 933 | }, 934 | "execution_count": 31, 935 | "metadata": {}, 936 | "output_type": "execute_result" 937 | } 938 | ], 939 | "source": [ 940 | "le.serialize()" 941 | ] 942 | }, 943 | { 944 | "cell_type": "markdown", 945 | "metadata": {}, 946 | "source": [ 947 | "### 파이썬 내부의 디스크립터 활용" 948 | ] 949 | }, 950 | { 951 | "cell_type": "code", 952 | "execution_count": 32, 953 | "metadata": {}, 954 | "outputs": [], 955 | "source": [ 956 | "\"\"\" 함수와 메서드 \"\"\"\n", 957 | "\n", 958 | "class Method:\n", 959 | " def __init__(self, name):\n", 960 | " self.name = name\n", 961 | " \n", 962 | " def __call__(self, instance, arg1, arg2):\n", 963 | " print(f\"{self.name}: {instance} 호출, 인자는 {arg1}과 {arg2}\")\n", 964 | " \n", 965 | "class MyClass:\n", 966 | " method = Method(\"Internal call\")" 967 | ] 968 | }, 969 | { 970 | "cell_type": "code", 971 | "execution_count": 33, 972 | "metadata": {}, 973 | "outputs": [ 974 | { 975 | "name": "stdout", 976 | "output_type": "stream", 977 | "text": [ 978 | "External call: <__main__.MyClass object at 0x7fdd7402d450> 호출, 인자는 1st과 2nd\n" 979 | ] 980 | } 981 | ], 982 | "source": [ 983 | "instance = MyClass()\n", 984 | "Method(\"External call\")(instance, \"1st\", \"2nd\")" 985 | ] 986 | }, 987 | { 988 | "cell_type": "code", 989 | "execution_count": 34, 990 | "metadata": {}, 991 | "outputs": [ 992 | { 993 | "ename": "TypeError", 994 | "evalue": "__call__() missing 1 required positional argument: 'arg2'", 995 | "output_type": "error", 996 | "traceback": [ 997 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 998 | "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", 999 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0minstance\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmethod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"1st\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"2nd\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 1000 | "\u001b[0;31mTypeError\u001b[0m: __call__() missing 1 required positional argument: 'arg2'" 1001 | ] 1002 | } 1003 | ], 1004 | "source": [ 1005 | "instance.method(\"1st\", \"2nd\")" 1006 | ] 1007 | }, 1008 | { 1009 | "cell_type": "code", 1010 | "execution_count": 35, 1011 | "metadata": {}, 1012 | "outputs": [], 1013 | "source": [ 1014 | "\"\"\" 함수와 메서드, get을 구현하여 클래스 메소드로도 사용할 수 있도록 개선 \"\"\"\n", 1015 | "from types import MethodType\n", 1016 | "\n", 1017 | "class Method:\n", 1018 | " def __init__(self, name):\n", 1019 | " self.name = name\n", 1020 | " \n", 1021 | " def __call__(self, instance, arg1, arg2):\n", 1022 | " print(f\"{self.name}: {instance} 호출, 인자는 {arg1}과 {arg2}\")\n", 1023 | " \n", 1024 | " def __get__(self, myclass_inst, myclass_cls):\n", 1025 | " if myclass_inst == None:\n", 1026 | " return self\n", 1027 | " else:\n", 1028 | " return MethodType(self, myclass_inst)\n", 1029 | " \n", 1030 | "class MyClass:\n", 1031 | " method = Method(\"Internal call\")" 1032 | ] 1033 | }, 1034 | { 1035 | "cell_type": "code", 1036 | "execution_count": 36, 1037 | "metadata": {}, 1038 | "outputs": [ 1039 | { 1040 | "name": "stdout", 1041 | "output_type": "stream", 1042 | "text": [ 1043 | "External call: <__main__.MyClass object at 0x7fdd6d7d0190> 호출, 인자는 1st과 2nd\n", 1044 | "Internal call: <__main__.MyClass object at 0x7fdd6d7d0190> 호출, 인자는 1st과 2nd\n" 1045 | ] 1046 | } 1047 | ], 1048 | "source": [ 1049 | "instance = MyClass()\n", 1050 | "Method(\"External call\")(instance, \"1st\", \"2nd\")\n", 1051 | "instance.method(\"1st\", \"2nd\")" 1052 | ] 1053 | }, 1054 | { 1055 | "cell_type": "code", 1056 | "execution_count": null, 1057 | "metadata": {}, 1058 | "outputs": [], 1059 | "source": [] 1060 | } 1061 | ], 1062 | "metadata": { 1063 | "kernelspec": { 1064 | "display_name": "Python 3", 1065 | "language": "python", 1066 | "name": "python3" 1067 | }, 1068 | "language_info": { 1069 | "codemirror_mode": { 1070 | "name": "ipython", 1071 | "version": 3 1072 | }, 1073 | "file_extension": ".py", 1074 | "mimetype": "text/x-python", 1075 | "name": "python", 1076 | "nbconvert_exporter": "python", 1077 | "pygments_lexer": "ipython3", 1078 | "version": "3.7.6" 1079 | } 1080 | }, 1081 | "nbformat": 4, 1082 | "nbformat_minor": 4 1083 | } 1084 | --------------------------------------------------------------------------------