├── .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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------