├── .gitignore
├── README.md
├── files
├── BetterWay01_KnowThyself.md
├── BetterWay01_to_03.py
├── BetterWay02_PythonStyleGuide.md
├── BetterWay03_Bytes_Str_Unicode.md
├── BetterWay04_UseHelpFunction.md
├── BetterWay05_HowToSequenceSlice.md
├── BetterWay06_Dontusestridetoomuch.md
├── BetterWay07_useListComp.md
├── BetterWay08_ListComprehension.md
├── BetterWay09_UseGeneratorExpression.md
├── BetterWay10_useEnumerate.md
├── BetterWay11_UseZip.md
├── BetterWay12_dontuse_else.md
├── BetterWay13_use_tryetc.md
├── BetterWay14_useexception.md
├── BetterWay15_useClosure.md
├── BetterWay16_generator.md
├── BetterWay17_IterateDefensively.md
├── BetterWay18_PositionalArg.md
├── BetterWay19_KeywordArg.md
├── BetterWay20_DynamicDefaultArgument.md
├── BetterWay21_ForceKeywordArgument.md
├── BetterWay22_UseHelperClass.md
├── BetterWay23_UseFuncForInterface.md
├── BetterWay24_classmethod.md
├── BetterWay25_InitializeSuperClassWithSuper.md
├── BetterWay26_UseMixinClass.md
├── BetterWay27_UsePrivateAttribute.md
├── BetterWay28_CustomContainer_collections.abc.md
├── BetterWay29_dontusegetter.md
├── BetterWay30_Use@property_for_refactoring.md
├── BetterWay31_UseDescriptorForReusablePropertyMethod.md
├── BetterWay32_Use__getattr__and_etc_for_lazy_attributes.md
├── BetterWay33_ValidateSubclassWithMetaclass.md
├── BetterWay34_RegisterClassWithMetaclass.md
├── BetterWay35_UseDocstringWithMetaclass.md
├── BetterWay36_Usesubprocess.md
├── BetterWay37_UseThreadForIO.md
├── BetterWay38_UseLockForRaceConditionInThread.md
├── BetterWay39_UseQueueToTuneUpTasksInThreads.md
├── BetterWay42_Use_functoolswraps.md
├── BetterWay43_UseContextlib.md
├── BetterWay44_UsePickleWithCopyreg.md
├── BetterWay45_UseDatetimeForLocalTime.md
├── BetterWay46_UseBuiltinAlgorithmsAndDataStructures.md
├── BetterWay47_UseDecimalForPrecision.md
├── BetterWay48_PypiModules.md
├── BetterWay49_WriteDocstring.md
├── BetterWay50_UsePackage.md
├── BetterWay51_DefineRootException.md
├── BetterWay52_HowToGetRidOfCircularDependency.md
├── BetterWay53_UseVirtualEnvironment.md
├── BetterWay54_ConsiderModuleScopeForDeployment.md
├── BetterWay55_UseReprForDebug.md
├── BetterWay56_UseUnittest.md
├── BetterWay57_Use_pdb.md
├── BetterWay58_ProfileBeforeOptimization.md
└── BetterWay59_UseTracemalloc.md
└── images
└── diamond-inheritance.png
/.gitignore:
--------------------------------------------------------------------------------
1 | a.txt
2 | b.txt
3 | c.txt
4 |
5 |
6 | # Created by https://www.gitignore.io/api/pycharm,python,sublimetext,django,windows
7 |
8 | ### PyCharm ###
9 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
10 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
11 |
12 | # User-specific stuff:
13 | .idea/workspace.xml
14 | .idea/tasks.xml
15 | .idea/dictionaries
16 | .idea/vcs.xml
17 | .idea/jsLibraryMappings.xml
18 |
19 | # Sensitive or high-churn files:
20 | .idea/dataSources.ids
21 | .idea/dataSources.xml
22 | .idea/dataSources.local.xml
23 | .idea/sqlDataSources.xml
24 | .idea/dynamic.xml
25 | .idea/uiDesigner.xml
26 |
27 | # Gradle:
28 | .idea/gradle.xml
29 | .idea/libraries
30 |
31 | # Mongo Explorer plugin:
32 | .idea/mongoSettings.xml
33 |
34 | ## File-based project format:
35 | *.iws
36 |
37 | ## Plugin-specific files:
38 |
39 | # IntelliJ
40 | /out/
41 |
42 | # mpeltonen/sbt-idea plugin
43 | .idea_modules/
44 |
45 | # JIRA plugin
46 | atlassian-ide-plugin.xml
47 |
48 | # Crashlytics plugin (for Android Studio and IntelliJ)
49 | com_crashlytics_export_strings.xml
50 | crashlytics.properties
51 | crashlytics-build.properties
52 | fabric.properties
53 |
54 | ### PyCharm Patch ###
55 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
56 |
57 | # *.iml
58 | # modules.xml
59 | # .idea/misc.xml
60 | # *.ipr
61 |
62 |
63 | ### Python ###
64 | # Byte-compiled / optimized / DLL files
65 | __pycache__/
66 | *.py[cod]
67 | *$py.class
68 |
69 | # C extensions
70 | *.so
71 |
72 | # Distribution / packaging
73 | .Python
74 | env/
75 | build/
76 | develop-eggs/
77 | dist/
78 | downloads/
79 | eggs/
80 | .eggs/
81 | lib/
82 | lib64/
83 | parts/
84 | sdist/
85 | var/
86 | *.egg-info/
87 | .installed.cfg
88 | *.egg
89 |
90 | # PyInstaller
91 | # Usually these files are written by a python script from a template
92 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
93 | *.manifest
94 | *.spec
95 |
96 | # Installer logs
97 | pip-log.txt
98 | pip-delete-this-directory.txt
99 |
100 | # Unit test / coverage reports
101 | htmlcov/
102 | .tox/
103 | .coverage
104 | .coverage.*
105 | .cache
106 | nosetests.xml
107 | coverage.xml
108 | *,cover
109 | .hypothesis/
110 |
111 | # Translations
112 | *.mo
113 | *.pot
114 |
115 | # Django stuff:
116 | *.log
117 | local_settings.py
118 |
119 | # Flask stuff:
120 | instance/
121 | .webassets-cache
122 |
123 | # Scrapy stuff:
124 | .scrapy
125 |
126 | # Sphinx documentation
127 | docs/_build/
128 |
129 | # PyBuilder
130 | target/
131 |
132 | # IPython Notebook
133 | .ipynb_checkpoints
134 |
135 | # pyenv
136 | .python-version
137 |
138 | # celery beat schedule file
139 | celerybeat-schedule
140 |
141 | # dotenv
142 | .env
143 |
144 | # virtualenv
145 | .venv/
146 | venv/
147 | ENV/
148 |
149 | # Spyder project settings
150 | .spyderproject
151 |
152 | # Rope project settings
153 | .ropeproject
154 |
155 |
156 | ### SublimeText ###
157 | # cache files for sublime text
158 | *.tmlanguage.cache
159 | *.tmPreferences.cache
160 | *.stTheme.cache
161 |
162 | # workspace files are user-specific
163 | *.sublime-workspace
164 |
165 | # project files should be checked into the repository, unless a significant
166 | # proportion of contributors will probably not be using SublimeText
167 | # *.sublime-project
168 |
169 | # sftp configuration file
170 | sftp-config.json
171 |
172 | # Package control specific files
173 | Package Control.last-run
174 | Package Control.ca-list
175 | Package Control.ca-bundle
176 | Package Control.system-ca-bundle
177 | Package Control.cache/
178 | Package Control.ca-certs/
179 | bh_unicode_properties.cache
180 |
181 | # Sublime-github package stores a github token in this file
182 | # https://packagecontrol.io/packages/sublime-github
183 | GitHub.sublime-settings
184 |
185 |
186 | ### Django ###
187 | *.log
188 | *.pot
189 | *.pyc
190 | __pycache__/
191 | local_settings.py
192 | db.sqlite3
193 | media
194 |
195 |
196 | ### Windows ###
197 | # Windows image file caches
198 | Thumbs.db
199 | ehthumbs.db
200 |
201 | # Folder config file
202 | Desktop.ini
203 |
204 | # Recycle Bin used on file shares
205 | $RECYCLE.BIN/
206 |
207 | # Windows Installer files
208 | *.cab
209 | *.msi
210 | *.msm
211 | *.msp
212 |
213 | # Windows shortcuts
214 | *.lnk
215 |
216 |
217 | ### Intellij ###
218 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
219 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
220 |
221 | # User-specific stuff:
222 | .idea/workspace.xml
223 | .idea/tasks.xml
224 | .idea/dictionaries
225 | .idea/vcs.xml
226 | .idea/jsLibraryMappings.xml
227 |
228 | # Sensitive or high-churn files:
229 | .idea/dataSources.ids
230 | .idea/dataSources.xml
231 | .idea/dataSources.local.xml
232 | .idea/sqlDataSources.xml
233 | .idea/dynamic.xml
234 | .idea/uiDesigner.xml
235 |
236 | # Gradle:
237 | .idea/gradle.xml
238 | .idea/libraries
239 |
240 | # Mongo Explorer plugin:
241 | .idea/mongoSettings.xml
242 |
243 | ## File-based project format:
244 | *.iws
245 |
246 | ## Plugin-specific files:
247 |
248 | # IntelliJ
249 | /out/
250 |
251 | # mpeltonen/sbt-idea plugin
252 | .idea_modules/
253 |
254 | # JIRA plugin
255 | atlassian-ide-plugin.xml
256 |
257 | # Crashlytics plugin (for Android Studio and IntelliJ)
258 | com_crashlytics_export_strings.xml
259 | crashlytics.properties
260 | crashlytics-build.properties
261 | fabric.properties
262 |
263 | ### Intellij Patch ###
264 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
265 |
266 | # *.iml
267 | # modules.xml
268 | # .idea/misc.xml
269 | # *.ipr
--------------------------------------------------------------------------------
/files/BetterWay01_KnowThyself.md:
--------------------------------------------------------------------------------
1 | # Better way 1. 사용 중인 파이썬의 버전을 알자
2 |
3 |
4 | #### 16쪽
5 |
6 | * Created : 2018/01/11
7 | * Modified: 2019/05/03
8 |
9 |
10 | ## 1. 사용하는 파이썬의 버전을 왜 알아야 할까?
11 |
12 | 프로그램은 개발자들에 의해 유지, 보수된다. 버전업될 때마다 여러 기능이 추가되거나 삭제되며,
13 | 특히 파이썬 2에서 3으로의 변화같이 프로그램의 상당 부분이 바뀌는 경우도 다분하다.
14 | 특정 파이썬 2코드가 파이썬 3에서 에러를 출력하는 경우가 꽤나 많다.
15 |
16 | 다른 개발자와 협업할 때, 버전이 다른 프로그램을 쓰는 것은 일반적으로 용납되지 않는다.
17 | 서로 같은 코드에서 결과물이 다를 수 있고, 이럴 때 다른 버전을 쓴다는 것을 인지하지 못하면
18 | 디버깅이 산으로 갈 수 있다. 협업하는 개발자들과는 같은 버전의 프로그램을 써야 하고,
19 | 그렇기 때문에 우리가 `requirements.txt` 등으로 같은 버전의 패키지를 쓰려고 하는 것일테다.
20 |
21 | 같은 버전의 프로그램을 쓰기 위해서는 내 버전을 알아야 하고,
22 | 그래서 파이썬에서 버전을 어떻게 확인하는지 아는 것도 의미가 있다.
23 |
24 |
25 |
26 |
27 |
28 | ## 2. 버전을 실제로 알아보자.
29 |
30 | 파이썬에서 버전을 확인해볼 수 있는 방법은 다음과 같다.
31 |
32 | ### 2.1. CLI에서 버전 인자를 통해 확인하기
33 |
34 | 이는 파이썬뿐만 아니라 다른 프로그램들에서도 일반적인 경우이다.
35 | 우리는 `python` 이라는 명령어를 입력하고 파이썬 인터프리터를 실행하는데(CLI에서),
36 | 알다시피 다양한 옵션을 줄 수 있다. 이때 파이썬의 버전을 확인할 수 있는 옵션은 다음 두 가지이다.
37 |
38 |
39 |
40 | ```sh
41 |
42 | python --version
43 | python -V
44 |
45 |
46 | >>> Python 3.5.2
47 | ```
48 |
49 | 둘은 완벽히 같은 결과를 낸다. 두 번째 `V`는 대문자이다.
50 | 이 명령어를 통해 파이썬 버전이 3.5.2라는 것을 확인했다.
51 |
52 | 한 가지 기억해둘건, 이 방법은 비단 파이썬에만 해당하는 내용은 아니라는 것이다.
53 | 수많은 프로그램의 버전을 위와 같은 방법을 통해 확인할 수 있다.
54 | 예를 들어 리눅스에서 가장 많이 쓰는 기능 중 하나일 `ls`도 **결국 다른 사람이 만든 프로그램이고,**
55 | 이 프로그램의 버전을 위와 같은 방법으로 확인할 수 있다.
56 |
57 |
58 | ```sh
59 | ls --version
60 |
61 |
62 | >>> ls (GNU coreutils) 8.25
63 | Copyright (C) 2016 Free Software Foundation, Inc.
64 | License GPLv3+: GNU GPL version 3 or later .
65 | This is free software: you are free to change and redistribute it.
66 | There is NO WARRANTY, to the extent permitted by law.
67 |
68 | Richard M. Stallman 및 David MacKenzie이(가) 만들었습니다.
69 | ```
70 |
71 | 난 처음에 `ls`가 명령어라고만 생각했지, 프로그램이라고는 생각 못했다.
72 | 이 방법은 기억해둘만 하다. 수많은 프로그램들에서 공통으로 쓸 수 있는 인자들이 여럿 있다.
73 | 대표적으로는 프로그램의 사용법을 확인하는 `--help` 등도 있다.
74 |
75 | 참고로 리눅스 프로그램에서 `--version`이 일반적이고 `-V`는 그렇지는 않다. `ls`에서는 이 인자를 받지
76 | 않는다.
77 |
78 |
79 |
80 |
81 |
82 |
83 | ## 2.2. sys 모듈을 사용하기
84 |
85 | 버전을 확인하는 다른 방법은 파이썬의 내장 모듈 `sys`를 사용하는 것이다.
86 | 이 모듈에는 파이썬의 버전을 출력해주는 특성이 두 개 있다.
87 |
88 | 하나는 `sys.version`으로 파이썬 버전을 문자열로 출력해준다.
89 |
90 |
91 |
92 | ```python
93 | import sys
94 |
95 | print(sys.version)
96 |
97 |
98 | >>> 3.5.2 (default, Jul 10 2017, 10:43:13)
99 | [GCC 5.4.0 20160609]
100 | ```
101 |
102 | `sys.version`는 순수한 문자열이다.
103 |
104 | 또 다른 방법은 `sys.version_info`를 사용하는 것이다.
105 | 이런 경우가 있을 수 있다. 파이썬 코드로 파이썬 프로그램을 실행하는데,
106 | 파이썬 버전에 따라 실행하는 코드를 다르게 한다고 하자.
107 |
108 | 파이썬 2와 3일 때를 구분해야 하는데, `sys.version`을 쓴다면 이 문자열을 슬라이싱하려고 할 것이다.
109 | 근데 그것뿐만 아니라 '3.2'인지, '3.6'인지도 구분해야 한다면 슬라이싱이 보다 복잡해질 것이다.
110 | 그때 `sys.version_info`를 쓴다.
111 |
112 |
113 |
114 | 가령 '3.5.2'라는 버전에서 '.'을 사이로 각 버전 정보가 구분되는데,
115 |
116 | * 3은 major
117 | * 5는 minor
118 | * 2는 micro
119 |
120 | 라고 .불린다. `version_info`는 [named tuple](https://docs.python.org/3/library/collections.html#collections.namedtuple){:target="_blank"}로서, **major, minor, micro라는 key로 각 숫자에 접근할 수 있다.**
121 | 자세히 살펴보면 다음과 같다.
122 |
123 |
124 | ```python
125 | import sys
126 |
127 | sys.version_info
128 |
129 | sys.version_info.major
130 | sys.version_info.minor
131 | sys.version_info.micro
132 |
133 |
134 | >>> sys.version_info(major=3, minor=5, micro=2, releaselevel='final', serial=0)
135 | >>> 3
136 | >>> 5
137 | >>> 2
138 | ```
139 |
140 | 위와 같은 방법으로는 파이썬 프로그램의 major, minor, micro 버전 값을 정확하고 쉽게 파악할 수 있다.
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | #### 핵심정리
149 |
150 | * 파이썬에는 CPython, Jython, PyPy 같은 다양한 런타임이 있다.
151 | * 특히 협업 프로젝트에서 팀원이 같은 파이썬 버전을 사용하는 것은 매우 중요하다.
152 | * 파이썬 2는 유지보수 정도의 지원만 남아있다. 이제는 2버전은 곧 지원이 종료되기 때문에 갈아타자.
153 |
--------------------------------------------------------------------------------
/files/BetterWay01_to_03.py:
--------------------------------------------------------------------------------
1 | import sys
2 | """
3 | Better way 1장부터 3장까지 중요하다고 생각되는 부분을 가져왔습니다.
4 | 더 많은데 한계가 있네요 ㅠㅠ 죄송합니다.
5 |
6 | """
7 |
8 |
9 | # 1장. 커맨드 라인 옵션에 대해 알자.
10 |
11 |
12 | """
13 | 제가 이 장에서 집중한 건 커맨드(터미널) 창에서 옵션이 들어갈 수 있다는 것입니다.
14 | python --version 을 통해 버젼을 확인할 수 있었는데요.
15 | 이 외에도 다른 옵션들이 들어갈 수 있습니다. 굉장히 많은데
16 |
17 | python -c "print('hi')"와 같이 파이썬을 단 한 줄만 실행시키는 '-c'
18 | python -q 와 같이 요란하게 파이썬 인터프리터를 시작 안하는 '-q'
19 | python -v 와 같이 시작부터 끝까지 인터프리터 전 과정을 출력하는 '-v' (verbose)
20 |
21 | 이 정도만 알고 나머지는 도저히 모르겠습니다...
22 |
23 | "python -h"를 통해 나오는 것을 같이 알아보면 좋겠습니다.
24 |
25 |
26 |
27 | 저는 sys.argv를 소개하겠습니다.
28 | """
29 |
30 | """
31 | sys.argv는 sys 모듈 안에 있는 argv 라는 변수입니다. sys모듈은 시스템의 약자로
32 | 파이썬 인터프리터와 관련된 설정을 다룰 수 있습니다.
33 | 여기서 argv란 무엇이냐. 파이썬 인터프리터를 커맨드나 터미널로 실행할 때 입력값으로 넣어준 것이
34 | 여기에 들어갑니다. '난 그런 것을 집어넣어본 적이 없어!'라고 말하실 수 있습니다.
35 | 맞습니다. python을 실행시켜 'import sys; sys.argv[0]'라고 명령을 치시면 '', 빈 문자열이 나옵니다.
36 | 이 상태는 단순히 파이썬 인터프리터가 실행된 상태라는거죠.
37 |
38 | """
39 |
40 |
41 | """
42 | 이와는 대조적으로,
43 | 어떤 변태들은 터미널에서 이런 것도 해보셨을텝니다. 'python foryou.py'
44 | 이렇게 하면 경로만 맞다면 foryou.py가 실행이 되죠.
45 | 이떄 만약 파일 안에 'import sys; sys.argv[0]'를 출력해보시면
46 | foryou.py 자신이 나옵니다. 이 파일이 실행된다는 뜻이구요. argv는 리스트로
47 | 첫 번째(index = 0)의 값은 무조건 가지고 있습니다.
48 | """
49 | if __name__ == "__main__":
50 | print('*' * 120+"\nsys.argv[0]의 값은 {0}입니다. ^^\n".format(sys.argv[0])+'*'*120)
51 | print('\n')
52 |
53 | """
54 | 터미널이나 커맨드를 이용하시면 옵션을 많이 넣어보셨을겁니다.
55 | 마찬가지죠. python을 실행할 때도 실행할 파일과 함께 많은 것을 넣을 수 있습니다.
56 |
57 | 'python foryou.py 12345'와 같이 입력해본다고 하면
58 |
59 | foryou.py가 실행되며 12345라는 입력값이 실행되는 것입니다.
60 | 이 안에는 무엇이 벌어지고 있느냐!
61 | sys.argv[0] = foryou.py, sys.argv[1] = 12345가 됩니다.
62 | sys.argv[1]을 사용해 연산을 할 수 있다는 말이 됩니다.
63 |
64 | """
65 |
66 |
67 |
68 | """
69 | 저는 이렇게 사용해보겠습니다.
70 | 저는 저만의 야구동영상 파일을 관리하는 py 파일을 가지고 있고 비밀번호를 입력해서 맞아야만
71 | 그 파일이 실행됩니다. 그 비밀번호는 123456이 됩니다. 실행하는 사람이 비밀번호를 처음에 제대로 입력해야 하는 것이죠.
72 | python baseball.py 123456과 같이 말이죠.
73 | 밑에는 그 코드를 짜보겠습니다.
74 | """
75 |
76 |
77 | if __name__ == '__main__':
78 | entry_permit = 0
79 | while True:
80 | if sys.argv[1] == 123456:
81 | print("환영합니다.")
82 | break
83 | else:
84 | sys.argv[1] == int(input("비밀번호를 똑바로 입력해주십시요 : " if entry_permit != 2 else "마지막입니다."))
85 | entry_permit += 1
86 | if entry_permit == 2:
87 | print("야구를 보실 수 없습니다. 종료합니다.")
88 | sys.exit()
89 |
90 | """
91 | 이것을 실행해보시면 비밀번호를 옳게 입력하면 야구를 볼 수 없고
92 | 3번 이상 틀리면 프로그램이 종료됩니다.
93 |
94 | sys.argv를 이런 식으로 사용해볼 수도 있겠습니다.
95 | 더 다양한 예는 무엇이 있을지요?
96 | """
97 |
98 |
99 |
100 | """
101 | string, bytes는 예를 하나만 보겠습니다.
102 |
103 | 터미널 창에
104 | python -c "from urllib import request; print(request.urlopen('http://www.naver.com').read())"
105 | 복붙 ㄱㄱ ↑
106 | -c 옵션이 가지는 힘은 이렇게 드러납니다. -c에는 한 줄만 입력할 수 있는데 ';'를 통해서면 여러 줄을 한 줄로 연결할
107 | 수 있습니다. 저 식은 urllib이라는 웹 url 다루는 패지키에서 request 모듈을 임포트 한뒤, 네이버 html 페이지를 읽어
108 | 출력하는 코드입니다. urllib은 저도 딱 저 정도만 할 수 있습니다. 나머지는 장고 시간에...
109 |
110 |
111 | 결과를 보시면 아마 한 글자도 이해 못하실 겁니다.
112 | python -c "from urllib import request; print(request.urlopen('http://www.naver.com').read().decode())"
113 | 이렇게 하시면 한글은 보이실거에요. 이렇게 바이트로 리드된 것을 str로 디코드 할 수 있습니다.
114 | (네이버 겁나 깁니다...)
115 | """
116 |
--------------------------------------------------------------------------------
/files/BetterWay02_PythonStyleGuide.md:
--------------------------------------------------------------------------------
1 | # Better way 02. PEP 8 스타일 가이드를 따르자
2 |
3 | #### 18쪽
4 |
5 | * Created : 2018/01/11
6 | * Modified: 2019/05/03
7 |
8 |
9 |
10 | **PEP(Python Enhancement Proposal), 다른 말로 파이썬 개선 제안서는 파이썬 코드를 어떻게 구성할지 알려주는 스타일 가이드.**
11 |
12 | Syntax만 지킨다면 코드를 짜는 것은 개발자 본인 마음대로이지만 개발에서 협엽이 필수로 자리잡고 있는 요즈음, 모두에게 일반적으로 통용되는 방식으로 코드를 짜는 것이 효율적이다. 그런 의미에서 PEP는 어떤 식으로 코드를 짜야 할지를 제안하는 역할을 한다.
13 |
14 |
15 | [여기](https://www.python.org/dev/peps/pep-0008/)에 관련된 매우 많은 내용이 있다. 그러나 현실적으로 모두 읽기는 힘들고, 책에 소개된 내용 중 정말 중요하다고 생각되는 것들을 하나씩 정리하고자 한다.
16 |
17 |
18 | ## 1. 공백(whitespace)
19 |
20 | 1. 탭이 아닌 스페이스로 들여쓴다. PyCharm 같은 IDE는 탭을 입력해도 SPACE로 변환하도록 디폴트 설정되어 있다.
21 | 2. 문법적으로 의미 있는 들여쓰기는 각 수준마다 스페이스 **4개를** 사용한다.
22 | 3. 표현식이 길어서 다음 줄로 이어지면 일반적인 들여쓰기 수준에 추가로 스페이스 4개를 더 들여쓴다.
23 | 4. 파일에서 함수와 클래스는 빈 줄 두 개로 구분해야 한다.
24 | 5. 클래스에서 메서드는 빈 줄 하나로 구분해야 한다.
25 |
26 |
27 |
28 |
29 | ## 2. 명명(naming)
30 |
31 | 1. 함수, 변수, 속성은 lowercase_underscore 형식을 지킨다.
32 | 2. 보호(protected) 인스턴스 속성은 _leading_underscore 형식을 지킨다.
33 | 3. 비공개(private) 인스턴스 속성은 __double_leading_underscore 형식을 지킨다.
34 | 4. 클래스와 예외는 CapitalizedWord 형식을 지킨다.
35 | 5. 모듈 수준 상수는 ALL_CAPS 형식을 따른다.
36 | 6. 클래스의 인스턴스 메소드의 첫 번째 파라미터는 'self'로 한다.
37 | 7. 클래스 메서드에서는 첫 번째 파라미터는 'cls'로 한다.
38 |
39 |
40 |
41 |
42 | ## 3. 표현식과 문장
43 |
44 | 1. 긍정 표현식의 부정(if not a is b)대신에 인라인 부정(if a is not b)을 사용한다.
45 | 2. 빈 값을 확인할 때 길이를 비교하는 형식(ex. _if len(somelist) == 0_)이 아닌
46 | _if not somelist_ 를 사용하자. **파이썬은 조건식에서 빈 문자열, 리스트는 False로 처리한다.**
47 | 3. 항상 파일의 맨 앞에 `import`문을 놓는다.
48 | 4. 임포트는 '표준 라이브러리의 모듈, 서드파티 모듈, 자신이 만든 모듈' 섹션 순으로 구분해야 한다.
49 | 각각의 하위 섹션에서는 알파벳 순서로 임포트한다.
50 |
51 |
52 |
53 |
54 | 어떤 언어이든 그 언어의 관습을 지켜주는 것이 중요하다. 개발자들은 그것에 대한 자부심까지 가지고 있는 것 같다.
55 | 이를 확인하고 잘 지키는 것은 언제나 이득이다. 파이썬에 더 다가가고 싶다면 언젠가 공부할 수 있도록 하자.
56 |
--------------------------------------------------------------------------------
/files/BetterWay03_Bytes_Str_Unicode.md:
--------------------------------------------------------------------------------
1 | # Better way 03. bytes, str, unicode의 차이점을 알자
2 |
3 | #### 20쪽
4 |
5 | * Created : 2016/09/10
6 | * Modified: 2019/05/03
7 |
8 |
9 | ## 1. Introduce
10 |
11 | 인코딩은 중요하다. 리눅스, 맥, 윈도우즈에서 파일을 옮겨본 사람들은 동의할 것이다.
12 | 생각해보자. 내가 웹 앱을 운영하는데 수많은 운영체제을 통한 서로 다른 인코딩 문자의 입력과 마주할 수 있다.
13 | 난 CP949로 일을 처리할 수 있는데 클라이언트가 UTF-8로 입력을 보내면 어떻게 대처해야 할까?
14 |
15 | 아마도, **상대방의 utf-8을 binary stream으로 변환하고 그 바이츠를 cp949로 다시 인코딩해야 할 것이다.**
16 | 즉, 먼저 상대방의 입력을 바이너리로 인코딩하고, 그 결과를 CP949로 디코딩해야 할 것이다.
17 |
18 | **3장은 인코딩과 관련된 장이고 파이썬 인코딩에 대한 기초적인 지식만 적어본다.**
19 |
20 |
21 | **파이썬에서 문자 시퀀스를 나타내는 방식은 유니코드 문자열 방식과 바이츠(raw 8 bit) 형식 2가지다.**
22 |
23 | * **파이썬 3는 문자는 str, 바이츠는 bytes 클래스로 표현하고**
24 | * **파이썬 2는 문자는 unicode, 바이츠는 str 클래스로 표현한다. 헷갈리지 말아야 한다.**
25 |
26 | 파이썬 2, 3간 연관된 바이너리 인코딩이 없어서 둘을 섞어 쓸때는 신중해야 한다. 여기서는 파이썬 3 기준으로 한다.
27 |
28 |
29 |
30 | bytes 인스턴스에 대해 간단히 소개한다.
31 | str 문자열을 생성하는 방법은 " ㅁㄴㅇㄻㄴㅇㄹ"와 같이 따옴표 안에 문자나 숫자를 적는 것이다.
32 | 반대로 bytes는 b'\x41'과 같이 문자열 앞에 b를 붙여야 한다.
33 |
34 | 아까 bytes는 8비츠로 이루어진다고 했다. 유의미한 바이츠 인스턴스는 ascii 상수로만 이루어진다.
35 | 저 위의 b'\x41'가 보이는가? 41은 16진수의 수이고 10진수로 65다.
36 | 그리고 '\x'가 저 숫자가 16진수임을 보증한다. 저걸 디코드하면 'A'가 나올 것이다. 왜냐하면 'A'의 ASCII
37 | 코드가 65이기 때문이다.
38 |
39 | ```python
40 | >>> b'\x41'
41 |
42 | b'A'
43 | ```
44 |
45 | 그러니까 4비트 + 4비트 붙어서 8비트가 되는 것.(참고로 4비트는 nibble이라고 한다.)
46 |
47 | 나중에 파이썬 인코딩을 더 다루게 되면 자세히 소개한다.
48 |
49 | 유니코드 str과 바이너리 bytes를 호환하려면 다음과 같은 방식을 거쳐야 한다.
50 |
51 | * **'str -> bytes'는 str의 encode 메소드를 통해야 한다.**
52 | * **'bytes -> str'은 bytes의 decode 메소드를 통해야 한다.**
53 |
54 | Ubuntu를 기준으로 **인코딩, 디코딩 모두 기본 인코딩은 'utf-8'을 사용하며, 'CP949' 등 다른 인코딩을 사용하려면
55 | 메소드에 인자를 주면 된다.** 기본 인코딩은 운영체제 환경에 따라 다를 수 있다.
56 |
57 |
58 | ```python
59 | # example
60 |
61 | 'a'.encode(encoding='CP949') # 1.
62 | b'\x45'.decode('CP949') # 2.
63 | ```
64 |
65 |
66 | 경험담을 하나 풀자면, 본인은 윈도우를 사용하며 인코딩을 공부하기 위해 바탕화면에 txt 파일을 놓고 쓰고 지우고를 많이 반복했다.
67 |
68 | ```python
69 | fp = open('example.txt', 'r', encoding='utf-8').read()
70 | ```
71 |
72 |
73 | 위 코드의 결과가 무엇이 나왔을까? _UnicodeDecodeError_ 가 발생했다. 윈도우는 기본적으로 CP949 인코딩을 사용한다.
74 | 그런데 그것을 utf-8 인코딩을 사용해 열었으니 안 열리는 것이다. 각 인코딩 방법마다 글자를 표현하는 방법이 다르다. 위와 같은 일을 막기 위해서는 인코딩을 알아야 한다.
75 |
76 | 내가 처음에 3장을 보고 충격을 먹었는데 왜냐하면 bytes의 존재 자체를 몰랐기에 그들의 차이점을 알자는 말에 벙찔 수밖에 없었다. 그래서 나와 같은 사람들을 위해서 bytes에 대해 먼저 소개하는 내용을 적었다. 두서 없이 급하게 적었는데 help(bytes)를 정독해보고, 질문이 생기면 연락하자.
77 |
78 |
79 |
80 | ## 2. 생각해볼 문제.
81 |
82 | * `codecs` module에 대해 알아보자.
83 | * '가'를 utf-8로 인코딩해보자. 아마 'A'의 b'\x41' 보다 훨씬 길텐데 왜 그럴까?
84 |
--------------------------------------------------------------------------------
/files/BetterWay04_UseHelpFunction.md:
--------------------------------------------------------------------------------
1 | # Better way 04. 복잡한 표현식 대신 헬퍼 함수를 작성하자
2 |
3 | #### 24쪽
4 |
5 | * Created : 2016/09/18
6 | * Modified: 2019/05/03
7 |
8 |
9 | ## 1. 예제 살펴보기
10 |
11 | 파이썬의 간결한 문법을 사용하면 많은 로직을 표현식 한 줄로 쉽게 작성할 수 있다.
12 | 예를 들어, URL에서 쿼리 문자열을 디코드해야 한다고 생각해보자. 다음 예에서 각 쿼리 문자열 파라미터는 정수 값이다.
13 |
14 | ```python
15 | from urllib.parse import parse_qs
16 | # urllib.parse는 python 3의 모듈이다. 2의 경우엔 urlparse 모듈을 사용하자.
17 |
18 |
19 | my_values = parse_qs('red=5&blue=0&green=',keep_blank_values=True)
20 | # parse_qs에서 'qs'는 'query string'의 약자인듯.
21 | ```
22 |
23 | _parse_qs()_ 의 인자에 따라 특정 키에 대한 값이 여러 개 존재할 수도 있고, 한 개만 있을 수도 있으며, 값이 비어있을 수도 있다. 딕셔너리에 get 메서드를 사용하면 각 상황에 따라 다른 값을 반환할 것이다.
24 |
25 | ```python
26 | print('Red ',my_values.get('red'))
27 | print('Green ',my_values.get('green'))
28 | print('Opacity ',my_values.get('opacity'))
29 |
30 | Red ['5']
31 | Green ['']
32 | Opacity None
33 | ```
34 |
35 |
36 | _my\_values_ 라는 딕셔너리에는 'opacity'라는 키가 없다. dict.get 메소드는 없는 키를 요구받았을 때 None을 반환한다.
37 | **'green'처럼 값이 비어 있거나, 'opacity'처럼 None이 반환된다면 기본적으로 0이 할당된다면 좋을 것이다.**
38 | 이 작업을 불(bool)로 처리할 수도 있다.
39 |
40 | 파이썬의 문법 덕분에 불 표현식으로도 아주 쉽게 처리할 수 있다. 이때 사용하는 트릭은 **파이썬의 조건문에서는 빈 문자열, 빈 리스트, 0이 모두 False로 처리된다는 점이다.**
41 |
42 | 따라서 다음 표현식들의 결과는 첫 번째 서브 표현식이 False일 때 'or' 연산자 뒤에 오는 서브 표현식을 평가한 값이 된다.
43 |
44 |
45 | ```python
46 | red = my_values.get('red', [''])[0] or 0
47 | green = my_values.get('green', [''])[0] or 0
48 | opacity = my_values.get('opacity', [''])[0] or 0
49 |
50 | print('Red : %r' % red)
51 | print('Green : %r' % green)
52 | print('Opacity : %r' % opacity)
53 |
54 | Red : '5'
55 | Green : 0
56 | Opacity : 0
57 | ```
58 |
59 |
60 |
61 | 'red'의 경우는 키가 _my\_values_ 안에 있다. 값은 '5'만 있는 리스트이다. 암시적으로 True가 되므로 'red'는 or 표현식의 첫 번째 부분을 할당 받는다.
62 |
63 | 'green'의 경우는 키는 있으나 값이 존재하지 않는다. 빈 문자열은 암시적으로 False이므로 or 표현식의 결과는 0이 된다.
64 |
65 | 'opacity'의 경우 키 자체가 존재하지 않는다. get 메서드는 딕셔너리에 키가 없으면 두 번째 인수를 반환한다. 위의 코드에서 'green'과 'opacitiy'가 같은 동작을 한다.
66 |
67 |
68 |
69 | ## 2. 위 코드의 문제점
70 |
71 | 이 표현식은 읽기가 어려울 뿐만 아니라 여전히 필요한 작업을 다 수행하지도 않는다. 위에서 숫자 5는 '5', 즉 문자 5로 표현되었는데 이를 정수화해야 할 것이다.
72 |
73 | ```python
74 | red = int(my_values.get('red', [''])[0] or 0)
75 | ```
76 |
77 | 위 코드는 읽기 무척 어렵다. 시각적 방해 요소가 너무 많다. 코드를 처음 보는 사람은 식을 이해하기 위해 각 부분을 떼어내서 이해하느라 시간을 많이 뺏길 것이다.
78 |
79 | **코드가 한 줄로 짧기는 하지만, 정말 중요한 건 가독성이다.** 한 줄에 모두 집어넣는 건 의미가 없다.
80 |
81 |
82 |
83 |
84 | ### 2.1. 해결책 1: if / else로 좀 더 쉽게 시각화하기
85 |
86 | **파이썬 2.5에 추가된 if/else 조건식(삼항 조건식)을 활용하면 코드를 짧게 하면서도 더 명확하게 표현할 수 있다.**
87 |
88 |
89 | ```python
90 | red = my_values.get('red',[''])
91 | red = int(red[0]) if red[0] else 0
92 | ```
93 |
94 | 이 코드가 한 줄 더 늘어나긴 했지만 훨씬 낫다. 덜 복잡한 상황에서는 if / else 조건식을 쓰면 코드를 명확하게 이해할 수 있다. 식을 더 펼쳐 모든 로직을 길게 보면 더 직관적이다.
95 |
96 | ```python
97 | green = my_values.get('green', [''])
98 | if green[0]:
99 | green = int(green[0])
100 | else:
101 | green = 0
102 | ```
103 |
104 |
105 | ### 2.1. 해결책 2: 헬퍼 함수 사용하기
106 |
107 | #### 헬퍼 함수의 필요성
108 |
109 | 이 로직을 반복해서 사용해야 한다면 헬퍼 함수를 만드는 게 좋다.
110 |
111 |
112 | ```python
113 | def get_first_int(values, key, default=0):
114 | found = values.get(key, [''])
115 | if found[0]:
116 | found = int(found[0])
117 | else:
118 | found = default
119 | return found
120 | ```
121 |
122 | 위의 헬퍼함수를 쓰면 or를 사용한 복잡한 표현식이나 if/else 조건식을 사용한 두 줄짜리 버젼을 쓸 때보다
123 | 호출 코드가 더 명확해진다.
124 |
125 | ```python
126 | green = get_first_int(my_values, 'green')
127 | ```
128 |
129 |
130 | **표현식이 복잡해지기 시작하면 최대한 빨리 해당 표현식을 작은 조각으로 분할하고, 로직을 헬퍼 함수로 옮기는 방안을 고려해야 한다.** 무조건 짧은 코드를 만들기보다는 가독성을 선택하는 편이 낫다. 이렇게 **이해하기 어려운 복잡한 표현식에는 파이썬의 함축적인 문법을 사용하면 안 된다.**
131 |
132 |
133 |
134 |
135 | ## 3. 핵심 정리
136 |
137 | * 파이썬의 문법을 이용하면 한 줄짜리 표현식을 쉽게 작성할 수 있지만 코드가 복잡해지고 읽기 어려워진다.
138 | * 복잡한 표현식은 헬퍼 함수로 옮기는 게 좋다. 특히, 같은 로직을 반복해서 사용해야 한다면 헬퍼 함수를 사용하자.
139 | * if / else 표현식을 이용하면 or나 and 같은 불 연산자를 사용할 때보다 읽기 수월한 코드를 작성할 수 있다.
140 |
--------------------------------------------------------------------------------
/files/BetterWay05_HowToSequenceSlice.md:
--------------------------------------------------------------------------------
1 | # Better way 05. 시퀀스를 슬라이스하는 방법을 알자
2 |
3 | #### 28쪽
4 |
5 | * Created : 2016/12/25
6 | * Modified: 2019/05/03
7 |
8 |
9 | ## 1. Slice tricks
10 |
11 | **파이썬은 다른 프로그래밍 언어와 마찬가지로 리스트 등의 시퀀스를 슬라이스해서 조각으로 만드는 문법을 제공한다. 이를 통해 시퀀스의 부분집합을 쉽게 구할 수 있다.**
12 |
13 | 간단한 시퀀스의 예로는 list, str, bytes가 있을 것이다. 추가로 \_\_getitem\_\_과 \_\_setitem\_\_이라는 특별한 메소드를 구현하는 다른 파이썬의 클래스에도 슬라이싱을 적용할 수 있다.
14 |
15 | 슬라이스의 기본 형태는 somelist[start:end]이다. 다른 언어와 같이 start는 포함되고 end는 제외된다.([start, end))
16 |
17 | ```python
18 | a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',]
19 |
20 | print('First four', a[:4]) # 1.
21 | print('Last four', a[:-4]) # 2.
22 | print('Middle two', a[3:-3]) # 3.
23 |
24 |
25 | ['a', 'b', 'c', 'd']
26 | ['e', 'f', 'g', 'h']
27 | ['d', 'e']
28 | ```
29 |
30 | 1. 첫 원소 4개를 추출한다. 리스트의 처음부터 슬라이스할 때는 보기 편하게 인덱스 0을 생략한다. 리스트의 끝까지 슬라이스 할 때도 마지막 인덱스는 넣지 않아도 된다.
31 | 2. 마지막 원소 4개를 추출한다. 리스트의 끝을 기준으로 계산할 때는 음수로 슬라이스하는 게 편하다.
32 | 3. 3번째 원소에서 끝에서 3번째 원소까지 추출한다.
33 |
34 |
35 | ```python
36 | a[:] # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
37 | a[:5] # ['a', 'b', 'c', 'd', 'e']
38 | a[:-1] # ['a', 'b', 'c', 'd', 'e', 'f', 'g']
39 | a[4:] # ['e', 'f', 'g', 'h']
40 | a[-3:] # ['f', 'g', 'h']
41 | a[2:5] # ['c', 'd', 'e']
42 | a[2:-1] # ['c', 'd', 'e', 'f', 'g']
43 | a[-3:-1] # ['f', 'g']
44 |
45 | ```
46 |
47 | **슬라이싱은 start와 end 인덱스가 리스트의 경계를 벗어나도 적절하게 처리한다.**(에러가 봔환되지 않고 대신 빈값이 나온다.)
48 | 덕분에 입력 시퀀스에 대응해 처리할 최대 길이를 코드로 쉽게 설정할 수 있다.
49 |
50 | ```python
51 | first_twenty = a[20:]
52 | last_twenty = a[:20]
53 |
54 | print(first_twenty)
55 | print(last_twenty)
56 |
57 | []
58 | ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
59 | ```
60 |
61 | start와 end가 배열의 크기를 훨씬 추월했지만 에러가 발생하지 않았다. 하지만 **Sequence의 길이를 넘어서는 인덱스를 직접 접근하면 에러가 발생한다.**
62 |
63 | ```python
64 | a[20] # Error!!
65 | ```
66 |
67 |
68 | **슬라이싱의 결과는 완전히 새로운 리스트이다.** 원본 리스트에 들어 있는 객체에 대한 참조는 유지되지만, 슬라이스 결과를 수정해도 원본에는 영향을 미치지 않는다.
69 |
70 |
71 | ```python
72 |
73 | b = a[:4]
74 | print('Before: ', b) # ['e', 'f', 'g', 'h']
75 | b[1] = 99
76 | print('After : ', b) # ['e', 99 , 'g', 'h']
77 | print('No Change:', a) # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
78 | ```
79 |
80 | _a_ 를 슬라이스한 _b_ 에 변화를 가해도 _a_ 는 변하지 않았다.
81 |
82 |
83 |
84 |
85 | 슬라이스를 할당에 사용하면(좌변에 두면) 원본 리스트에서 지정한 범위를 대체한다. 슬라이스 할당의 길이는 달라도 되며, 할당받은 슬라이스의 앞뒤 값은 유지된다. 리스트는 새로 들어온 값에 맞춰 늘어나거나 줄어든다.
86 |
87 | ```python
88 | print('Before : ', a) # ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
89 | a[2:7] = [99, 24, 14]
90 | print('After : ', a) # ['a', 'b', 99, 22, 14, 'h']
91 | ```
92 |
93 |
94 |
95 | **시작과 끝 인덱스를 모두 생략하고 슬라이스하면 원본의 복사본을 얻는다.**
96 |
97 | ```python
98 | b = a[:]
99 | assert b == a and b is not a
100 | # b와 a가 내용이 같지만 서로 다른 객체임을 확인하는 assert문이다.
101 | ```
102 |
103 | 슬라이스에 시작과 끝 인덱스를 지정하지 않고 할당하면(새 리스트를 할당하지 않고) 슬라이스의 전체 내용을 참조 대상의 복사본으로 대체한다.
104 |
105 | ```python
106 | b = a
107 | print('Before', a) # ['a', 'b', 99, 22, 14, 'h']
108 | a[:] = [101, 102, 103]
109 | assert a is b
110 | print('After ', a) # [101, 102, 103]
111 | ```
112 |
113 |
114 | 만약 _a[:]_ 가 아닌 _a = [101, 102, 103]_ 이렇게 했다면,
115 | _a_ 가 가리키는 리스트 객체가 바뀌어 _a_ 와 _b_ 는 달라질 것이다.
116 |
117 | 반대로 _a[:]_ 를 할당에 사용하면 객체의 참조가 아닌, 객체 자체의 값이 변한다.
118 |
119 |
120 |
121 | ## 2. 핵심 정리
122 |
123 | * 너무 장황하게 하지 말자. start에 0을 쓰거나 end에 시퀀스의 길이를 설정하지 말자.
124 | * 슬라이싱은 범위를 벗어난 인덱스를 허용하므로, a[:1000000]와 같은 것도 에러가 나지 않는다.
125 | * list 슬라이스에 할당하면(왼쪽에 두면) 원본 시퀀스의 지정한 범위를 대체한다.
126 |
--------------------------------------------------------------------------------
/files/BetterWay06_Dontusestridetoomuch.md:
--------------------------------------------------------------------------------
1 | # Better way 06. 한 슬라이스에 start, end, stride를 함께 쓰지 말자
2 |
3 | #### 32쪽
4 |
5 | * Created : 2017/01/10
6 | * Modified: 2019/05/04
7 |
8 |
9 | ## 1. stride의 기본을 알자
10 |
11 | 파이썬은 기본 슬라이싱뿐만 아니라 _somelist[start : end : stride]_ 처럼 슬라이싱의 간격(stride)를 지정하는 특별한 문법도 있다.
12 |
13 | 이 문법을 사용하면 **시퀀스(Sequence)를 슬라이스할 때 매 n번째 아이템을 가져올 수 있다.** 예를 들어 stride를 쓰면 리스트에서 홀수와 짝수 인덱스를 손쉽게 그룹으로 묶을 수 있다.
14 |
15 |
16 | ```python
17 |
18 | a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
19 | evens = a[::2]
20 | odds = a[1::2]
21 |
22 | # 값이 아니라 인덱스다!
23 | print("Evens are ", evens) # Even indices are [1, 3, 5, 7, 9]
24 | print("Odds are", odds) # Odd indeices are [2, 4, 6, 8, 10]
25 | ```
26 |
27 | 문제는 stride 문법이 종종 예상치 못한 동작을 해서 버그를 만들어내기도 한다는 것이다.
28 | 예를 들어 파이썬에서 바이트 문자열을 역순으로 만드는 일반적인 방법은 스트라이트 -1로 문자열을 슬라이스하는 것이다.
29 |
30 | ```python
31 | x = b'mongoose'
32 | y = x[::-1]
33 | print(y) # b'esoognom'
34 | ```
35 |
36 |
37 | 위의 코드는 바이트 문자열이나 아스키 문자에는 잘 동작하지만, UTF-8 바이트 문자열로 인코드된 유니코드 문자에는 원하는 대로 동작하지 않는다.
38 |
39 | ```python
40 | w = '김철수'
41 | x = w.encode('utf-8')
42 | y = x[::-1]
43 |
44 | z = y.decode('utf-8')
45 | UnicodeDecodeError: 'utf-8' codec can't decode byte 0x98 in position 0: invalid start byte.
46 | ```
47 |
48 | 이 이슈는 주요 인코딩 방식인 UTF-8에 종속되는 문제로, 인코딩에 대한 이해가 부족하면 쉽게 이해할 수 없는 문제이기도 하다.
49 |
50 |
51 |
52 |
53 | ## 2. stride의 주요 문제점
54 |
55 | ```python
56 | a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
57 | print(a[::2]) # ['a', 'c', 'e', 'g']
58 | print(a[::-2]) # ['h', 'f', 'd', 'b']
59 | ```
60 |
61 | `::2`는 처음부터 시작해서 매 두 번째 아이템을 선택하라는 의미이다.
62 | `::-2`는 끝부터 시작해서 반대 반향으로 매 두 번째 아이템을 선택하라는 의미이다.
63 |
64 | 이건 뭘까?
65 |
66 | ```python
67 | print(a[2::2]) # ['c', 'e', 'g']
68 | print(a[-2::-2]) # ['g', 'e', 'c', 'a']
69 | print(a[-2:2:-2]) # ['g', 'e']
70 | print(a[2:2:-2]) # []
71 | ```
72 |
73 | 물론 고민하면 이해할 수는 있다. 문제는 슬라이싱의 stride 문법이 매우 혼란스러울 수 있다는 것이다. 대괄호 안에 숫자가 세 개나 있으면 빽빽해서 읽기 힘들다.
74 |
75 | 그래서 start, end 인덱스가 stride와 연계되어 어떤 작용을 하는지 분명하지 않다.
76 | 특히 stride가 음수이면 더더욱 그렇다.
77 |
78 | 이런 문제를 방지하려면 가급적 **start, end, stride를 함께 사용하지 말아야 한다.**
79 |
80 |
81 |
82 | Sequence를 slicing할 때, stride를 쓰고 싶다면 다음을 기억하도록 하자.
83 |
84 | * **stride를 사용해야 하면 양수 값을 사용하고, start, end를 생략하자.**
85 | * **stride를 start나 end와 함께 꼭 사용해야 한다면**
86 | 1. **stride를 적용한 결과를 변수에 할당하고**
87 | 2. **이 변수를 슬라이스한 결과를 다른 변수에 할당해서 사용하자.**
88 |
89 |
90 | ```python
91 | b = a[::2] # ['a', 'c', 'e', 'g']
92 | c = b[1:-1] # ['c', 'e']
93 | ```
94 |
95 | 슬라이싱부터 하고 스트라이딩을 하면 데이터의 얕은 복사본(shallow copy)가 추가로 생긴다. 첫 번째 연산은 결과로 나오는 슬라이스의 크기를 최대한 줄여야 한다.
96 |
97 | 프로그램에서 두 과정에 필요한 시간과 메모리가 충분하지 않다면 내장 모듈 _itertools_ 의 _islice_ 메서드를 사용해보자.
98 |
99 |
100 |
101 |
102 | ## 3. 핵심정리
103 |
104 | * 한 슬라이스에 start, end, stride를 같이 지정하면 매우 혼란스러울 수 있다.
105 | * 슬라이스에 start와 end 인덱스 없이 양수 stride 값을 사용하자. 음수 stride 값은 가능하면 피하자.
106 | * 한 슬라이스에 start, end, stride를 함께 사용하는 상황은 피하자. 꼭 필요하면 두 번 할당하거나, 내장 모듈 _itertools_ 의 _islice_ 를 사용하자.
107 |
--------------------------------------------------------------------------------
/files/BetterWay07_useListComp.md:
--------------------------------------------------------------------------------
1 | # Better way 07. _map_ 과 _filter_ 대신에 list comprehension을 사용하자
2 |
3 | #### 34쪽
4 |
5 | * Created : 2017/01/16
6 | * Modified: 2019/05/04
7 |
8 |
9 |
10 |
11 | ## 1. Introduction
12 |
13 | 파이썬에는 하나의 iterable에서 리스트를 만들어내는 간결한 문법이 있다. 이를 list comprehension(이하 "리스트 컴프리헨션")이라고 한다.
14 |
15 | 예를 들어 원래의 _list_ 에서 각 수를 제곱한 새로운 _list_ 를 계산한다고 하자. 리스트 컴프리헨션을 쓰면 리스트와, 관련된 _range_ 등의 객체에 할당과 연산을 한 번에 같이 할 수 있다.
16 |
17 |
18 | ```python
19 | a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
20 | r = range(1, 11)
21 |
22 | assert [n**2 for n in a] == [i**2 for i in r]
23 | ```
24 |
25 | 똑같은 식을 내부 함수인 map을 통해서도 할 수 있지만 복잡해보인다.
26 |
27 | ```python
28 | map_a = map(lambda x: x**2, a)
29 | ```
30 |
31 |
32 |
33 | ## 2. Comprehension의 장점
34 |
35 |
36 | 또한 리스트 컴프리헨션은 map과 달리 리스트에 있는 아이템을 편하게 걸러낼 수도 있다. 예를 들어 제곱수가 짝수인 수만 걸러내서 제곱수를 구한다고 해보자.
37 |
38 | ```python
39 | even_squares = [i**2 for i in range(1, 11) if not i % 2]
40 |
41 | # 내장 함수 filter와 map을 사용하면 같은 결과를 얻지만 복잡해 보인다.
42 | map_even_squares = map(lambda x: x**2, filter(lambda x: not x % 2, a))
43 |
44 | assert even_squares == list(map_even_squares)
45 | ```
46 |
47 |
48 |
49 | ## 3. Comprehension의 다른 사용처
50 |
51 | 파이썬의 다른 기본 자료구조인 _dict_ 와 _set_ 에도 리스트 컴프리헨션에 해당하는 문법이 있다.
52 |
53 | ```python
54 | chile_ranks = {'ghost': 1, 'habanero': 2, 'cayene': 3}
55 | # Comprehension으로 dict 만들기
56 | ranks_dict = {value: key for key, value in chile_ranks.items()}
57 |
58 | print(ranks_dict) # {1: 'ghost', 2: 'habanero', 3: 'cayene'}
59 |
60 |
61 | # Comprehension으로 set 만들기
62 | chile_len_set = {len(name) for name in ranks_dict.values()}
63 |
64 | print(chile_len_set) # {8, 5, 6}
65 | ```
66 |
67 |
68 |
69 | ## 4. 핵심 정리
70 |
71 | * 리스트 컴프리헨션은 추가적인 lambda 표현식이 필요 없어서, 내장 함수인 _map_, _filter_ 를 사용하는 것보다 명확하다.
72 | * 리스트 컴프리헨션을 사용하면 입력 리스트에서 아이템을 간단히 건너뛸 수 있다. _map_ , _filter_ 로는 그렇지 않다.
73 | * _dict_ 와 _set_ 도 컴프리헨션 표현식을 지원한다.
74 |
--------------------------------------------------------------------------------
/files/BetterWay08_ListComprehension.md:
--------------------------------------------------------------------------------
1 | # Better Way 08. list comprehension에서 표현식을 두 개 넘게 쓰지 말자
2 |
3 | #### 36쪽
4 |
5 | * Created : 2017/01/24
6 | * Modified: 2019/05/04
7 |
8 |
9 |
10 | ## 1. List Comprehension의 중첩 허용
11 |
12 | **List comprehension(이하 "리스트 컴프리헨션")은 [기본 사용법](https://github.com/shoark7/Effective-Python/blob/master/files/BetterWay07_useListComp.md)뿐 아니라 다중 루프도 지원한다.**
13 |
14 | 예를 들어 2차원 행렬을 1차원 리스트 하나로 간략화한다고 가정하자.
15 |
16 | ```python
17 | complex_list = [[1 ,2 ,3], [4 ,5 ,6], [7 ,8 ,9]]
18 | flat_one = [x for row in complex_list for x in row]
19 | print(flat_one)
20 |
21 | >>> [1, 2, 3, 4, 5, 6, 7, 8, 9]
22 | ```
23 |
24 | *flat\_one* 을 만드는 식에서 두 개의 for 문이 쓰였는데 이 순서가 처음에는 참 헷갈린다. 개인적으로 참 힘들었다.
25 |
26 | *flat_one* 의 해석 순서는 **왼쪽부터 오른쪽** 으로 읽혀 들어간다. 이 예제에서는 간단하고 읽기 쉽고 합당한 다중 루프를 사용했다.
27 |
28 | 다중 루프의 또 다른 할당과 사용법은 입력 리스트의 레이아웃을 두 레벨로 중복해서 구성하는 것이다.
29 |
30 | 예를 들어 2차원 행렬의 각 셀에 있는 값의 제곱을 구한다고 하자. 이 표현식은 추가로 `[]` 문자를 사용하기 때문에 그리 좋아 보이진 않지만 그래도 이해하기는 쉽다.
31 |
32 |
33 | ```python
34 | squared_list = [[x**2 for x in row] for row in complex_list]
35 | print(squared_list)
36 |
37 | >>> [[1, 4, 9], [16, 25, 36], [49, 64, 81]]
38 | ```
39 |
40 | for문 중첩에 있어서 아까의 *flat\_one* 과 혼동하지 말자. 여기서는 **오른쪽에서부터 왼쪽**으로 가야한다. 순서가 아까랑 달라서 처음에는 참 이해하기 힘든데 이는 여러 번 사용해봐야 익숙해질 수 있다.
41 |
42 | 사실 리스트 컴프리헨션은 작동방식이 이상하다고 불평을 간간히 듣는다. list comprehension의 작동이 불편하다고 주장하는 사람들이 대신 _map_, _filter_ 등의 함수 지향 언어의 기능들을 쓰자고 강력히 권고하는 것을 Codewars에서 보기도 했다.
43 |
44 | 반복문을 3개 이상 중첩할 수도 있는데 그러면 리스트 컴프리헨션이 여러 줄로 구분해야 할 정도로 길어진다.
45 |
46 | ```python
47 | my_lists = [
48 | [[1,2,3], [4,5,6]],
49 | ...
50 | ...
51 | ]
52 |
53 | flat = [x for sublist1 in my_lists
54 | for sublist2 in sublist1
55 | for x in sublist2]
56 | ```
57 |
58 |
59 | 정말 고된 식이다. 이 정도 되면 리스트 컴프리헨션이 다른 방법보다 그다지 짧아보이지 않는다.
60 |
61 | 이번엔 일반 루프문으로 같은 결과를 만들어본다.
62 |
63 | ```python
64 | flat = []
65 | for sublist1 in my_list2:
66 | for sublist2 in sublist1:
67 | flat.extend(sublist2)
68 | ```
69 |
70 | 이 버전은 들여쓰기를 사용해서 리스트 컴프리헨션보다 이해하기 쉽다.
71 |
72 | **즉, 리스트 컴프리헨션이 언제나 최선은 아니라는 의미.**
73 |
74 |
75 |
76 |
77 | ## 2. 다중 if의 지원
78 |
79 | 리스트 컴프리헨션은 다중 if 조건도 지원한다. **같은 루프 레벨에 여러 조건이 있으면 암시적인 _and_ 표현이 된다.**
80 |
81 | 예를 들어, 숫자로 구성된 리스트에서 **4보다 큰 _and_ 짝수** 값만 가지고 온다면 다음 두 리스트 컴프리헨션은 동일하다.
82 |
83 | ```python
84 | a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
85 | b = [x for x in a if x > 4 if x % 2 == 0]
86 | c = [x for x in a if x > 4 and x % 2 == 0]
87 |
88 | # b와 c는 동일!
89 | ```
90 |
91 | _b_ 가 더 짧다고는 하지만 _c_ 가 훨씬 더 바람직해보인다. 리스트 컴프리헨션을 이 정도까지 공부하지 않은 사람들이 보기엔 _b_ 는 논리연산자가 없어 오히려 SyntaxError처럼 보이기도 한다.
92 |
93 | **if 조건은 루프의 각 레벨에서 for 표현식 뒤에 설정할 수 있다.** 예를 들어 주어진 행렬에서 각 행(row)의 합이 10 이상일 때 그 행의 셀 중에서 3으로 나누어 떨어지는 셀을 구해보자.
94 |
95 | 다음처럼 리스트 컴프리헨션으로 표현하면 간단하지만 이해하기는 매우 어렵다.
96 |
97 | ```python
98 | matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
99 | filtered = [[x for x in row if x % 3 == 0] for row in matrix if sum(row) >= 10]
100 |
101 | print(filtered)
102 |
103 | >>> [[6], [9]]
104 | ```
105 |
106 | 위 식에서는
107 |
108 | 1. _matrix_ 에서 행의 합이 10이 넘는 건 `[4, 5, 6]`, `[7, 8, 9]`이며,
109 | 2. 이 행들 중에서 3의 배수인 셀은 6, 9가 나온다.
110 |
111 | 난해한 예로 이런 표현식은 가급적 피하는 것이 좋다. 이런 코드는 다른 사람들이 이해하기 매우 어렵다. **몇 줄을 절약한 장점이 나중에 겪을 어려움보다 크지 않다.**
112 |
113 | **경험칙으로 볼 때, 리스트 컴프리헨션을 사용할 때는 표현식이 두 개를 넘어가면 피해야 한다.** 조건 두 개, 루프 두 개, 혹은 조건 한 개와 루프 한 개 정도면 된다.
114 |
115 | **이것보다 복잡해지면 그냥 if 문과 for문을 사용하고 헬퍼 함수를 작성해야 한다.**
116 |
117 |
118 |
119 | ## 3. 핵심 정리
120 |
121 | 1. 리스트 컴프리헨션은 다중 루프와 루프 레벨별 다중 조건을 지원한다.
122 | 2. 표현식이 두 개가 넘게 들어 있는 리스트 컴프리헨션은 이해하기 매우 어려우므로 피해야 한다.
123 |
--------------------------------------------------------------------------------
/files/BetterWay09_UseGeneratorExpression.md:
--------------------------------------------------------------------------------
1 | ## Better Way 9. 컴프리헨션이 클 때는 제너레이터 표현식을 고려하자
2 |
3 | #### 39쪽
4 |
5 | * Created : 2017/01/25
6 | * Modified: 2019/05/06
7 |
8 |
9 |
10 | ## 1. List comprehension의 문제와 대안
11 |
12 | **리스트 컴프리헨션의 문제점은 입력 시퀀스에 있는 각 값별로 아이템을 하나씩 담은 새 리스트를 통째로 생성한다는 점이다.** 입력이 적을 때는 괜찮지만 클 때는 메모리를 많이 소모해서 프로그램을 망가뜨리는 원인이 될 수도 있다.
13 |
14 | 예를 들어, 파일을 읽고 각 줄에 있는 문자의 개수를 반환한다고 하자. 이 작업을 리스트 컴프리헨션으로 하면 파일에 있는 각 줄의 길이만큼 메모리가 필요하다.
15 |
16 | 파일에 오류가 있거나 끊김이 없는 네트워크 소켓일 경우 리스트 컴프리헨션을 사용하면 문제가 발생한다.
17 |
18 | ```python
19 | # 일반 리스트 컴프리헨션
20 |
21 | value = [len(x) for x in open('/tmp/my_file.txt')] # x가 하나의 줄이 된다.
22 |
23 | >>> print(value)
24 |
25 | [100, 35, 1, 121, 43 , 12, 12]
26 | ```
27 |
28 | 파이썬은 이 문제를 해결하려고 리스트 컴프리헨션과 제너레이터를 일반화한 제너레이터 표현식(generator expression)을 제공한다. **제너레이터 표현식은 실행될 때 시퀀스를 모두 구체화(여기서는 메모리에 로딩)하지 않는다.** 대신 표현식에서 한 번에 한 아이템을 내주는 이터레이터로 평가한다.
29 |
30 | > 제너레이터는 이터레이터다. 즉 제너레이터는 이터레이터를 상속받는다.
31 |
32 | 제너레이터 표현식은 리스트 컴프리헨션 문법과 비슷한데 차이점은 `[ ]` 대신에 `( )`를 사용한다.
33 |
34 | 다음은 같은 예를 제너레이터 표현식으로 작성한 예다. 하지만 제너레이터 표현식은 즉시 이터레이터로 평가되므로 더는 진행되지 않는다.
35 |
36 | ```python
37 | it = (len(x) for x in open('/tmp/my_file.txt')
38 |
39 |
40 | >>> print(it)
41 | at 0x101b81480>
42 | ```
43 |
44 | **필요할 때 제너레이터 표현식에서 다음 출력을 생성하려면 내장 함수 **next**로 반환받은 이터레이터를 한 번에 전진시키면 된다.** 코드에서는 메모리 사용량을 걱정하지 않고 제너레이터 표현식을 사용하면 된다.
45 |
46 | ```python
47 | print(next(it))
48 | print(next(it))
49 |
50 | 100
51 | 35
52 |
53 | # 처음 식의 첫 번째, 두 번째 인자와 값이 같다!
54 | ```
55 |
56 |
57 |
58 |
59 | ## 2. Generator의 활용방안
60 |
61 | **제너레이터 표현식의 또 다른 강력한 결과는 다른 제너레이터 표현식과 함께 사용할 수 있다는 점이다.** 다음은 앞의 제너레이터 표현식이 반환한 이터레이터를 다른 제너레이터 표현식의 입력으로 사용한 예다.
62 |
63 |
64 | ```python
65 | roots = ((x, x ** 0.5) for x in it)
66 |
67 | # it은 위에서 정의한 각 라인의 줄을 세는 제너레이터 표현식!
68 | ```
69 |
70 | 이 이터레이터를 전진시킬 때마다 루프의 도미노 효과로 내부 이터레이터도 전진시키고 조건 표현식을 계산해서 입력과 출력을 처리한다.
71 |
72 | ```python
73 | print(next(roots))
74 |
75 | >>>
76 | (100, 10)
77 | ```
78 |
79 | 이처럼 제너레이터를 연결하면 파이썬에서 매우 빠르게 실행할 수 있다. 큰 입력 스트림에 동작하는 기능을 결합하는 방법을 찾을 때는 제너레이터 표현식이 최선의 도구다.
80 |
81 | 단, 제너레이터 표현식이 반환한 이터레이터는 상태가 있으므로, 다시 말해 더 이상 반환할 원소가 없으면 'StopIteration Exception'을 반환하므로 이터레이터를 한 번 넘게 사용하면 안 된다.
82 |
83 |
84 |
85 | ## 3. 핵심정리
86 |
87 | * 리스트 컴프리헨션은 큰 입력을 처리할 때 너무 많은 메모리를 소모해서 문제를 일으킬 수 있다.
88 | * 제너레이터 표현식은 이터레이터로, 한 번에 한 번만 값을 만들기 때문에 메모리 문제를 피할 수 있다.
89 | * 한 제너레이터 표현식에서 나온 이터레이터를 또 다른 제너레이터 표현식의 for 서브 표현식으로 넘기는 방식으로 제너레이터 표현식을 조합할 수 있다.
90 | * 제너레이터 표현식은 서로 연결되어 있을 때 매우 빠르게 실행된다.
91 |
--------------------------------------------------------------------------------
/files/BetterWay10_useEnumerate.md:
--------------------------------------------------------------------------------
1 | ## Better Way 10. range보다는 enumerate를 사용하자
2 |
3 | #### 41쪽
4 |
5 | * Created : 2017/01/26
6 | * Modified: 2019/05/06
7 |
8 |
9 |
10 | ## 1. enumerate
11 |
12 | 파이썬을 처음 배울 때 for문을 배우게 된다. for문에서 **range**를 수없이 썼을 것이다.
13 |
14 | ```python
15 | from random import randint
16 |
17 | random_bit = 0
18 | for i in range(64):
19 | if randint(0, 1):
20 | random_bit |= (1 << i)
21 | # 이 코드는 정말 어썸.. 하나 배웠다.
22 |
23 | print(random_bit)
24 |
25 | >>> 13250328546979216565
26 | ```
27 |
28 | 또한 range를 통해 문자열의 리스트 같은 순회할 자료 구조가 있을 때는 직접 루프를 실행할 수도 있다.
29 |
30 | ```python
31 | flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
32 |
33 | for flavor in flavor_list:
34 | print('{} is delicious'.format(flavor))
35 |
36 | >>> vanilla is delicious
37 | >>> chocolate is delicious
38 | >>> pecan is delicious
39 | >>> strawberry is delicious
40 | ```
41 |
42 |
43 |
44 | 좋다. 그런데 종종 리스트를 위와 같이 선회하는데 현재 아이템의 인덱스가 필요한 경우가 있다. 예를 들어 좋아하는 아이스크림의 순위를 출력하고 싶다고 하자.
45 |
46 | 일반적인 방법은 **range**를 쓰는 것이다.
47 |
48 | ```python
49 | for i in range(len(flavor_list)):
50 | flavor = flavor_list[i]
51 | print('{index} : {flavor}'.format(
52 | index=i + 1,
53 | flavor=flavor))
54 |
55 | >>> 1 : vanilla
56 | >>> 2 : chocolate
57 | >>> 3 : pecan
58 | >>> 4 : strawberry
59 | ```
60 |
61 | 결과는 나왔는데.. 뭔가 께름칙하다. `range(len(flavor_list))`.. 너무 안 이쁘지 않은가??
62 |
63 | 파이썬은 이런 상황을 처리하려고 내장 함수 **enumerate**를 제공한다. 이 함수는 지연 제너레이터(lazy generator)로 이터레이터를 감싼다.(lazy의 뜻은 나중에 포스트한다.)
64 |
65 | **이 제너레이터는 이터레이터에서(예를 들면 list iterator) 루프 인덱스와 다음 값을 한 쌍으로 가져와 넘겨준다.**
66 |
67 | 직접 예를 보자.
68 |
69 | ```python
70 | for i, flavor in enumerate(flavor_list):
71 | print('{index} : {flavor}'.format(
72 | index=i + 1,
73 | flavor=flavor))
74 |
75 | >>> 1 : vanilla
76 | >>> 2 : chocolate
77 | >>> 3 : pecan
78 | >>> 4 : strawberry
79 | ```
80 |
81 | 이 예제에서 enumerate은 for문 안에서 기존 리스트의 원소(flavor)와 인덱스 i를 튜플로 반환하고 있다.
82 | 인덱스와 아이템을 같이 받을 수 있어서 위와 같은 예제를 깔끔하게 해결할 수 있다.
83 |
84 |
85 |
86 | 또한 enumerate에 추가 인자를 넣어서 인덱스의 시작값을 설정할 수도 있다.
87 |
88 | ```python
89 | for i, flavor in enumerate(flavor_list, 1):
90 | print('{index}: {flavor}'.format(
91 | index=i,
92 | flavor=flavor))
93 |
94 | # 동일한 결과가 나온다.
95 | ```
96 |
97 |
98 |
99 | ### 핵심 정리
100 |
101 | * enumerate는 이터레이터를 순회화면서 이터레이터에서 각 아이템의 인덱스를 얻어오는 간결한 문법을 제공한다.
102 | * range로 루프를 실행하고 시퀀스에 인덱스로 접근하기보다는 enumerate를 사용하는 게 좋다.
103 | * enumerate에 두 번째 파라미터를 사용하면 세기 시작할 숫자를 지정할 수 있다.(기본 0)
104 |
--------------------------------------------------------------------------------
/files/BetterWay11_UseZip.md:
--------------------------------------------------------------------------------
1 | ## Better Way 11. 이터레이터를 병렬로 처리하려면 zip을 사용하자
2 |
3 | #### 43쪽
4 |
5 | * Created : 2017/01/30
6 | * Modified: 2019/05/09
7 |
8 |
9 |
10 | ## 1. zip 함수 소개
11 |
12 | 파이썬에서 관련 객체로 구성된 리스트를 많이 사용한다는 사실은 쉽게 알 수 있다. 리스트 컴프리헨션을 사용하면 소스 리스트에 표현식을 적용하여 파생 리스트(derived list)를 쉽게 얻을 수 있다.
13 |
14 | ```python
15 | names = ['Park', 'Lee', 'Kim']
16 | letters = [len(name) for name in names]
17 | ```
18 |
19 | 파생 리스트의 아이템과 소스 리스트의 아이템은 서로 인덱스로 연관되어 있다. 따라서 두 리스트를 병렬로 순회하려면 소스 리스트인 `names`의 길이만큼 순회하면 된다.
20 |
21 | ```python
22 | longest_name = None
23 | max_letters = 0
24 |
25 | for i in range(len(names)):
26 | count = letters[i]
27 | if count > max_letters:
28 | longest_name = names[i]
29 | max_letters = count
30 |
31 | print(longest_name)
32 | ```
33 |
34 | 무난한 루프이지만 별로 아름답지 않다. __*names*와 *letters*를 인덱스로 접근하면 코드를 읽기 어려워진다.__
35 |
36 | 루프의 인덱스 *i*로 배열에 접근하는 동작이 두 번 일어난다. *enumerate*를 사용하면 문제점을 약간 개선할 수 있지만 여전히 완벽하지는 않다.
37 |
38 | ```python
39 | for i, name in enumerate(names):
40 | count = letters[i]
41 | if count > max_letters:
42 | longest_name = name
43 | max_letters = count
44 | ```
45 |
46 | 파이썬은 위 코드를 좀 더 명료하게 하는 내장 함수 `zip`을 제공한다. 파이썬 3에서 *zip*은 지연 제너레이터(lazy generator)로 이터러블 두 개 이상을 감싼다.
47 |
48 | __*zip* 제너레이터를 사용한 코드는 다중 리스트에서 인덱스로 접근하는 코드보다 훨씬 명료하다.__
49 |
50 | ```python
51 | for name, count in zip(names, letters):
52 | if count > max_letters:
53 | longest_name = name
54 | max_letters = count
55 | ```
56 |
57 | 훨씬 코드가 명료하고 깔끔하다.
58 |
59 | 내장 함수 *zip*을 사용할 때의 문제점은 두 가지가 있다.
60 |
61 | 1. 파이썬 2에서 *zip*은 제너레이터가 아니다.
62 | > 제공한 이터레이터를 완전히 순회해서 생성한 모든 튜플을 반환하기 때문에
63 | > 메모리를 많이 사용해서 프로그램이 망가질 수 있다.
64 | > 파이썬2 에서 매우 큰 이터레이터를 *zip*으로 묶어서 사용하려고 한다면 내장 모듈 *itertools*의 *izip*을 사용해야 한다.
65 |
66 | 2. 입력 이터러블들의 길이가 서로 다르다면 *zip*이 이상하게 동작한다.
67 | > 예를 들어 *names* 리스트에만 다른 이름을 추가하고 *letters*는 그대로 둬서 두 리스트의 길이가 다르게 하자.
68 |
69 | ```python
70 | names.append('Choi')
71 |
72 | for name, count in zip(names, letters):
73 | print(name)
74 |
75 | >>> Park
76 | >>> Lee
77 | >>> Kim
78 | ```
79 |
80 | 새 아이템 'Choi'가 없다. __*zip*은 이렇게 이터러블들의 길이가 다르면 가장 짧은 이터러블까지만 적용된다.__
81 |
82 | 그래서 만약 *zip*을 적용할 이터러블들의 길이가 같음을 확신할 수 없다면, 내장 모듈 *itertools*의 *zip\_longest*를 사용하는 방안도 생각할 수 있다.
83 |
84 |
85 |
86 | ## 2. 부록
87 |
88 | *zip*을 재밌게 써볼 수도 있다.
89 |
90 | 예를 들어 어떤 알고리즘 문제를 푸는데 원소의 다음 원소와 값을 비교해야 할 일이 있다고 해보자.
91 |
92 | ```python
93 | from random import randint
94 |
95 | k = [randint(1, 10) for _ in range(10)]
96 | print(k)
97 |
98 | # 바로 뒤 원소보다 큰 경우만 출력하자.
99 |
100 | for i, n in enumerate(k[:-1]):
101 | if k[i] > k[i + 1]:
102 | print(k[i])
103 |
104 | >>> [8, 9, 2, 9, 6, 9, 5, 10, 6, 1]
105 | >>> 9
106 | >>> 9
107 | >>> 9
108 | >>> 10
109 | >>> 6
110 | ```
111 |
112 | 무난무난하고 평범한 계산이다. 그런데 이 식을 *zip*을 사용해 재치 있게 사용해볼 수도 있다.
113 |
114 | ```python
115 | from random import randint
116 |
117 | k = [randint(1, 10) for _ in range(10)]
118 | print(k)
119 |
120 |
121 | for n1, n2 in zip(k[:-1], k[1:]):
122 | if n1 > n2:
123 | print(n1)
124 |
125 | >>> [10, 9, 10, 7, 9, 7, 3, 5, 5, 5]
126 | >>> 10
127 | >>> 10
128 | >>> 9
129 | >>> 7
130 | ```
131 |
132 | 정말 어썸하지 않을 수 없다. 이런 활용도 있음을 참고하자.
133 |
134 |
135 |
136 |
137 | ## 3. 핵심 정리
138 |
139 | * 내장 함수 *zip*은 여러 이터러블을 병렬로 순회할 때 사용할 수 있다.
140 | * 파이썬 3의 *zip*은 튜플을 생성하는 지연 제너레이터이다. 파이썬 2에서는 전체 결과를 튜플 리스트로 반환한다.
141 | * 길이가 다른 이터레이터를 사용하면 *zip*은 더 긴 이터러블의 결과를 조용히 잘라낸다.
142 | * 내장 모듈 *itertools*의 *zip\_longest*를 사용하면 이터레이터를 길이에 상관없이 병렬로 순회할 수 있다.
143 |
--------------------------------------------------------------------------------
/files/BetterWay12_dontuse_else.md:
--------------------------------------------------------------------------------
1 | ## Better Way 12. for와 while 루프 뒤에는 else 블록을 쓰지 말자
2 |
3 | #### 46쪽
4 |
5 | * Created : 2017/01/31
6 | * Modified: 2019/05/09
7 |
8 |
9 |
10 | ## 1. 반복문에서의 else
11 |
12 |
13 | 파이썬의 루프에는 대부분의 다른 프로그래밍 언어에는 없는 추가적인 기능이 있다. **루프에서 반복되는 내부 블록 다음에 else 블록을 쓸 수 있는 기능이다.**
14 |
15 | ```python
16 | for i in range(3):
17 | print('Loop {}'.format(i))
18 | else:
19 | print('Else block')
20 |
21 | >>> Loop 0
22 | >>> Loop 1
23 | >>> Loop 2
24 | >>> Else block
25 | ```
26 | 놀랍게도 else 블록은 루프가 종료되자마자 실행된다. 이걸 왜 else라고 부를까??
27 |
28 | if/else문에서나, try/except 문에서 else(except)는 이전 구문(if or try)이 실행되지 않으면 이 else 블록이 실행된다는 의미이다.
29 | 이는 다른 프로그맹 언어에서도 마찬가지이다.
30 |
31 | 그렇기에 파이썬을 처음 접하는 프로그래머들은 for/else의 else 부분이 '루프가 완료되지 않으면 이 블록을 실행한다'
32 | 라고 짐작할 것이다. 실제로는 정확히 반대다.
33 |
34 | 루프에서 break문을 사용해야 else 블록을 건너뛸 수 있다.
35 |
36 | ```python
37 | for i in range(3):
38 | print('Loop {}'.format(i))
39 | if i == 1:
40 | break
41 | else:
42 | print('Else block!')
43 |
44 | >>> Loop 0
45 | >>> Loop 1
46 |
47 | # break문을 만나서 else 블록이 실행되지 않았다.
48 | ```
49 |
50 |
51 | 다른 놀랄 점은 빈 시퀀스를 처리하는 루프 문에서도 else 블록이 즉시 실행된다는 것이다.
52 |
53 | ```python
54 | for x in []:
55 | print('Never runs')
56 |
57 | else:
58 | print('Not else block!')
59 |
60 | >>> Not else block!
61 | ```
62 |
63 | 또 while 루프가 처음부터 거짓인 경우에도 실행된다.
64 |
65 | ```python
66 | while False:
67 | print('Never runs')
68 | else:
69 | print('While else block!')
70 |
71 | >>> While else block!
72 | ```
73 |
74 |
75 |
76 | 대관절 왜 이렇게 헷갈리게 만들어 놓았을까? 이렇게 동작하는 이유는 루프 다음에 오는 else 블록은 루프로 뭔가를 검색할 때 유용하기 때문이다.
77 |
78 | 예를 들어, 두 숫자가 서로소(coprime)인지를 판별한다고 해보자. 이제 가능한 모든 공약수를 구하고 숫자를 테스트해보자. 모든 옵션을 시도한 후에 루프가 끝난다. else 블록은 루프가 break을 만나지 않아서 숫자가 서로소일 때 실행된다.
79 |
80 | ```python
81 | a = 4
82 | b = 9
83 |
84 | for i in range(2, min(a, b) + 1):
85 | print('Tesing' , i)
86 | if a % i == 0 and b % i == 0:
87 | print('Not prime')
88 | break
89 | else:
90 | print('Coprime')
91 |
92 | >>> Tesing 2
93 | >>> Tesing 3
94 | >>> Tesing 4
95 | >>> Coprime
96 | ```
97 |
98 | 실제로는 이렇게 코드를 작성하면 안 된다.
99 |
100 | 대신에 이런 계산을 하는 헬퍼 함수를 작성하는 것이 좋다.
101 | 이런 헬퍼 함수는 두 가지 일반적인 스타일로 작성한다.
102 |
103 | ```python
104 | # way 1. 조건을 찾으면 바로 반환하기
105 |
106 | def coprime(a, b):
107 | for i in range(2, min(a, b) + 1):
108 | if a % i == 0 and b % i == 0:
109 | return False
110 | return True
111 | ```
112 |
113 |
114 | ```python
115 | # way 2. 루프에서 찾으려는 대상을 찾았는지 알려주는 결과 변수를 사용하기
116 |
117 | def coprime2(a, b):
118 | is_coprime = True
119 | for i in range(2, min(a, b) + 1):
120 | if a % i == 0 and b % i == 0:
121 | is_coprime = False
122 | break
123 | return is_coprime
124 | ```
125 |
126 | 이 일반적인 두 가지 방법을 사용하면 낯선 코드를 접하는 개발자들이 코드를 훨씬 쉽게 이해할 수 있다.
127 |
128 | else 블록을 사용한 표현의 장점이 나중에 여러분 자신을 비롯해 코드를 이해하려는 사람들이 받을 부담감보다 크지 않다.
129 | 루프처럼 간단한 구조는 파이썬에서 따로 설명할 필요가 없어야 한다. 그러므로 루프 다음에 오는 else 블록은 사용하지 말자.
130 |
131 |
132 |
133 | ## 2. 핵심 정리
134 |
135 | * 파이썬에는 for와 while 루프의 내부 블록 바로 뒤에 else 블록을 사용할 수 있게 하는 특별한 문법이 있다.
136 | * 루프 본문이 break문을 만나지 않은 경우에만 루프 다음에 오는 else 블록이 실행된다.
137 | * 루프 뒤에 else 블록을 사용하면 직관적이지 않고 혼동하기 쉬우니 사용하지 말아야 한다.
138 |
--------------------------------------------------------------------------------
/files/BetterWay13_use_tryetc.md:
--------------------------------------------------------------------------------
1 | ## Better Way 13. try / except / else / finally에서 각 블록의 장점을 이용하자
2 |
3 | #### 50쪽
4 |
5 | * Created : 2017/02/01
6 | * Modified: 2019/05/10
7 |
8 |
9 |
10 | **파이썬에는 예외 처리 과정에서 동작을 넣을 수 있는 네 번의 구분되는 시점이 있다. try, except, else, finally 블록 기능으로 각 시점을 처리한다.** 각 블록은 복합문에서 독자적인 목적이 있으며, 이 블록들을 다양하게 조합하면 유용하다.
11 |
12 | ## 1. finally 블록
13 |
14 | **예외를 전달하고 싶지만, 예외가 발생해도 정리 코드를 실행하고 싶을 때 try / finally를 사용하면 된다.**
15 |
16 | try / finally의 일반적인 사용예 중 하나는 파일 핸들러를 제대로 종료하는 작업이다.
17 |
18 | ```python
19 | handle = open('tmp/random_data.txt') # IOError 가 발생할 수 있음
20 | try:
21 | data = handle.read() # UnicodeDecodeError가 일어날 수 있음
22 | finally:
23 | handle.close() # try 이후 항상 실행됨
24 | ```
25 |
26 | read에서 발생한 예외는 항상 호출 코드까지 전달되며, handle의 close 메서드 또한 finally 블록에서 실행되는 것이 보장된다.
27 |
28 | 파일이 없을 때 일어나는 IOError처럼, 파일을 열 때 일어나는 예외는 finally 블록에서 처리하지 않아야 하므로, try 블록 앞에서 open을 호출해야 한다.
29 |
30 |
31 |
32 | ## 2. else 블록
33 |
34 |
35 | **코드에서 어떤 예외를 처리하고, 어떤 예외를 전달할지를 명확하게 하려면 try/except/else를 사용해야 한다.**
36 |
37 | try 블록이 예외를 일으키지 않으면 else 블록이 실행된다. else 블록을 사용하면 try 블록의 코드를 최소로 줄이고 가독성을 높일 수 있다.
38 |
39 | 예를 들어 문자열에서 JSON 딕셔너리 데이터를 로드하여 그 안에 든 키의 값을 반환한다고 하자.
40 |
41 | ```python
42 | import json
43 |
44 | def load_json_key(data, key):
45 | try:
46 | result_dict = json.loads(data) # ValueError가 일어날 수 있음
47 | except ValueError as e:
48 | raise KeyError from e
49 | else:
50 | return result_dict[key] # KeyError가 일어날 수 있음
51 | ```
52 |
53 | 데이터가 올바른 JSON이 아니라면 json.loads로 디코드할 때 ValueError가 일어난다. 이 예외는 except에서 발견되어 처리된다. 디코딩이 성공하면 else 블록에서 키를 찾는다. 키를 찾을 때 어떤 예외가 일어나면 그 예외는 try 블록 밖에 있으므로 호출코드까지 전달된다.
54 |
55 | else 절은 try / except 그 다음에 나오는 처리를 시각적으로 except 블록과 구분해준다.
56 | 그래서 예외 전달 행위를 명확하게 한다.
57 |
58 |
59 |
60 |
61 | ## 3. 모두 함께 사용하기
62 |
63 |
64 | 복합문 하나로 모든 것을 처리하고 싶다면 try / except / else / finally를 사용하면 된다. 예를 들어 파일에서 수행할 작업 설명을 읽고 처리한 후 즉석에서 파일을 업데이트한다고 하자.
65 |
66 | **try** 블록은 파일을 읽고 처리하는 데 사용한다.
67 | **except** 블록은 try 블록에서 일어난 예외를 처리하는 데 사용한다.
68 | **else** 블록은 파일을 즉석에서 업데이트하고 이와 관련한 예외가 전달되게 하는 데 사용한다.
69 | **finally** 블록은 파일 핸들을 정리하는 데 사용한다.
70 |
71 | ```python
72 | import json
73 |
74 | UNDEFINED = object()
75 |
76 | def divide_json(path):
77 | handle = open(path, 'r+') # IOError가 일어날 수 있음
78 | try:
79 | data = handle.read() # UnicodeDecodeError가 일어날 수 있음
80 | op = json.loads(data) # ValueError가 일어날 수 있음
81 | value = (
82 | op['numerator'] /
83 | op['denominator']) # ZeroDivisionError가 일어날 수 있음
84 | except ZeroDivisonError as e:
85 | return UNDEFINED
86 | else:
87 | op['result'] = value
88 | result = json.dumps(op)
89 | handle.seek(0)
90 | handle.write(result) # IOError가 일어날 수 있음
91 | return value
92 | finally:
93 | handle.close() # 언제나 실행됨. 파일 핸들러 종료
94 | ```
95 |
96 | 이 레이아웃은 모든 블록이 직관적인 방식으로 엮여서 동작하므로 특히 유용하다. 예를 들어 결과 데이터를 재작성하는 동안에 else 블록에서 예외가 일어나도 finally 블록은 여전히 실행되어 파일 핸들을 닫는다.
97 |
98 |
99 |
100 | ## 4. 핵심 정리
101 |
102 | * try / finally 복합문을 이용하면 try 블록에서 예외 발생 여부와 상관없이 정리코드를 실행할 수 있다.
103 | * else 블록은 try 블록에 있는 코드의 양을 최소로 줄이는 데 도움을 주며, try / except 블록과 성공한 경우에 실행할 코드를 시각적으로 구분해준다,
104 | * else 블록은 try 블록의 코드가 성공적으로 실행된 후 finally 블록에서 공통 정리코드를 실행하기 전에 추가 작업을 하는 데 사용할 수 있다.
105 |
--------------------------------------------------------------------------------
/files/BetterWay14_useexception.md:
--------------------------------------------------------------------------------
1 | ## Better Way 14. _None_ 을 반환하기보다는 예외를 일으키자
2 |
3 | #### 54쪽
4 |
5 | * Created : 2017/02/02
6 | * Modified: 2019/05/10
7 |
8 |
9 |
10 |
11 | ## 1. 함수에서 _None_ 을 반환할 때의 문제점.
12 |
13 | 파이썬 프로그래머들은 유틸리티 함수를 작성할 때 반환 값 _None_ 에 특별한 의미를 부여하는 경향이 있다. 어떤 경우에는 일리 있어 보인다. 예를 들어 어떤 숫자를 다른 숫자로 나누는 헬퍼 함수를 생각해보자. 0으로 나누는 경우에는 결과가 정의되어 있지 않기 때문에 _None_ 을 반환하는 게 자연스러워 보인다.
14 |
15 | ```python
16 | def divide(a, b):
17 | try:
18 | return a / b
19 | except ZeroDivisionError:
20 | return None
21 |
22 | # 이 함수는 다음과 같이 활용해볼 수 있다.
23 | result = divide(x, y)
24 | if result is None:
25 | print('Invalid inputs')
26 | ```
27 |
28 | 위와 같이 활용한다면 큰 문제는 없어 보인다.
29 | 그런데 같은 활용식을 다음과 같이 써보면 어떨까?
30 |
31 | ```python
32 | result = divide(x, y)
33 | if not result:
34 | print('Invalid inputs')
35 | ```
36 |
37 | `if not` 같은 식도 파이썬에서는 자주 사용되는 조건식이다. 이 함수를 사용하는 프로그래머들이 충분히 사용하리라 예상해볼 수 있다.
38 |
39 | 그렇지만 이 식은 문제가 있는데, 만약 분자가 0이라면, _result_ 는 0이 되고 그러면 조건식에서는 거짓으로 판단된다는 것이다. 파이썬에서는 **_None_, 빈 문자열, 빈 리스트, 0 등이 모두 조건식에서 거짓으로 판단된다.** 결국 **나눈 값이 단순히 0인데도 결과가 0으로 나눴을 때의 예외처리를 한 것처럼 작동할 수 있다.** 이렇기 때문에 함수에서 _None_ 을 반환하면 오류가 일어나기 쉽다. 이런 오류가 일어나는 상황을 줄이는 방법은 크게 두 가지가 있다.
40 |
41 |
42 |
43 |
44 | ## 2. 해결 방법
45 |
46 | ### 2.1. 반환 값을 두개로 나눠서 튜플에 담는다.
47 |
48 | 튜플의 첫 번째 부분은 작업이 성공했는지 실패했는지를 알려준다. 두 번째 부분은 계산된 실제 결과다.
49 |
50 | ```python
51 | def divide(a, b):
52 | try:
53 | return True, a / b # 튜플로 결과를 반환
54 | except ZeroDivisionError:
55 | return False, None # 튜플로 결과를 반환
56 |
57 | success, result = divide(x, y)
58 | if not success:
59 | print('Invalid inputs')
60 | ```
61 |
62 | 이 함수를 호출하는 쪽에서는 튜플을 풀어야 한다. 따라서 나눗셈의 결과만 얻을 게 아니라 튜플에 들어 있는 상태(_success_) 부분까지 고려해야 한다.
63 |
64 | 이 방법의 문제는 호출자가 파이썬에서 사용하지 않을 변수에 붙이는 관례인 밑줄 변수를 사용해서, 튜플의 첫 번째 부분을 쉽게 무시할 수 있다는 점이다.
65 |
66 | 얼핏 보면 이 작성법이 잘못된 것 같지 않지만 결과는 그냥 _None_ 을 반환하는 것만큼 나쁘다.
67 |
68 | ```python
69 | _, result = divide(x, y)
70 | if not result:
71 | print('Invalid results')
72 | ```
73 |
74 | 언제나 내가 만든 코드, 유탈리티 함수 등은 다른 사람들이 쓸 수도 있음을 염두에 둬야 한다.
75 |
76 |
77 |
78 | ### 2.2. 결코 _None_ 을 반환하지 않는다.
79 |
80 | 대신 **호출하는 쪽에 예외를 일으켜서 호출하는 쪽에서 그 예외를 처리하게 한다.** 여기서는 호출하는 쪽에 입력값이 잘못됐음을 알리려고 _ZeroDivisonError_ 을 _ValueError_ 로 변경했다.
81 |
82 | ```python
83 | def divide(a, b):
84 | try:
85 | return a / b
86 | except ZeroDivisionError as e:
87 | raise ValueError('Invalid inputs') from e
88 | ```
89 |
90 | 이제 호출하는 쪽에서는 잘못된 입력에 대한 예외를 처리해야 한다. 호출하는 쪽에서는 더는 함수의 반환 값을 조건식으로 검사할 필요가 없다. 함수가 예외를 일으키지 않았다면 반환 값은 문제가 없다. 예외를 처리하는 코드는 깔끔해진다.
91 |
92 | ```python
93 | x, y = 5, 2
94 |
95 | try:
96 | result = divide(x, y)
97 | except ValueError:
98 | print('Invalid inputs')
99 | else:
100 | print('Result is {:.2f}'.format(result))
101 |
102 | >>> Result is 2.50
103 | ```
104 |
105 |
106 |
107 | ## 3. 부록
108 |
109 | 위와 같은 식의 장점이 무엇일지 더 고민해보았다. 저 _divide_ 함수는 _ZeroDivisionError_ 말고도 다른 에러도 출력할 수 있는데, 가령 `5 / '2'`같이 나누면 _TypeError_ 가 발생할 것이다.
110 |
111 | _divide_ 함수에서 만약 _TypeError_ 까지 잡아서 같이 _ValueError_ 로 출력한다면, **호출하는 쪽(나일 수도, 다른 사람일 수도 있다)은 오류 메시지가 서로 다른 _ValueError_ 하나만 처리하면 된다.** 편의성이 증대되는 것.
112 |
113 |
114 |
115 | ## 4. 핵심 정리
116 |
117 | * 특별한 의미를 나타내기 위해 _None_ 을 반환하는 함수가 오류를 일으키기 쉬운 이유는 _None_ 이나 다른 값(예를 들어 0)이 조건식에서 _False_ 로 평가되기 때문이다.
118 | * 특별한 상황을 알릴 때 _None_ 을 반환하는 대신에 예외를 일으키자. 문서화가 되어 있다면 호출하는 코드에서 예외를 적절하게 처리할 것이라고 기대할 수 있다.
119 |
--------------------------------------------------------------------------------
/files/BetterWay15_useClosure.md:
--------------------------------------------------------------------------------
1 | ## Better Way 15. 클로저가 변수 스코프와 상호 작용하는 방법을 알자
2 |
3 | #### 57쪽
4 |
5 | * Created : 2017/02/03
6 | * Modified: 2019/05/11
7 |
8 |
9 |
10 | ## 1. 까다로운 Closure & Scope
11 |
12 | 숫자 리스트를 정렬할 때 특정 그룹의 숫자들이 먼저 오도록 우선순위를 매기려고 한다. 이런 패턴은 사용자 인터페이스를 표현하거나, 다른 것보다 중요한 메시지나 예외 이벤트를 먼저 보여줘야 할 때 유용하다.
13 |
14 | 이렇게 만드는 일반적인 방법을 리스트이 _sort_ 메서드에 헬퍼 함수를 _key_ 인수로 넘기는 것이다.
15 |
16 | 헬퍼의 반환 값은 리스트에 있는 각 아이템을 정렬하는 값으로 사용된다. 헬퍼는 주어진 아이템이 중요한 그룹에 있는지 확인하고 그에 따라 정렬키를 다르게 할 수 있다.
17 |
18 | ```python
19 | def sort_priority(values, group):
20 | def helper(x):
21 | if x in group: # 1. 클로저
22 | return (0, x) # 2. 튜플 비교 규칙
23 | return (1, x)
24 | values.sort(key=helper) # 3. 일급 객체
25 |
26 | numbers = [8, 3, 1, 2, 5, 4, 7, 6]
27 | group = {2, 3, 5, 7}
28 | sort_priority(numbers, group)
29 | print(numbers)
30 |
31 | >>> [2, 3, 5, 7, 1, 4, 6, 8]
32 | ```
33 |
34 | 개인적으로 이 함수는 너무 어썸해서 모두에게 공유했으면 하는 코드다. _group_ 에 속하는 숫자들이 우선적으로 정렬되었다. 이 코드가 동작하는 이유는 크게 세 가지가 있다.
35 |
36 | 1. 파이썬은 클로저를 지원한다.
37 | > **클로저(closure)란 자신이 정의된 스코프에 있는 변수를 참조하는 함수다.**
38 | >바로 이 점 때문에 _helper_ 함수가 *sort_priority* 스코프에 있는 _group_인수에 접근할 수 있다.
39 |
40 | 2. 함수는 파이썬에서 **일급 객체**(first-class object)이다.
41 | > 함수를 직접 참조하고, 변수에 할당하고, 다른 함수의 인수로 전달하고, 표현식과 if 문 등에서 비교할 수 있다.
42 | > 파이썬에서는 당연하다고 생각했는데 되지 않는 언어도 있다는 뜻.
43 | > **따라서 _sort_ 메서드에서 클로저 함수를 _key_ 인수로 받을 수 있었다.**
44 |
45 | 3. 파이썬에는 이터러블의 대소관계를 비교하는 특정한 규칙이 있다.
46 | > 먼저 인덱스 0의 아이템을 비교하고 같다면 1의 아이템, ... 과 같이 비교한다.
47 | > **_sort_ 는 기본적으로 오름차순이다. 그래서 조건에 맞는 값을 0으로 보내서 1인 다른 값들보다 먼저 보이게 한 것!!**
48 |
49 |
50 |
51 | 함수에서 우선순위가 높은 아이템을 발견했는지 여부를 반환해서 사용자 인터페이스 코드가 그에 따라 동작하게 하면 좋을 것이다.
52 | 이런 동작을 추가하는 일은 쉬워보인다.
53 |
54 | ```python
55 | def sort_priority2(numbers, group):
56 | found = False # flag를 두고 값을 찾으면 True로 전환!!
57 | def helper(x):
58 | if x in group:
59 | found = True # 찾으면 여기서!!
60 | return 0, x
61 | return 1, x
62 | numbers.sort(key=helper)
63 | return found
64 | found = sort_priority2(numbers, group)
65 | print('Found', found)
66 |
67 | >>> False # what the?
68 | ```
69 |
70 | _numbers_ 나 _group_ 코드는 전혀 바뀌지 않았는데 _found_ 값이 True가 아니다. 정상적으로 값을 찾아서 flag가 뒤집혔어야 하는데 말이다. 왜 이런 일이??
71 |
72 | 구체적인 답안을 알기 전에 파이썬의 **scope**(범위)를 생각해봐야 한다. 파이썬에서는 다양한 구역에서의 변수, 함수명들이 뒤섞여 namespace를 오염시키지 않게 scope을 둬서 각 변수, 함수들이 영향을 미치는 공간을 한정하고 있다.
73 |
74 | 예를 들어 내가 리스트의 최대값을 구하는 함수를 만들었고 그 최대값을 _max_ 라는 변수에 담았다고 치자. 그런데 **이미 파이썬에는 _max_ 라는 내장 함수가 있고 내가 정한 _max_ 라는 이름과 정확히 겹친다.** 값이 완전히 치환되면 차후 _max_ 라는 함수를 영영 쓸 수 없을 것이다.
75 |
76 | 그것을 막기 위해 내가 만든 _max_ 변수는 함수 안에서, 함수 스코프 내에서만 의미가 있고, 그 밖의 공간에서는 참조가 되지 않게 해놓았다.(실제로 해보자) 이렇게 함으로써 전역 공간의 namespace가 오염되는 것을 막은 것이다.
77 |
78 | 이렇게 **scope는 각 변수, 함수명들이 영향을 미치는 범위를 의미한다고 생각하면 좋다.**
79 |
80 | 표현식에서 변수를 참조하면 파이썬 인터프리터는 참조를 해결하려고 다음과 같은 순서로 스코프를 탐색한다.
81 |
82 | 1. 현재 함수의 스코프(위의 helper 함수)
83 | 2. (현재 스코프를 담고 있는 다른 함수 같은) 감싸고 있는 스코프(위의 sort\_priority 함수)
84 | 3. 코드를 포함하고 있는 모듈의 스코프(전역 스코프라고도 함)
85 | 4. (len이나 str 같은 함수를 담고 있는) 내장 스코프
86 |
87 | 이 중 어느 스코프에도 참조한 이름으로 된 변수가 정의되어 있지 않으면 _NameError_ 예외가 일어난다.
88 |
89 | 첫 번째 예제 같은 경우에는 _group_ 이 2번 경우에 해당하여 _group_ 을 _helper_ 함수에서도 쓸 수 있었던 것이다.
90 |
91 | **그러면 두 번째 예제에서 _found_ 는 왜 문제가 되었던 것인가?**
92 |
93 | 그 이유는 **참조할 때와 달리 변수에 값을 할당할 때는 다른 방식으로 동작하기 때문이다.**
94 |
95 | 파이썬은 변수에 값을 할당할 때 변수가 현재 스코프에 존재하지 않으면 바깥 스코프와 관계 없이 새로운 변수 정의로 취급한다. 새로 정의되는 변수의 스코프는 그 할당을 포함하고 있는 함수가 된다.
96 |
97 | 코드를 다시 보자.
98 |
99 | ```python
100 | def sort_priority2(numbers, group):
101 | found = False # sort_priority2의 스코프
102 | def helper(x):
103 | if x in group:
104 | found = True # helper의 스코프
105 | return 0, x
106 | return 1, x
107 | numbers.sort(key=helper)
108 | return found # sort_priority2의 스코프
109 | found = sort_priority2(numbers, group)
110 | print('Found', found)
111 | ```
112 |
113 | **_helper_ 함수에서 _found_ 의 값에 True를 할당하고 있다. 할당이기 때문에 현재 존재하지 않았던 _found_ 에는 True가 할당되었고 이는 바깥쪽 _found_ 와는 엄연히 다른 값이다. 값을 참조한 것이 아니다.**
114 |
115 | *sort_priority2* 함수는 마지막에 _found_ 를 리턴하는데 여기서 _found_ 는, _helper_ 의 스코프가 아닌 *sort_priority* 스코프의 값을 반환하기 때문에 True가 아닌 False를 반환하는 것이다.
116 |
117 |
118 |
119 | 이 문제는 꽤 까다로워서 때로는 스코프 버그(scoping bug)라고도 한다. 하지만 언어 설계자의 의도라고 한다. 이 동작은 함수의 지역 변수가 자신을 포함하는 모듈을 오염시키는 문제를 막아준다.
120 |
121 | 그렇지 않았다면 함수 안에서 일어나는 모든 할당이 전역 모듈 스코프에 쓰레기를 넣는 결과로 이어졌을 것이다. 이로 인해 생기는 버그는 꽤 파악하기 어렵다.
122 |
123 |
124 |
125 |
126 | ## 2. 해결방안
127 |
128 | ### 2.1. nonlocal로 데이터 얻어오기
129 |
130 | 파이썬 3에는 클로저에서 데이터를 얻어오는 특별한 문법이 있는데 _nonlocal_ 을 사용하는 것이다. 이것은 **특정 변수 이름에 할당할 때 스코프 탐색이 일어나야 함을 나타낸다.** 유일한 제약은 _nonlocal_ 이 전역변수의 오염을 피하기 위해 모듈 수준 스코프까지는 탐색할 수 없다는 것이다.(2번까지만)
131 |
132 | 사용 예시는 다음과 같다.
133 |
134 | ```python
135 | def sort_priority3(numbers, group):
136 | found = False
137 | def helper(x):
138 | nonlocal found
139 | if x in group:
140 | found = True
141 | return 0, x
142 | return 1, x
143 | numbers.sort(key=helper)
144 | return found
145 | ```
146 |
147 | `nonlocal found`라는 문장을 통해 _found_ 변수에 값이 할당되면 이 스코프에 변수를 새로 만드는 것이 아니라, 스코프 탐색을 통해 변수를 찾아서 값을 주라는 구문으로 변하였다.
148 |
149 | _nonlocal_ 문을 통해 클로저에서 데이터를 다른 스코프에 할당하는 시점을 알아보기 쉽게 해준다. _nonlocal_ 문은 변수할당이 모듈 스코프에서 직접 들어가게 하는 _global_ 문을 보완한다.
150 |
151 | 하지만 전역변수의 안티패턴과 마찬가지로 간단한 함수 의외에는 _nonlocal_ 을 사용하지 않도록 주의해야 한다. _nonlocal_ 의 부작용은 특히 알아내기 어렵고, _nonlocal_ 문과 관련 변수에 대한 할당이 멀리 떨어진 긴 함수에서는 이해하기 더욱 어렵다.
152 |
153 | 주의해서 쓰도록 하자.
154 |
155 |
156 |
157 | ### 2.2. 헬퍼 클래스로 감싸자.
158 |
159 | 이건 일단 코드를 봐야 안다.
160 |
161 | ```python
162 | class Sorter:
163 | def __init__(self, group):
164 | self.group = group
165 | self.found = False
166 |
167 | def __call__(self, x):
168 | if x in self.group:
169 | self.found = True
170 | return 0, x # 튜플은 ( )로 안 감싸도 된다.
171 | return 1, x
172 |
173 | sorter = Sorter(group)
174 | numbers.sort(key=sorter)
175 | assert sorter.found is True
176 | ```
177 |
178 | 여기서 _Sorter_ 는 헬퍼 클래스다.
179 |
180 | _sort_ 메서드의 _key_ 에 _Sorter_ 클래스 인스턴스를 넣고 있는데 instnace를 호출할 수 있기 위해서는(callable 하게 만들기 위해서는) \_\_call\_\_ 메서드를 정의해야 한다.
181 |
182 | call 되는 순간 _x_ 라는 값을 _self.group_ 에 있는지 검정하여 계산한다.
183 | 결과는 똑같다.
184 |
185 |
186 |
187 | ## 3. 핵심 정리
188 |
189 | * 클로저 함수는 자기 자신이 정의된 스코프 중 어디에 있는 변수도 참조할 수 있다.
190 | * 기본적으로 클로저에서 변수를 할당하면 바깥쪽 스코프에는 영향을 미치지 않는다.
191 | * 파이썬 3에서는 _nonlocal_ 문을 사용하여 클로저를 감싸고 있는 스코프의 변수를 수정할 수 있다.
192 |
--------------------------------------------------------------------------------
/files/BetterWay16_generator.md:
--------------------------------------------------------------------------------
1 | ## Better Way 16. 리스트를 반환하는 대신 제너레이터를 고려하자
2 |
3 | #### 63쪽
4 |
5 | * Created : 2017/02/04
6 | * Modified: 2019/05/13
7 |
8 |
9 |
10 | ## 1. 함수에서 list 반환의 문제점
11 |
12 |
13 | 어떤 일련의 결과를 생성하는 함수에서 반환 Sequence의 타입을 정하는 가장 일반적이고 간단한 방법은 _list_ 를 반환하는 것이다. 예를 들어, 어떤 문자열에 있는 모든 단어의 인덱스를 출력하고 싶다고 하자. 여기서 단어란 `' '`를 기준으로 나뉘는 어구를 단위로 한다.
14 |
15 | 나같은 사람이 일반적으로 택할 수 있는 방법은 말한대로 인덱스의 _list_ 를 반환하는 방법일 것이다.
16 |
17 |
18 | ```python
19 | def index_words(text):
20 | result = []
21 | text = text.strip()
22 |
23 | if text:
24 | result.append(0)
25 |
26 | for index, word in enumerate(text):
27 | if word == ' ':
28 | result.append(index + 1)
29 | return result
30 |
31 |
32 | >> print(index_words('Do you wanna build a snowman?'))
33 |
34 | [0, 3, 7, 13, 19, 21]
35 | ```
36 |
37 | 반환된 리스트는 문장의 각 단어가 시작되는 인덱스를 담고 있다. 원 문자열에 _str.strip_ 메소드를 적용함으로써 빈 문자열 등에도 잘 작동하고 있다.
38 |
39 |
40 |
41 | 이 코드에 문제가 있을까? 사실 내가 일반적으로 사용하는 방식도 이런 식이기 때문에 여기서 문제점을 찾자는 저자의 문장에 의아해했다. 어떤 문제점이 있을까? 어떻게 코드를 좀더 바람직하게 만들 수 있을까?
42 |
43 | 저자가 말하는 저 코드의 개선할 점은 다음과 같다.
44 |
45 | * **코드가 약간 복잡하고 깔끔하지 않다.**
46 | - 이 함수의 핵심은 문자열의 인덱스를 뽑아내는 것이다. 그런데 **함수의 핵심기능과 별 관련이 없는 _result_ 리스트 선언, 원소 추가, 반환에 많은 바이트를 소모하고 있어 정작 인덱스를 뽑아내는 작업에 주의가 덜 집중된다.**
47 | * **결과를 일괄적으로 반환해서 메모리 에러가 발생할 수 있다.**
48 | - 이는 리스트 같은 자료구조를 쓸 때 흔히 발생할 수 있는 에러로 가령 크기가 10억에 달하는 리스트를 선언하고 쓰려고 한다면 시스템에 따라 Segmentation Fault가 발생해서 파이썬 자체가 종료될 수도 있다. 이에 대한 바람직한 해결책이 바로 Iterator, Generator를 사용하는 것이다. 이 둘은 _next_ 내장함수로 평가 시에만 한 번에 한 값씩 반환하기 때문에 아까처럼 10억 개의 개수를 메모리에 모두 담지 않아도 된다.
49 |
50 |
51 |
52 | 단순 list 사용 시의 단점에 대해서는 잘 알고 있었다. 그것이 Iterator, Generator를 쓰는 핵심적인 이유이기도 하다. 그런데 첫 번째 단점에 대해서는 생각해보지 못했다. 나도 그동안 무감각하게 단순 리스트를 선언하고, 원소 추가하고, 결과를 반환해왔다. 하지만 그게 정말 바람직한 것은 아닐 수 있구나.
53 |
54 | 정말 일리 있는 생각이다. **확실히 저 위의 함수를 조금만 민감하게 생각하면 _result_ 와 관련된 코드 때문에 인덱스를 찾아내는 과정이 명료하게 눈에 들어오지 않는다.** 지금 저 함수는 워낙 짧고 간단해서 그렇지 각 함수가 수백, 수천 줄에 이르게 되면 이런 복잡도는 함수의 가독성을 낮추는 치명적인 역할을 할 수 있다.
55 |
56 | 이에 대한 해결책으로 저자는 Generator를 사용하자고 말하고 있다.
57 |
58 |
59 |
60 |
61 | ## 2. Generator 사용하기
62 |
63 |
64 | 같은 함수를 제너레이터를 사용해 다시 만들어보자. 그리고 결과에 대해 리스트를 만들어야 한다면 그때 제너레이터를 리스트로 변환하자.
65 |
66 | ```python
67 | def index_words_iter(text):
68 | text = text.strip()
69 |
70 | if text:
71 | yield 0
72 |
73 | for index, letter in enumerate(text):
74 | if letter == ' ':
75 | yield index + 1
76 |
77 |
78 | result = list(index_words('Do you wanna build a snowman?'))
79 | >>> print(result)
80 |
81 | [0, 3, 7, 13, 19, 21]
82 | ```
83 |
84 | 같은 기능의 함수를 이번엔 generator를 사용해 한 값씩 반환하는 기능으로 변환했다. 이 함수를 이전의 _index\_words_ 함수와 비교하면서 내가 느낀 확연한 개선은 다음과 같다.
85 |
86 | * 가독성이 증대됐다.
87 | - 확실히, 단어의 시작 인덱스를 출력한다는 원 함수의 취지가 눈에 확 들어온다. 인정?
88 | * 결과 제너레이터에 대한 사용자의 사용 자유도가 증가한다.
89 | - 이건 반환된 제너레이터를 _list_ 로 변환하는 코드를 통해 느꼈다. 위 예에서는 리스트로 변환했지만 사용자의 수요에 따라 때로는 set, tuple 등으로 충분히 변환할 수 있을 것이다. 이것도 참 괜찮은 것 같다.
90 | - 그러면 '첫 번째 반환된 리스트에 set, tuple 생성자 등을 써도 되지 않느냐'라는 질문도 충분히 할 수 있다. 하지만 이는 적절하지 않은데, 첫 째로 이미 특정 자료구조로 변환한 값을 다시 다른 자료구조로 변환하는 데 자원이 소모되고, 가령 set 등의 순서없는 자료구조로 변환한 값을 다시 _list_ 등으로 변환하면 '순서'가 사라지는 문제가 발생한다.
91 |
92 |
93 |
94 | 다음 예제를 살펴보자. 이번에는 파일에서 한 줄마다 한 번에 한 단어씩 출력을 내어주는 제너레이터를 만들어보자. **이 함수가 동작할 때 사용하는 메모리는 전체 파일의 크기가 아니라, 입력 한 줄의 최대 길이까지다.**
95 |
96 |
97 | ```python
98 | def index_file(handle):
99 | offset = 0
100 | for line in handle :
101 | if line:
102 | yield offset
103 | for letter in line:
104 | offset += 1
105 | if letter == ' ':
106 | yield offset
107 |
108 |
109 | with open('somebody.txt', 'r') as f:
110 | it = index_file(f)
111 | result = itertools.islice(it, 0, 3)
112 |
113 | >>> print(list(result))
114 |
115 | [0, 5, 11]
116 | ```
117 |
118 | 제너레이터를 사용할 때 한 가지 주의할 점은 **반환되는 이터레이터는 상태가 있고, 한 번 순회하면 다시 재사용할 수 없다는 것이다.** 다시 사용하고 싶다면 다시 할당해야 한다. 이 점만 염두에 두자.
119 |
120 |
121 |
122 |
123 | ## 3. 핵심 정리
124 |
125 | * 제너레이터를 사용하는 방법이 누적된 결과의 리스트를 반환하는 방법보다 이해하기 명확하다.
126 | * 제너레이터에서 반환한 이터레이터는 제너레이터 함수의 본문에 있는 _yield_ 표현식에 전달된 값들의 순서 있는 집합이다.
127 | * **제너레이터는 모든 입력과 출력을 메모리에 저장하지 않으므로, 입력값의 양을 알기 어려울 때도 안전하게 각 단위씩 연속된 출력을 만들 수 있다.**
128 |
--------------------------------------------------------------------------------
/files/BetterWay18_PositionalArg.md:
--------------------------------------------------------------------------------
1 | ## Better Way 18. 가변 위치 인수로 깔끔하게 보이게 하자
2 |
3 | #### 72쪽
4 |
5 | * Created : 2016/09/03
6 | * Modified: 2019/05/15
7 |
8 | * 18, 19, 21장에서는 그 유명한 Packing과 Unpacking에 대해 다룬다.
9 |
10 |
11 |
12 |
13 |
14 | ## 1. 임의의 개수를 받는 위치 인수 사용법
15 |
16 | **함수에서 선택적인 위치 인수(positional arguments, 함수 정의에서 _\*args_ 와 같은 변수명을 사용)를 받게 만들면 함수 호출을 더 명확하게 할 수 있고 보기에 방해가 되는 요소를 없앨 수 있다.**
17 |
18 | 먼저 위치 인수를 사용하지 않는 간단한 사용함수를 만들어보자.
19 |
20 | 디버그 정보 몇 개를 로그에 남기는 함수를 만든다고 하자. 인수의 개수가 고정되어 있다면 메시지와 값
21 | 리스트를 받는 함수를 만든다고 할 수 있다.
22 |
23 | ```python
24 | def log(message, values):
25 | if not values:
26 | print(message)
27 | else:
28 | joined_values = ', '.join(str(v) for v in values)
29 | print(message + ': ', joined_values)
30 |
31 |
32 | >>> log('My numbers are', [1, 2])
33 | >>> log('반갑소 제군들', [])
34 |
35 | My numbers are: 1, 2
36 | 반갑소 제군들
37 | ```
38 |
39 | 잘 작동한다만 두 번째 사용예처럼 로그에 남길 값이 없을 때 빈값을 넘기는 것은 불편한 일이다. 또한
40 | 남길 값이 한 개가 아닌, 두 개, 세 개, 수천만 개일 수도 있지 않은가? 지금 저 로그 함수는 그런
41 | 입력들에 대해 대응하지 못한다.
42 |
43 | 이때 선택적 위치 인수를 사용한다. 함수 정의에서 위치 인수 Packing을 사용하면 입력들의 임의의 개수에
44 | 대해 적절하게 반응할 수 있다.
45 |
46 | ```python
47 | def log(message, *values): # !!!
48 | if not values:
49 | print(message)
50 | else:
51 | joined_values = ', '.join(str(v) for v in values)
52 | print(message + ': ', joined_values)
53 |
54 |
55 | >>> log('My numbers are', [1, 2])
56 | >>> log('My numbers are', 1, 2)
57 | >>> log('반갑소 제군들')
58 |
59 | My numbers are: 1, 2
60 | My numbers are: 1, 2
61 | 반갑소 제군들
62 | ```
63 |
64 | 아까의 로그 함수와 거의 비슷한데 다만, _message_ 변수 이후에 **임의의 개수의 입력에 대해 모두 적절하게 반응할 수 있도록 '\*'를 사용해 Packing을 지시했다.**
65 |
66 | 이를 통해서 세 번째 예처럼 _message_ 이후에 0개의 입력을 주는 것은 물론, 두 번째처럼 두 개의 개수의
67 | 변수를 입력해도 문제없이 작동했다.
68 |
69 |
70 |
71 | 이런 위치 인수를 사용하도록 정의된 함수에서는, 다시 말해 임의의 개수의 인자를 하나의 개수로
72 | Packing하는 함수는 실행할 때 어떻게 사용할까? 만약 **입력이 Iterable이라면 실행 시에 인자에 '\*'를 붙여줘서 Unpacking하면 된다.**
73 |
74 | ```python
75 | log('test1', 1, 2, 3)
76 | log('test2', *['a', 'b', 'c', 'd', 'e']) # !!
77 | log('test3', 1, 2, *['가', '나', '다']) # !!
78 |
79 |
80 | test1: 1, 2, 3
81 | test2: a, b, c, d, e
82 | test3: 1, 2, 가, 나, 다
83 | ```
84 |
85 | 앞서 _log_ 함수 정의에서는 첫 인자를 제외한 다른 모든 입력 인자들을 하나의 단일 인자로 합쳤다.(Packed) 이때 합친 인자의 타입은 _tuple_ 이 된다. 이 함수를 사용할 때는 리스트처럼 이미 합쳐진 인자라면 펼친 채로 입력하면 로그 함수가 알아서 합쳐서 잘 실행할 것이다. 따라서 실행 시에는 '\*'를 써서 인자를 풀어준다.(Unpacked)
86 |
87 | 이게 Packing과 Unpacking의 핵심으로 이것만 이해하면 향후 다룰 키워드 전용 인자도 문제없다.
88 |
89 |
90 |
91 |
92 |
93 | ## 2. 선택적 위치 인자 사용의 단점
94 |
95 | 앞서 살펴본 Packing과 Unpacking을 통해 자바 등의 언어에서 지원하는 메소드 오버로딩을 파이썬에서
96 | 어떻게 지원하는지 알 수 있었다. 다만 이 사용법에는 두 가지 주요 문제가 있다.
97 |
98 |
99 | ### 2.1. 메모리 고갈의 문제점
100 |
101 |
102 | 함수 정의 시에 Packing을 사용하면 입력을 하나의 튜플로 묶으며, 사용 시에 Unpacking을 사용하면 Iterable한 입력을 풀어헤친다는 것을 살폈다. 여기서 문제가 되는 것은 Unpacking이다. **함수를 호출하는 쪽에서 제너레이터에 '\*' 연산자를 쓰면 제너레이터가 모두 소진될 때까지 순회해서 튜플을 만든다는 뜻이 되기 때문에, 결과로 생성되는 튜플은 메모리를 너무 많이 차지해 프로그램이 망가질 수 있다.**
103 |
104 | ```python
105 | def my_generator():
106 | for i in range(10):
107 | yield i
108 |
109 | def my_func(*args):
110 | type(args)
111 |
112 |
113 | it = my_generator()
114 | >>> my_func(*it)
115 |
116 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
117 | ```
118 |
119 | **\*args를 받는 함수는 인수 리스트에 있는 입력의 수가 적당히 적다는 사실을 아는 상황에서 가장 좋은 방법이다.** 이런 함수는 많은 리터럴이나 변수 이름을 한꺼번에 넘기는 함수 호출에 이상적이다. 주로 개발자들을 편하게 하고, 가독성을 높이려고 사용한다.
120 |
121 | ### 2.2. 기능 확장 시에 디버깅의 어려움
122 |
123 | 두 번째 문제는 선택적 위치 인수를 사용해 정의한 기능을 확장할 때 생기는 문제점이다. 일반적으로, 원래의 기능 정의를 확장한다면 그 기능을 사용하는 이전 호출코드는 문제없이 사용되기를 바랄 것이다. 즉 새 기능은 이전 기능을 문제없이 포함할 수 있어야 한다.
124 |
125 | 다만 문제가 되는 것은 **기능에 새 위치 인수를 추가하고 싶을 때에는 추후에 호출 코드를 모두 변경해야 한다는 점이다.** 함수 인자 목록 앞쪽에 위치 인자를 추가하면, 기존의 호출 코드가 수정 없이는 정상적으로 동작하지 않는다.
126 |
127 | 기존의 로그 함수에서 메시지와 값에 추가로 로그의 코드 번호를 추가한다고 하자. 이는 HTTP response의 status code와 같은 역할을 생각하면 될 것이다.
128 |
129 | ```python
130 | def log(code, message, *values):
131 | if not values:
132 | print('{} / {}'.format(code, message))
133 | else:
134 | values_str = ', '.join(str(x) for x in values)
135 | print('{} / {}: {}'.format(code, message, values_str))
136 |
137 | >>> log(200, 'favorites', 7,33) # 1.
138 | >>> log('Favorite', 7, 33) # 2.
139 |
140 |
141 | 200 / favorites: 7, 33
142 | Favorite / 7: 33
143 | ```
144 |
145 | 1. 확장된 기능에 맞게 함수를 호출한 로그 함수에서는 의도대로 결과물이 출력됐다.
146 | 1. 두 번째 호출은 이전 호출 코드를 그대로 가져온 것인데 출력이 의도대로 나오지 않았다.
147 |
148 | 이런 문제가 발생한 이유는 두 번째 호출이 _code_ 인수를 받지 못했기 때문에 'Favorite'를 _code_ 로, 7을 _message_ 로 사용했다는 점이다. **이런 버그는 코드에서 예외를 일으키지 않고 계속 실행되기 때문에 발견하기가 극히 어렵다. 이런 문제가 생길 가능성을 없애려면 _\*args_ 를 받는 함수를 확장할 때 키워드 전용(keyword only) 인수를 사용하는 것이다. 이는 21장에서 다룰 것이다.**
149 |
150 |
151 |
152 | ## 3. 핵심 정리
153 |
154 | * def 문에서 \*args를 사용하면 함수에서 가변 개수의 위치 인수를 받을 수 있다.
155 | * \* 연산자를 쓰면 시퀀스에 들어 있는 아이템을 함수의 위치 인수로 사용할 수 있다.
156 | * 제너레이터와 \* 연산자를 함께 사용하면 프로그램이 메모리 부족으로 망가질 수 있다.
157 | * \*args를 받는 함수에 새 위치 파라미터를 추가하면 정말 찾기 어려운 버그가 생길 수도 있다.
158 |
--------------------------------------------------------------------------------
/files/BetterWay19_KeywordArg.md:
--------------------------------------------------------------------------------
1 | ## Better Way 19. 키워드 인수로 선택적인 동작을 제공하자
2 |
3 | #### 75쪽
4 |
5 | * Created : 2016/10/08
6 | * Modified: 2019/05/16
7 |
8 |
9 |
10 |
11 | ## 1. 함수 호출에서의 키워드 인자의 사용
12 |
13 |
14 | 18장에서 확인했다시피 파이썬에서도 다른 프로그래밍 언어와 마찬가지로 함수를 호출할 때 인자를 위치로(Positional arguments) 전달할 수 있다.
15 |
16 | ```python
17 | # 정수를 다른 정수로 나눈 나머지를 반환하는 함수
18 | def remainder(n, d):
19 | return n % d
20 |
21 |
22 | >>> print(remainder(20, 7))
23 |
24 | 6
25 | ```
26 |
27 | **위치 인자는 함수 호출 시에 단순히 값만 전달했지만, 인자를 이름과 값 쌍으로 전달하는 키워드 방식도 있다.(Keyword arguments)** 인자의 이름은 함수 정의 시에 정의된 인자 이름을 그대로 사용해야 하고 위치 인자와 키워드 인자를 섞어 쓰는 것도 얼마든지 가능하다.
28 |
29 |
30 | ```python
31 | remainder(20, 7)
32 | remainder(20, d=7)
33 | remainder(n=20, d=7)
34 | remainder(d=7, n=20) # 인자를 모두 키워드로 쓰면 순서를 바꿔도 문제없이 동작한다.
35 | ```
36 |
37 | 만약 위치 인자와 키워드 인자를 섞어쓴다면 **파이썬 문법상 위치 인자는 무조건 키워드 인자 앞에 위치해야 한다.**
38 |
39 |
40 | ```python
41 | remainder(n=20, 7)
42 |
43 | SyntaxError: positional argument follows keyword argument
44 | ```
45 |
46 | 또한 각 인자는 한 번만 지정할 수 있다.
47 |
48 |
49 | ```python
50 | remainder(20, n=7)
51 |
52 | TypeError: remainder() got multiple values for argument 'n'
53 | ```
54 |
55 | 정의상 함수의 첫 인자(20)는 _n_ 인데 뒤에 키워드 인자로 _n_ 을 한 번 더 받았기 때문에 에러가 출력됐다.
56 |
57 |
58 |
59 |
60 | ## 2. 키워드 인자 사용의 장점
61 |
62 | 함수 호출 시에 위치 인자를 사용하는 것보다 키워드 인자를 사용하는 것이 비용이 더 많이 든다. 파일의 크기가 커질 것이고, 인자를 입력하는 데 따르는 시간과 개발자의 에너지 비용이 더 소모될 것은 말할 것도 없다. 그럼에도 키워드 인자를 사용하는 데는 몇 가지 장점이 있다.
63 |
64 | * **코드를 처음 보는 사람이 함수 호출을 명확하게 이해할 수 있다**
65 |
66 | 이는 꽤나 명확하다. 협업하게 된 사람이 우리가 만든 기가 막힌 _remainder_ 함수를 보게 됐다고 하자. _remainder(20, 7)_ 이라고 적혀 있는 것보다는 _remainder(n=20, d=7)_ 라고 적혀 있는 것이 이해하기 훨씬 쉽다. 만약 가독성을 좀더 증대시켜야 한다면 함수 정의와 호출에서 인자 이름을 보다 구체적으로 바꿔 _remainder(number=20, divisor=7)_ 처럼 사용한다면 제 3자 입장에서 이해하기 더 쉬울 것이다.
67 |
68 |
69 |
70 | * **함수를 정의할 때 기본값을 설정할 수 있다**
71 |
72 | 한 기능이 여러 가지 행동 옵션을 제공하도록 정의할 수 있는데 이런 경우 대부분 사용자들이 원하는 행동 옵션은 정해져있기 마련이다. 이때는 지분이 큰 옵션을 기본 행동으로 정해놓고 사용자가 다른 옵션을 원할 때만 추가로 지정하는 것이 바람직할 것이다.
73 |
74 | 예를 통해 살펴보자. 큰 통에 들어가는 액체의 유속을 계산하고 싶다고 하자. 큰 통의 무게를 잴 수 있다면, 각기 다른 시각에 측정한 두 무게의 차이를 이용해 유속을 알 수 있다.
75 |
76 |
77 | ```python
78 | def flow_rate(weight_diff, time_diff):
79 | return weight_diff / time_diff
80 |
81 | weight_diff = .5
82 | time_diff = 3
83 | flow = flow_rate(weight_diff, time_diff)
84 |
85 | >>> print('%.3f kg per second' % flow)
86 |
87 | 0.167 kg per second
88 | ```
89 |
90 | 이런 문제는 항상 '단위'에 대한 고민을 해야 하는데 일단 시간 단위는 초 단위였다고 하자. 하지만 때로는 시간이나 날짜처럼 더 큰 단위로 계산하고 싶을 수 있다. **다시 말해 시간 단위의 기본값은 초 단위이고 이 단위가 가장 많이 쓰이지만 때로는 사용자의 수요에 따라 다른 단위 옵션을 제공하는 것이 필요할 수 있다는 뜻.**
91 |
92 | 함수 정의에서 기간 환산 계수를 추가하면 이런 동작을 쉽게 제공할 수 있다.
93 |
94 | ```python
95 | def flow_rate(weight_diff, time_diff, period): # period는 초 단위
96 | return weight_diff / time_diff * period
97 | ```
98 |
99 | 뭐 무난하게 동작한다. **하지만 이 방법의 단점은 함수를 호출할 때마다, 심지어 초당 유속을 사용하는 일반적인 경우(_period_ 가 1)에도 기간을 설정해야 한다는 점이다.**
100 |
101 | ```python
102 | flow = flow_rate(weight_diff, time_diff, 1)
103 | ```
104 |
105 | 이런 상황을 피하기 위해 **함수 정의에서 키워드 인자를 통해 기본값을 설정해주면 좋다.**
106 |
107 | ```python
108 | def flow_rate(weight_diff, time_diff, period=1): # period는 초 단위
109 | return weight_diff / time_diff * period
110 |
111 |
112 | flow_rate(weight_diff, time_diff) # 1.
113 | flow_rate(weight_diff, time_diff, period=3600) # 2.
114 | ```
115 |
116 | 첫 번째 예제에서는 _period_ 인자를 주지 않았기 때문에 기본값(1)을 쓴다는 의미가 되고, 두 번째는 사용자가 일반적이지 않은 시간 단위의 계산을 원하기 때문에 인자를 명시적으로 입력해주었다. 이제 _period_ 는 선택적인 인자가 되었으며 함수 사용이 더 유연해졌다.
117 |
118 |
119 |
120 |
121 | * **기존 호출 코드와 호환성을 유지하면서 파라미터를 확장할 수 있다**
122 |
123 | 앞선 18장에서 위치 인자만 사용할 때는 함수를 확장하면 기존 호출 코드가 비정상적으로 작동할 수 있다는 점을 살펴보았다. 하지만 **키워드 인자를 사용하면 기존 코드와 호환성을 유지하며 함수의 파라미터(인자)를 확장할 수 있다.** 이 방법을 쓰면 코드를 많이 수정하지 않고도 추가적인 기능을 제공할 수 있고, 디버깅하기 힘든 버그가 생길 가능성을 줄일 수 있다.
124 |
125 | 이전 함수를 한 단계 더 확장해서 이제는 무게 단위도 기존 킬로그램 단위가 아닌 다른 무게 단위로도 유속을 계산한다고 하자. 함수를 확장해서 원하는 측정 단위와 킬로그램의 변환 비율을 선택 파라미터로 추가해야 할 것이다.
126 |
127 |
128 | ```python
129 | def flow_rate(weight_diff, time_diff,
130 | period=1, units_per_kg=1):
131 | return weight_diff / units_per_kg / time_diff * period
132 | ```
133 |
134 | _units\_per\_kg_ 인수의 기본값은 1로 단위를 kg로 쓴다는 의미가 된다. 킬로그램을 무게 단위의 기본 도량형으로 쓰는 한국인 입장에서 적절한 조치라고 할 수 있다. 이렇게 함수를 확장했을 때 장점은 **이 함수 이전에 실행했던 모든 함수 호출이 문제없이 실행됐다는 것이다.**
135 |
136 | 물론 이 방법에도 단점은 있는데, **기간이나 무게 단위와 같은 선택적 키워드 인자를 여전히 위치 인자로도 넘길 수 있다는 점이다.**
137 |
138 | ```python
139 | pounds_rate(flow_rate(wd, td, 3600, 2.2))
140 | ```
141 |
142 | 이런 호출은 3600, 2.2 같은 단순 숫자가 의미하는 바가 명확하지 않아 문맥을 이해하지 못하고 있는 리뷰어들에게는 혼동을 일으킬 수 있다. 이런 경우를 대비하기 위해서는 **항상 키워드 이름으로 선택적인 인수를 지정하고 위치 인수로는 아예 넘기지 않는 것이 바람직하다.**
143 |
144 |
145 |
146 |
147 | ## 3. 핵심 정리
148 |
149 | * 함수의 인자는 위치뿐만 아니라 키워드로도 지정할 수 있다.
150 | * 위치 인자만으로는 이해하기 어려울 때 키워드 인자를 쓰면 각 인자를 사용하는 목적이 분명해진다.
151 | * 키워드 인자에 기본값을 지정하면 함수에 새 동작을 쉽게 추가할 수 있다. 특히 함수를 호출하는 기존 코드가 있을 때 사용하면 좋다.
152 | * 선택적인 키워드 인자는 항상 위치가 아닌 키워드로 넘기는 것이 좋다.
153 |
--------------------------------------------------------------------------------
/files/BetterWay20_DynamicDefaultArgument.md:
--------------------------------------------------------------------------------
1 | ## Better Way 20. 동적 기본 인수를 지정하려면 None과 docstring을 사용하자
2 |
3 | #### 79쪽
4 |
5 | * Created : 2016/10/08
6 | * Modified: 2019/05/17
7 |
8 |
9 |
10 | ## 1. 함수에서 비정적 타입의 기본인자 사용의 문제점
11 |
12 | 앞선 장들에서 우리는 키워드 인자의 기본값으로 3, 1 같은 비정적(non-static) 타입만을 사용했다. 이때는 함수를 사용함에 있어서 섬세한 주의가 필요하다.
13 |
14 | 예를 들어 이벤트 발생 시각을 포함해 로깅 메시지를 출력한다고 하자. 서버에 로그를 남길 때 로깅 시간은 같이 기록하기 마련이다. 일반적인 로그 함수에 기록될 시간은 함수가 호출되는 시간이지만 원할 경우에는 임의의 시간 기록이 가능하도록 작성한다고 하자. 아마 다음과 같을 것이다.
15 |
16 |
17 | ```python
18 | import datetime
19 | import time
20 |
21 |
22 | # Use Python 3.6 boy
23 | def log(message, when=datetime.datetime.now()):
24 | print(f"{when}: {message}")
25 | ```
26 |
27 | 메시지와 시간을 기록하는 로그 함수를 만들었다. 로그 메시지는 일반적으로 호출된 그 시간을 기록하기 때문에 19장에서 배운대로 키워드 인자를 통해 _when_ 의 기본값을 할당했다. 잘 작동하는지 볼까?
28 |
29 |
30 | ```python
31 | # 1. Test 1
32 | log("Greetings!")
33 |
34 | # 2. Test 2 after sleep 0.1 sec
35 | time.sleep(0.1)
36 | log("Greetings again")
37 |
38 | 2019-05-17 12:15:14.811373: Greetings!
39 | 2019-05-17 12:15:14.811373: Greetings again
40 | ```
41 |
42 | 첫 번째 로그 함수를 호출한 뒤 _time.sleep_ 으로 0.1초 정도 뒤에 다시 호출했다. 당연히 두 로그에 기록된 시간이 다르리라 예상할 수 있다. 그런데 이상하게도 두 함수의 시간이 동일하다. 참 기묘한 일인데 왜 그럴까?
43 |
44 | **_now_ 함수는 함수 호출이 아닌 함수 정의식에서 호출되었다. 함수 정의는 프로그램이 실행될 때 한 번만 진행되므로 _when_ 은 처음 함수가 정의될 때의 시간으로 고정된 것이다.** 결과적으로 '3' 같은 정적값을 준 것과 큰 차이가 없게 되었다. 그래서 비정적 타입을 기본 인자로 쓸 때 주의해야 한다.
45 |
46 |
47 |
48 |
49 | ## 2. 해결책
50 |
51 | 이런 상황에서 의도한 대로 결과가 나오게 하려면 **기본값을 _None_ 으로 설정하고 docstring으로 실제 동작을 문서화하는 게 관례다.** 코드에서 인수 값으로 _None_ 이 나타나면 알맞은 기본값을 할당하면 된다.
52 |
53 |
54 | ```python
55 | def log(message, when=None):
56 | """Log a message with a timestamp
57 |
58 | :input:
59 | message: Message to print
60 | when: datetime of when the message occurred.
61 | Defaults to the present time.
62 | """
63 | if when is None:
64 | when = datetime.datetime.now()
65 | print(f"{when}: {message}")
66 |
67 |
68 |
69 | log("Greetings!")
70 | time.sleep(0.1)
71 | log("Greetings again")
72 |
73 |
74 | 2019-05-17 12:27:54.804982: Greetings!
75 | 2019-05-17 12:27:54.906290: Greetings again
76 | ```
77 |
78 | 이제 의도한 대로 동작하고 있다.
79 |
80 |
81 |
82 | ## 3. 추가 예제
83 |
84 | **기본 인자로 _None_ 을 사용하는 방법은 인자가 Mutable할 때 특히 중요하다.** 예를 들어 문자열 형태로 직렬화된 JSON 데이터를 파이썬 dict로 로드한다고 해보자. 데이터 디코딩이 어떤 이유로든 실패하면 기본값으로 빈 딕셔너리를 반환하려고 한다. 다음과 같은 방법을 써볼 수 있겠다.
85 |
86 | ```python
87 | import json
88 |
89 |
90 | def decode(data, default={}):
91 | try:
92 | return json.loads(data)
93 | except ValueError:
94 | return default
95 | ```
96 |
97 | 이 코드는 아까의 로그 예제와 같은 문제가 있다. **기본 인자값은 모듈이 로드될 때 딱 한 번만 평가되므로, 기본값으로 설정한 딕셔너리를 모든 decode 호출에서 공유한다.** 이는 더 심각한 문제를 낳는다.
98 |
99 | ```python
100 | foo = decode('bad data')
101 | foo['stuff'] = 3
102 |
103 | bar = decode('So do I!')
104 | bar['eggs'] = 5
105 |
106 | >>> print('foo:', foo)
107 | >>> print('bar:', bar)
108 |
109 |
110 | foo: {'stuff': 3, 'eggs': 5}
111 | bar: {'stuff': 3, 'eggs': 5}
112 | ```
113 |
114 | 이 예제에서는 당연히 _foo_, _bar_ 각각 단일 키와 값을 담은 서로 다른 딕셔너리를 가졌으리라 예상했을 것이다. 하지만 **두 함수 모두 함수 정의 시에 설정된 _default_ 딕셔너리를 공유하고, 딕셔너리는 값이 변할 수 있기 때문에, 다시 말해 Mutable하기 때문에 하나가 수정되면 다른 하나도 수정되는 것처럼 보인다.** 실제로 반환된 딕셔너리는 같은 값이다.
115 |
116 |
117 | ```python
118 | assert foo is bar
119 | ```
120 |
121 | 이때에도 _default_ 값을 _dict_ 가 아닌 _None_ 으로 초기화하고 함수의 docstring에 동작을 문서화해 이 문제를 고친다.
122 |
123 | ```python
124 | import json
125 |
126 |
127 | def decode(data, default=None):
128 | """Load JSON data from a string
129 |
130 | :input:
131 | data: JSON string to parse.
132 | default: Value to return if decoding fails. Defaults to an empty dict.
133 | """
134 | if default is None:
135 | default = {}
136 |
137 | try:
138 | return json.loads(data)
139 | except ValueError:
140 | return default
141 | ```
142 |
143 | 이제 앞서 나온 테스트 코드들이 예상한 대로 동작할 것이다.
144 |
145 |
146 | ```python
147 | foo = decode('bad data')
148 | foo['stuff'] = 3
149 |
150 | bar = decode('So do I!')
151 | bar['eggs'] = 5
152 |
153 | >>> print('foo:', foo)
154 | >>> print('bar:', bar)
155 |
156 |
157 | foo: {'stuff': 3}
158 | bar: {'eggs': 5}
159 | ```
160 |
161 | 이렇듯 키워드 인자의 기본값을 비정적 타입으로 줄 때는 조심해야 한다.
162 |
163 |
164 |
165 |
166 | ## 4. 핵심 정리
167 |
168 | * 기반 인자는 모듈 로드 시점에 함수 정의 과정에서 딱 한 번만 평가된다. 그래서 (`{}`나 `[]`와 같은) 동적 값에는 이상하게 동작하는 원인이 된다.
169 | * 값이 동적인 키워드 인자는 기본값으로 _None_ 을 사용하자. 그러고 나서 함수의 docstring에 실제 기본 동작을 문서화하자.
170 |
--------------------------------------------------------------------------------
/files/BetterWay21_ForceKeywordArgument.md:
--------------------------------------------------------------------------------
1 | ## Better Way 21. 키워드 전용 인수로 명료성을 강요하자
2 |
3 | #### 83쪽
4 |
5 | * Created : 2016/10/27
6 | * Modified: 2019/05/20
7 |
8 |
9 |
10 |
11 | ## 1. 일반 키워드 인자 사용의 맹점
12 |
13 | 키워드로 인수를 넘기는 방법은 파이썬 함수의 강력한 기능이다. 이는 [19장](https://github.com/shoark7/Effective-Python/blob/master/files/BetterWay19_KeywordArg.md)에서 확인한 바 있다. 키워드 인자의 유연성 덕분에 쓰임새가 분명하게 코드를 작성할 수 있다.
14 |
15 | 예를 들어 어떤 숫자를 다른 숫자로 나눈다고 하자. 이때 특별한 경우를 상정하자. 때로는 0을 분모로 할 때 _ZeroDivisionError_ 대신 무한대를 반환하고 싶을 수도 있고, 어떨 때는 값이 너무 커질 때 반환되는 _OverflowError_ 대신 0을 반환하고 싶을 수도 있다.
16 |
17 | 이를 함수로 짜보자.
18 |
19 |
20 | ```python
21 | def safe_division(n, d, ignore_overflow, ignore_zero_division):
22 | try:
23 | return n / d
24 | except OverflowError:
25 | if ignore_overflow:
26 | return 0
27 | else:
28 | raise
29 | except ZeroDivisionError:
30 | if ignore_zero_division:
31 | return float('inf')
32 | else:
33 | raise
34 | ```
35 |
36 | try 문이 두 개 쓰여서 복잡해보이지만 사용법은 간단하다. 단순히 분자를 분모로 나누되, 지정한 에러가 발생하면 앞서 정의한 대로 문제를 처리한다.
37 |
38 | 다음 호출은 나눗셈에서 일어나는 float 오버플로우를 무시하고 0을 반환한다.
39 |
40 |
41 | ```python
42 | result = safe_division(1, 10**500, True, False)
43 | print(result)
44 |
45 | 0.0
46 | ```
47 |
48 | 다음은 0으로 나누면 일어나는 오류를 무시하고 무한대 값을 반환한다.
49 |
50 |
51 | ```python
52 | result = safe_division(1, 0, False, True)
53 | print(result)
54 |
55 | inf
56 | ```
57 |
58 |
59 | **문제는 예외 무시 동작을 제어하는 두 bool 인자의 위치를 혼동하기 쉽고 이해하기도 힘들다는 것이다.** 이 때문에 찾기 어려운 버그가 쉬이 발생할 수 있다. 이런 코드의 가독성을 높이는 한 가지 방법은 역시 키워드 인자를 사용하는 것이다.
60 |
61 | ```python
62 | def safe_division_b(n, d, ignore_overflow=False, ignore_zero_division=False):
63 | ...
64 | ```
65 |
66 | 함수를 호출하는 쪽에서는 다음과 같이 사용하리라 기대할 수 있다.
67 |
68 | ```python
69 | result = safe_division_b(1, 0, ignore_zero_division=True)
70 | ```
71 |
72 | 이것도 괜찮은 방법이지만 완벽하지는 않는데, **이런 키워드 인자가 선택적인 동작이라서 함수를 호출하는 쪽에서 키워드 인자로 의도를 명확하게 드러내라고 강제할 방법이 없다.** 새로 정의한 함수 또한 여전히 위치 인자를 사용하는 이전 방식으로 호출할 수 있다.
73 |
74 |
75 | ```python
76 | result = safe_division_b(1, 0, True, False)
77 | ```
78 |
79 |
80 |
81 | ## 2. 해결책
82 |
83 |
84 | 이처럼 인자가 많은 복잡한 함수를 작성할 때는 호출하는 쪽에서 의도를 명확히 드러내도록 요구하는 것이 낫다. **파이썬 3에서는인자를 키워드 전용 인수(keyword-only argument)로 정의해서 의도를 명확히 드려내도록 요구할 수 있다.** 키워드 전용 인수는 키워드로만 넘길 뿐, 위치로는 절대 넘길 수 없다.
85 |
86 | 사용법은 간단한데, 다음과 같다.
87 |
88 | ```python
89 | def safe_division_c(n, d, *,
90 | ignore_overflow=False, ignore_zero_division=False):
91 | ...
92 | ```
93 |
94 | **이제 '\*' 뒤에 있는 인수들은 위치 인자로 보내면 동작하지 않는다.** 설정된 기본값만 쓰든지, 아니면 함수에 명시적인 키워드 인자를 건네야 한다.
95 |
96 | ```python
97 | safe_division_c(1, 0, True)
98 |
99 | TypeError: safe_division_c() takes 2 positional arguments but 3 were given
100 | ```
101 |
102 | 일반적인 키워드 인자처럼 활용하면 전혀 문제가 없다. **함수에 관리해야 할 인자가 많거나, 인자 자체의 의미가 중요해서 그 의미를 사용자가 명확히 인지해야 할 경우, 인자를 키워드 인자로 강제하는 방법도 생각해봄직 하다.**
103 |
104 |
105 |
106 | ## 3. 핵심 정리
107 |
108 | * 키워드 인자는 함수 호출의 의도를 더 명확하게 해준다.
109 | * bool 플래그를 여러 개 받는 함수처럼 헷갈리기 쉬운 함수를 호출할 때 키워드 인자를 넘기게 하려면 키워드 전용 인자를 사용하자.
110 | * 파이썬 3에서는 함수의 키워드 전용 인자 문법을 명시적으로 지원한다. 파이썬 2는 지원하지 않는다.
111 |
--------------------------------------------------------------------------------
/files/BetterWay23_UseFuncForInterface.md:
--------------------------------------------------------------------------------
1 | ## Better Way 23. 인터페이스가 간단하면 클래스 대신 함수를 받자
2 |
3 | #### 97쪽
4 |
5 | * Created : 2016/10/31
6 | * Modified: 2019/05/21
7 |
8 |
9 |
10 | ## 1. 파이썬에서 함수는 1급 객체
11 |
12 | 파이썬의 내장 API의 상당수는 함수를 넘겨서 동작을 사용자화하는 기능이 있다. API는 이런 후크(hook)를 이용해서 작성한 코드를 실행 중에 호출한다. 예를 들어 `list`의 `sort` 메소드에는 `key`라는 추가적인 인자를 받을 수 있다.
13 |
14 | ```python
15 | names = ['안철수', '안안철수', '안안안철수', '안안안안안철수']
16 | # 글자 길이의 내림차순으로 리스트를 정렬한다.
17 | names.sort(key=lambda x: len(x), reverse=True)
18 |
19 | print(names)
20 |
21 | >>> ['안안안안안철수', '안안안철수', '안안철수', '안철수']
22 | ```
23 |
24 | `lambda`(이하 "람다") 함수를 후크로 받아서 정렬의 방법을 사용자화했다.
25 |
26 | 다른 언어에서는 이런 후크를 클래스로 정의하지만 파이썬에서는 일반적으로 함수를 사용한다. 이유는 파이썬이 [일급 함수](https://en.wikipedia.org/wiki/First-class_function)(first-class function)를 갖췄기 때문이다. 다시 말해, **언어에서 함수와 메소드에 다른 함수를 일반적인 정수, 문자열처럼 전달하고 참조할 수 있다.**
27 |
28 |
29 |
30 |
31 | ## 2. 활용 예제
32 |
33 | 이번엔 `collections`모듈의 [defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict)라는 자료구조를 사용해 _defaultdict_ 의 동작을 사용자화한다고 하자. 이 자료구조는 **찾을 수 없는 키에 접근할 때마다 호출될 함수를 첫 번째 인자로 받는다. 또한 이 함수는 찾을 수 없는 키에 대응할 기본값을 반환해야 한다.** 다음은 키를 찾을 수 없을 때마다 로그를 남기고, 기본값으로 0을 반환하는 후크를 정의한 코드다.
34 |
35 | ```python
36 | from collections import defaultdict
37 |
38 | # 키가 없을 때마다 '키: 0'을 반환하는 함수.
39 | def log_missing():
40 | print('Key added.')
41 | return 0
42 |
43 | current = {'green': 12, 'blue': 3}
44 | increments = [
45 | ('red', 5),
46 | ('blue', 17),
47 | ('white', 11),
48 | ]
49 |
50 | result = defaultdict(log_missing, current)
51 | print('before: ', dict(result))
52 |
53 | for key, amount in increments:
54 | result[key] += amount
55 |
56 | print('After: ', dict(result))
57 |
58 | before: {'green': 12, 'blue': 3}
59 | Key added.
60 | Key added.
61 | After: {'green': 12, 'blue': 20, 'red': 5, 'white': 11}
62 | ```
63 |
64 | `log_missing` 같은 함수를 넘기면 결정 동작과 부작용을 분리하므로 API를 쉽게 구축하고 테스트할 수 있다.
65 |
66 | 이번에는 기본값 후크를 `defaultdict`에 넘겨서 찾을 수 없는 키의 총 개수를 센다고 해보자.
67 | 이렇게 만드는 한 가지 방법은 상태 보존 클로저를 사용하는 것이다.
68 |
69 | ```python
70 | # 인자 current와 increments는 위 예제의 것을 사용.
71 |
72 | def increment_with_report(current, increments):
73 | added_count = 0
74 |
75 | def missing():
76 | nonlocal added_count
77 | added_count += 1
78 | return 0
79 |
80 | result = defaultdict(missing, current)
81 | for key, amount in increments:
82 | result[key] += amount
83 |
84 | return result, added_count
85 |
86 | result, count = increment_with_report(current, increments)
87 |
88 | assert count == 2
89 | ```
90 |
91 | `defualtdict`는 `missing`후크가 상태를 유지한다는 사실을 모르지만, `increment_with_report` 함수를 실행하면 튜플의 요소로 기대한 개수인 2를 얻는다.
92 |
93 |
94 |
95 |
96 | ## 3. 다른 방안: 클래스 사용하기
97 |
98 | 상태 보존용으로 `nonlocal`과 같은 클로저를 사용하면 상태가 없는 예제의 함수보다 이해하기 어렵다는 단점이 있다.
99 | 또 다른 방법은 보존할 상태를 캡슐화하는 작은 클래스를 정의하는 것이다.
100 |
101 | ```python
102 | class CountMissing:
103 | def __init__(self):
104 | self.added = 0
105 |
106 | def missing(self):
107 | self.added += 1
108 | return 0
109 | ```
110 |
111 | 다른 언어에서라면 이제 `CountMissing`의 인터페이스를 수용하도록 `defaultdict`를 수정해야 한다고 생각할 수 있다. 하지만 파이썬에서는 일급함수 덕분에 객체로 `CountMissing.missing` 메소드를 직접 참조해서 `defaultdict`의 기본값 후크로 넘길 수 있다.
112 |
113 | ```python
114 | counter = CountMissing()
115 | result = defaultdict(counter.missing, current)
116 |
117 | for key, amount in increments:
118 | result[key] += amount
119 |
120 | assert counter.added == 2
121 | ```
122 |
123 |
124 |
125 | 위의 예를 조금만 더 업그레이드해보자. 코드를 처음 보는 사람들은 **`counter`와 `missing` 두 개 이상의 상태를 파악해야 한다.** 상태의 개수는 복잡도와 관련이 있기 때문에 프로그램의 복잡도를 높이는 안 좋은 영향이 있다. 이때 `__call__`를 사용해서 가독성을 조금 더 높여보자.
126 |
127 | ```python
128 | class BetterCountMissing:
129 | def __init__(self):
130 | self.added = 0
131 |
132 | def __call__(self):
133 | print('added!')
134 | self.added += 1
135 | return 0
136 |
137 | counter = BetterCountMissing()
138 | result = defaultdict(counter, current)
139 |
140 | for key, amount in increments:
141 | result[key] += amount
142 |
143 | assert counter.added == 2
144 | ```
145 |
146 | \_\_call\_\_ 메소드는 객체를 함수처럼 호출할 수 있게 해준다. 또한 내장 함수 _callable_ 이 이런 인스턴스에 대해서는 True를 반환하게 만든다.
147 |
148 |
149 |
150 |
151 | ## 4. 핵심 정리
152 |
153 | * 파이썬에서 컴포넌트 사이의 간단한 인터페이스용으로 클래스를 정의하고 인스턴스를 생성하는 대신, 함수만 써도 종종 충분하다.
154 | * 파이썬에서 함수와 메소드에 대한 참조는 1급이다. 즉, 다른 타입처럼 표현식에서 사용할 수 있다.
155 | * \_\_call\_\_이라는 특별한 메소드는 클래스의 인스턴스를 일반 파이썬 함수처럼 호출할 수 있게 해준다.
156 | * 상태를 보존하는 함수가 필요할 때 상태 보존 클로저를 정의하는 대신, \_\_call\_\_ 메소드를 제공하는 클래스를 정의하는 방안을 고려하자.
157 |
--------------------------------------------------------------------------------
/files/BetterWay25_InitializeSuperClassWithSuper.md:
--------------------------------------------------------------------------------
1 | ## Better Way 25. super로 부모 클래스를 초기화하자
2 |
3 | #### 108쪽
4 |
5 | * Created : 2016/12/25
6 | * Modified: 2019/05/23
7 |
8 |
9 |
10 | ## 1. 파이썬 2의 상속의 단점 1: 생성자 메소드 실행 순서의 어려움
11 |
12 | 클래스 상속관계를 사용할 때 오래 전 파이썬에서는 자식 클래스에서 부모 클래스의 \_\_init\_\_ 생성자 메소드를 직접 호출하는 방법으로 부모 클래스를 초기화했다.
13 |
14 | ```python
15 | class MyBaseClass:
16 | def __init__(self, value):
17 | self.value = value
18 |
19 |
20 | class MyChildClass(MyBaseClass):
21 | def __init__(self):
22 | MyBaseClass.__init__(self, 5)
23 | ```
24 |
25 | 이 방법은 간단한 계층 구조에서는 잘 작동하지만 많은 경우 제대로 작동하지 않는다. 특히 클래스가 다중 상속(한 서브 클래스가 두 개 이상의 부모 클래스를 상속받는 상속)의 영향을 받는다면 슈퍼클래스의 \_\_init\_\_ 메소드를 직접 호출하는 행위는 예기치 못한 동작을 일으킬 수 있다.
26 |
27 | **초창기 파이썬 2의 클래스 동작의 한 가지 문제점은 다중 상속에서 \_\_init\_\_ 메소드의 호출 순서가 모든 서브 클래스에 걸쳐 명시되지 않았다는 점이다.** 예를 들어 인스턴스의 _value_ 필드로 연산을 수행하는 부모 클래스 두 개를 정의해보자.
28 |
29 |
30 | ```python
31 | class TimesTwo:
32 | def __init__(self):
33 | self.value *= 2
34 |
35 |
36 | class PlusFive:
37 | def __init__(self):
38 | self.value += 5
39 | ```
40 |
41 | 다음 클래스는 앞서 정의한 _MyBaseClass_ 와 방금 정의한 두 개의 클래스를 다중 상속받는 클래스를 정의한다. 상속 순서를 눈여겨보기 바란다.
42 |
43 | ```python
44 | class OneWay(MyBaseClass, TimesTwo, PlusFive):
45 | def __init__(self, value):
46 | MyBaseClass.__init__(self, value)
47 | TimesTwo.__init__(self)
48 | PlusFive.__init__(self)
49 | ```
50 |
51 | _OneWay_ 의 인스턴스를 만들면 _value_ 에 상속 순서와 일치하는 _value_ 값이 정의된다.
52 |
53 | ```python
54 | foo = OneWay(5)
55 | print("One way's ordering is (5 * 2) + 5 =", foo.value)
56 |
57 |
58 | One way's ordering is (5 * 2) + 5 = 15
59 | ```
60 |
61 | 5로 초기화한 값에 2를 곱하고 5를 더해 15가 출력됐다. 이는 상속순서와 생성자 메소드 호출순서와 일치한다. 그런데 만약 상속 순서를 다르게 한다면 어떻게 될까?
62 |
63 |
64 | ```python
65 | class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
66 | def __init__(self, value):
67 | MyBaseClass.__init__(self, value)
68 | TimesTwo.__init__(self)
69 | PlusFive.__init__(self)
70 | ```
71 |
72 | 새로운 클래스 _AnotherWay_ 를 선언했다. **이 클래스는 생성자 메소드 호출순서는 같지만, 상속순서를 다르게 해서 _value_ 에 5를 더하는 클래스를 먼저 상속받는다.**
73 |
74 | ```python
75 | bar = AnotherWay(5)
76 | print("Second way's result is still", bar.value)
77 |
78 |
79 | Second way's result is still 15
80 | ```
81 |
82 | 결과값은 여전히 15이다. 그러니까 결론은 **다중상속관계에서 생성자 호출 순서는 상속받는 순서와 상관없이 생성자 직접 호출 순서에 의해 결정된다는 것이다.** 이는 뭔과 직관적이지 않다. 이는 단순히 헷갈리는 문제이고 익숙해지면 괜찮다고 생각할 수도 있지만 더 큰 문제가 기다리고 있다.
83 |
84 |
85 |
86 |
87 | ## 2. 파이썬 2의 상속의 단점 2: 슈퍼클래스 생성자 함수의 중복 실행
88 |
89 | 다른 문제는 `다이아몬스 상속(diamond inheritance)`에서 찾을 수 있다. 다이아몬드 상속은 서브 클래스가 계층 구조에서 같은 슈퍼클래스를 둔 서로 다른 두 클래스를 상속받을 때 발생한다.
90 |
91 | 
92 |
93 | **다이아몬스 상속은 공통 슈퍼클래스의 \_\_init\_\_ 메소드를 여러 번 실행하게 해서 예상치 못한 동작을 일으킨다.** 예를 들어, _MyBaseClass_ 에서 상속받는 자식 클래스 두 개를 정의하자.
94 |
95 | ```python
96 | class TimesFive(MyBaseClass):
97 | def __init__(self, value):
98 | MyBaseClass.__init__(self, value)
99 | self.value *= 5
100 |
101 |
102 | class PlusTwo(MyBaseClass):
103 | def __init__(self, value):
104 | MyBaseClass.__init__(self, value)
105 | self.value += 2
106 | ```
107 |
108 | 이 클래스는 _value_ 에 5를 할당하는 _MyBaseClass_ 를 공통 상속하고 있다. 다음으로 두 클래스를 모두에서 상속받는 자식 클래스를 정의하여 _MyBaseClass_ 를 다이아몬드의 꼭대기로 만든다.
109 |
110 | ```python
111 | class ThirdWay(TimesFive, PlusTwo):
112 | def __init__(self, value):
113 | TimesFive.__init__(self, value)
114 | PlusTwo.__init__(self, value)
115 | ```
116 |
117 | 예제를 만들어보자.
118 |
119 |
120 | ```python
121 | foo = ThirdWay(5)
122 | print("Result should be (8 * 5) + 2 = 27, but it's", foo.value)
123 |
124 |
125 | Result should be (5 * 5) + 2 = 27, but it's 7
126 | ```
127 |
128 | 예상대로라면 초기화된 5에 5를 곱하고 2를 더해서 27이 나오리라고 예상할 수 있지만 7이라는 쌩뚱맞은 값이 나왔다. 왜 그럴까? 그 이유는 생각보다 싱겁다. 첫 번째 부모 클래스의 생성자가 호출되어 _MyBaseClass_ 가 한 번 실행된뒤, **두 번째 부모 클래스의 생성자 *PlusTwo.\_\_init\_\_* 를 호출하는 코드에서 *MyBaseClass.\_\_init\_\_* 가 두 번째 호출될 때 _self.value_ 가 5로 다시 리셋되기 때문이다.**
129 |
130 | 보통 이런 상황에서 공통 슈퍼클래스의 생성자 함수가 두 번 실행되는 것을 기대하지는 않는다.
131 |
132 |
133 |
134 |
135 | ## 3. 해결책: super 내장함수 사용
136 |
137 | 까마득한 예전에 **파이썬 2.2 부터는 위와 같은 문제들을 해결하기 위해 _super_ 내장함수를 추가하고 메소드 해석 순서(MRO, Method Resolution Order)를 정의했다. MRO는 다중 상속에서 상속 순서에 따라 어떤 슈퍼클래스부터 초기화하는지를 정한다. 그 순서는 '깊이 우선, 왼쪽에서 오른쪽으로'가 기본 원칙이다. 결정적으로 다이아몬드 계층 구조에 있는 공통 슈퍼클래스를 단 한 번만 실행하게 한다.**
138 |
139 | 다음 코드는 다이아몬드 클래스 구조지만 _super_ 로 부모 클래스를 초기화한다. 먼저 파이썬 2의 버전부터 살펴본다.
140 |
141 | ```python
142 | # 파이썬 2방식
143 | class TimesFiveCorrect(MyBaseClass):
144 | def __init__(self, value):
145 | super(TimesFiveCorrect, self).__init__(value)
146 | self.value *= 5
147 |
148 |
149 | class PlusTwoCorrect(MyBaseClass):
150 | def __init__(self, value):
151 | super(PlusTwoCorrect, self).__init__(value)
152 | self.value += 2
153 | ```
154 |
155 | 이 둘을 상속받는 클래스를 정의한다. 상속 순서를 눈여겨 보길 바란다.
156 |
157 | ```python
158 | class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
159 | def __init__(self, value):
160 | super(GoodWay, self).__init__(value)
161 |
162 |
163 | foo = GoodWay(5)
164 | print('Should be 5 * (5 + 2) = 35 and the result is', foo.value)
165 |
166 |
167 | Should be 5 * (5 + 2) = 35 and the result is 35
168 | ```
169 |
170 |
171 | 이 결과를 어떻게 이해해야 할까? 상술했듯이 바뀐 파이썬의 상속 정책, MRO에서는 초기화 순서가 '깊이 우선, 왼쪽에서 오른쪽으로' 방식으로 진행된다. **MRO의 동작 방식은 본질적으로 Stack과 유사하다. _GoodWay_ 부터 시작해서 상속받는 클래스의 왼쪽부터 스택에 집어넣는다. 이후 모든 클래스의 조상 _object_ 클래스까지 도달하면 스택에서 한 클래스씩 빼내서 클래스 생성자를 실행한다.** 그랬기 때문에 5를 먼저 초기화했고(_MyBaseClass_), 그 값에 2를 더했으며(_PluseTwoCorrect_), 5를 곱해(_TimesFiveCorrect_) 35라는 5와 7의 최소공배수가 나올 수 있었다.
172 |
173 | 또한 정확한 MRO 순서는 _mro_ 라는 클래스 메소드로 확인할 수 있다.
174 |
175 | ```python
176 | from pprint import pprint
177 |
178 | pprint(GoodWay.mro())
179 |
180 |
181 | [,
182 | ,
183 | ,
184 | ,
185 | ]
186 | ```
187 |
188 |
189 |
190 |
191 | ## 4. 파이썬 3에서의 super의 개선
192 |
193 | 앞서 파이썬 2.2에서 추가된 _super_ 함수의 사용법을 다시 살펴보자.
194 |
195 | ```python
196 | super(GoodWay, self).__init__(value)
197 | ```
198 |
199 | 이 함수는 끝내주게 잘 동작하지만 두 가지 문제점이 있다.
200 |
201 | * **문법이 장황하다.**
202 | - 현재는 함수를 호출할 때 정의하는 클래스, sef, \_\_init\_\_ 과 모든 인수를 설정해야 한다. 언제나 상태가 많아지면 복잡해지고 가독성이 떨어지기 마련이다.
203 | * **_super_ 를 호출하면서 현재 클래스의 이름을 지정해야 한다.**
204 | - 클래스의 이름을 변경하는 일은 클래스 계층 구조를 개선할 때 아주 흔히 하는 행동이다. 이때 _super_ 를 호출하는 모든 코드를 수정해야 하는 번거로움이 있다.
205 |
206 |
207 |
208 | 파이썬에서는 _super_ 의 문제점을 개선했다. 파이썬 3에서는 _super_ 를 인수 없이 호출하면 *\_\_class\_\_* 와 _self_ 를 인수로 넘겨서 호출한 것으로(default 값으로 설정해서) 처리해서 이 문제를 해결한다. 파이썬 3에서는 항상 _super_ 를 사용해야 한다. 이 내장함수는 명확하고 간결하며 항상 제대로 동작한다.
209 |
210 |
211 | ```python
212 | class ExplicitOne(MyBaseClass):
213 | def __init__(self, value):
214 | super(__class__, self).__init__(value * 2)
215 |
216 |
217 | class ImplicitOne(MyBaseClass):
218 | def __init__(self, value):
219 | super().__init__(value * 2)
220 |
221 |
222 | assert ExplicitOne(10).value == ImplicitOne(10).value
223 |
224 |
225 | ```
226 |
227 |
228 |
229 | ## 5. 핵심 정리
230 |
231 | * 파이썬의 표준 메소드 해석 순서(MRO)는 슈퍼클래스의 초기화 순서와 다이아몬드 상속 문제를 해결한다.
232 | * 항상 내장함수 _super_ 로 부모 클래스를 초기화하자.
233 |
--------------------------------------------------------------------------------
/files/BetterWay27_UsePrivateAttribute.md:
--------------------------------------------------------------------------------
1 | ## Better Way 27. 공개 속성보다는 비공개 속성을 사용하자
2 |
3 | #### 119쪽
4 |
5 | * Created : 2017/01/05
6 | * Modified: 2019/05/27
7 |
8 |
9 |
10 |
11 | ## 1. 파이썬 클래스에서의 비공개 속성
12 |
13 |
14 | 자바를 해본 사람들이라면 자바에서 클래스의 가시성이 'public', 'default', 'package', 'private'와 같이 비교적 세분화된 것을 기억할 것이다. 반면 **파이썬에서는 클래스 속성의 가시성이 공개(public)와 비공개(private) 두 유형밖에 없다.**
15 |
16 | 공개는 일반 변수처럼 이름 짓고, **비공개 속성은 이름 앞에 `__`를 붙인다.**
17 |
18 |
19 | ```python
20 | class MyObject:
21 | def __init__(self):
22 | self.public = 5
23 | self.__private = 10
24 |
25 | def get_private_field(self):
26 | return self.__private
27 |
28 | @classmethod
29 | def get_private_field_of_instance(cls, instance):
30 | return instance.__private
31 | ```
32 |
33 |
34 | 공개 속성은 어디서든 객체에 `.`을 사용해 접근할 수 있다.
35 |
36 | ```python
37 | foo = MyObject()
38 | assert foo.public == 5
39 | ```
40 |
41 | 반면 **비공개 속성은 클래스 외부에서 `.`을 사용해 직접 접근하면 예외가 일어난다.**
42 |
43 | ```python
44 | assert foo.__private == 10
45 |
46 | AttributeError: 'MyObject' object has no attribute '__private'
47 | ```
48 |
49 | 그렇지만 같은 클래스에 속한 메서드는 비공개 속성에 직접 접근할 수 있다.
50 |
51 | ```python
52 | assert foo.get_private_field() == 10
53 | ```
54 |
55 | 클래스 메소드도 같은 class 블록에 선언되어 있으므로 비공개 속성에 접근할 수 있다.
56 |
57 | ```python
58 | assert MyObject.get_private_field_of_instance(foo) == 10
59 | ```
60 |
61 |
62 |
63 |
64 | 또한 비공개 필드의 주요한 특징으로 **서브클래스에서는 부모클래스의 비공개 필드에 직접 접근할 수 없다.**
65 |
66 | ```python
67 | class MyParent:
68 | def __init__(self):
69 | self.__private_field = 71
70 |
71 | class MyChild(MyParent):
72 | def get_private_field(self):
73 | return self.__private_field
74 |
75 |
76 | baz = MyChild()
77 | baz.get_private_field()
78 |
79 | AttributeError: 'MyChild' object has no attribute '_MyChild__private_field'
80 | ```
81 |
82 | 이 이유는 **비공개 속성의 동작의 독특한 방식 때문인데, 클래스에 비공개 속성을 할당하면 외부에 변수를 노출시키지 않는 대단한 로직이 있는 것이 아니라 단순히 비공개 속성은 속성 이름을 변환해 할당되는 방식으로 구현되기 때문이다.**
83 |
84 | 파이썬 컴파일러는 _MyChild.get\_private\_field_ 같은 메소드에서 비공개 속성에 접근하는 코드를 발견하면 *\_\_private\_field*를 _\_MyChild\_\_private\_field_ 에 접근하는 코드로 변환한다. 이 예제에서는 *\_\_private\_field* 가 *MyParent.\_\_init\_\_* 에만 정의되어 있으므로 비공개 속성의 실제 이름은 _\_MyParent\_\_private\_field_ 가 된다. **자식클래스에서 부모의 비공개 속성에 접근하는 동작은 단순히 변환된 속성 이름이 일치하지 않아서 실패하는 것이다.**
85 |
86 | 이 체계를 이해하면 공개, 비공개 등 접근 권한을 확인하지 않고서도 서브클래스나 외부 클래스에서 어떤 클래스의 비공개 속성이든 쉽게 접근할 수 있다.
87 |
88 | ```python
89 | assert baz._MyParent__private_field == 71
90 | ```
91 |
92 | _baz_ 는 자식클래스(_MyChild_)의 인스턴스임에도 부모클래스에서 정의한 비공개 속성에 접근하는 데 문제가 없다. 바뀐 이름으로, 공개 속성으로 할당된 것이라고 봐도 된다. 추가로 객체의 속성 딕셔너리를 들여다보면 실제로 비공개 속성이 변환 후의 이름으로 저장되어 있음을 알 수 있다.
93 |
94 |
95 |
96 | ```python
97 | print(baz.__dict__)
98 |
99 | {'_MyParent__private_field': 71}
100 | ```
101 |
102 | **왜 파이썬에서는 다른 고급 언어들과 달리 가시성을 엄격하게 강제하지 않을까? 가장 간단한 답은 파이썬에서 자주 인용되는 "우리 모두 성인이라는 사실에 동의합니다"라는 좌우명에 있다.** 파이썬 프로그래머들은 개방으로 얻는 장점이 폐쇄로 얻는 단점보다 크다고 믿는다.
103 |
104 | 파이썬의 이런 특징은 이외에도 속성에 접근하는 것처럼 언어 기능을 가로채는 기능을 사용해 마음만 먹으면 언제든지 객체의 내부를 조작할 수 있다. 우리가 이전 장에서 _setattr_ 함수로 객체에 속성을 동적으로 할당한 것을 예로 들 수 있겠다.
105 |
106 |
107 |
108 |
109 | ## 2. 비공개 속성 사용의 문제점
110 |
111 | 그러면 완전히 다른 질문으로 '**이럴거면 애초에 엉성한 비공개 속성 자체를 두는 이유가 뭐냐**'고 물을 수 있다. 누구나 뚫을 수 있는 비공개면 차라리 그냥 다 공개 속성으로 두지 그래? 충분히 합리적인 질문으로 비공개 속성을 두기라도 두는 것이 어떤 가치가 있을까?
112 |
113 | 과유불급이라고, **무분별하게 객체의 내부에 접근하는 것에도 위험이 따른다.** 파이썬 프로그래머들은 이 위험을 최소화하기 위해 스타일가이드에 정의된 명명 관례를 따른다. **\_protected\_field 처럼 앞에 `_`를 한 개 붙인 필드는 보호 필드로 취급해서 클래스의 외부 사용자들이 신중하게 다뤄야 함을 의미한다.**
114 |
115 | 하지만 파이썬을 처음 접하는 많은 프로그래머가 서브클래스나 외부에서 접근하면 안 되는 내부 API를 비공개 필드로 나타낸다.
116 |
117 | ```python
118 | class MyClass:
119 | def __init__(self, value):
120 | self.__value = value
121 |
122 | def get_value(self):
123 | return str(self.__value)
124 |
125 | foo = MyClass(5)
126 | assert foo.get_value() == '5'
127 | ```
128 |
129 | 이 접근방식은 잘못 되었다. 여러분을 포함해 누군가는 클래스에 새 동작을 추가하거나 기존 메소드의 결함을 해결하기 위해서(위 예제에서는 _get\_value_ 메소드가 항상 문자열을 반환하도록 한다) 서브클래스를 만들기 마련이다. **비공개 속성을 선택하면 서브클래스의 오버라이드(override)와 학장(extension)을 다루기 어렵고 불안정하게 만든다.** 게다가 나중에 만들 서브클래스에서 꼭 필요하면 여전히 비공개 필드에 접근할 수 있다.
130 |
131 |
132 | ```python
133 | class MyIntegerSubclass(MyClass):
134 | def get_value(self):
135 | return int(self._MyClass__value)
136 |
137 | foo = MyIntegerSubclass(5)
138 | assert foo.get_value() == 5
139 | ```
140 |
141 | 지금이야 동작하지만 **나중에 클래스의 계층이 변경되면 MyIntegerSubclass 같은 클래스는 비공개 참조가 더는 유효하지 않게 되어 동작하지 않을 수 있다.** 이 클래스가 나중에 다른 클래스를 상속받게 변경될지 누가 알 수 있겠는가?
142 |
143 | 예를 들어서 _MyIntegerSubclass_ 의 직계 부모인 _MyClass_ 에 _MyBaseClass_ 라는 또 다른 부모 클래스를 추가했다고 하자.
144 |
145 |
146 | ```python
147 | class MyBaseClass:
148 | def __init__(self, value):
149 | self.__value = value
150 | # ...
151 |
152 | class MyClass(MyBaseClass):
153 | pass
154 |
155 |
156 | class MyIntegerSubclass(MyClass):
157 | def __init__(self, value):
158 | self.value = value
159 |
160 | def get_value(self):
161 | return int(self._MyClass__value)
162 |
163 |
164 | foo = MyIntegerSubclass(5)
165 | foo.get_value()
166 |
167 | AttributeError: 'MyIntegerSubclass' object has no attribute '_MyClass__value'
168 | ```
169 |
170 | 바뀐 상속관계 속에서 *\_\_value* 속성을 _MyClass_ 가 아닌 _MyBaseClass_ 에서 할당한다. 그러면 처음 사용했던 _MyIntegerSubclass_ 의 비공개 변수 참조인 _self.\_MyClass\_\_value_ 가 이제는 존재하지 않기 때문에 동작하지 않는다.
171 |
172 |
173 |
174 |
175 | ## 3. 해결책: 보호 속성을 사용하자.
176 |
177 | 일반적으로 **비공개가 아닌 보호 속성을 사용해서 서브클래스가 더 많은 일을 할 수 있게 하는 편이 낫다.** 각각의 보호 필드를 문서화해서 서브클래스에서 내부 API 중 어느 것을 쓸 수 있고 어느 것을 그대로 둬야하는지 설명하자. 이렇게 하면 자신이 작성한 코드를 미래에 안전하게 확장하는 지침이 되는 것처럼 다른 프로그래머에게도 조언이 된다.
178 |
179 | ```python
180 | class MyClass:
181 | def __init__(self, value):
182 | # 객체를 표현하는 값을 설정
183 | # 문자열로 강제할 수 있는 값이어야 하며(__str__ 구현)
184 | # 객체에 할당하고 나면 불변으로 취급해야 한다.
185 | self._value = value
186 | ```
187 |
188 | 반대로 **비공개 속성을 사용할지 진지하게 고민할 시점은 서브클래스와 이름이 충돌할 염려가 있을 때뿐이다.** 이 문제는 자식클래스가 부지불식간에 부모클래스에서 이미 정의한 속성을 정의할 때 일어난다.
189 |
190 |
191 | ```python
192 | class ApiClass:
193 | def __init__(self):
194 | self._value = 5
195 |
196 | def get(self):
197 | return self._value
198 |
199 | class Child(ApiClass):
200 | def __init__(self):
201 | super().__init__()
202 | self._value = 'hello' # _value 속성이 겹침!
203 |
204 |
205 | c = Child()
206 | print(c.get(), 'and', c._value, 'should be different')
207 |
208 | hello and hello should be different
209 | ```
210 |
211 | 이런 문제는 주로 클래스가 공개 API의 일부일 때 문제가 되며 특히 속성 이름이 value 처럼 아주 일반적일 때 일어날 확률이 특히 높다. 이런 상황이 일어날 위험을 줄이려면 부모 클래스에서 비공개 속성을 사용해서 자식 클래스와 속성 이름이 겹치지 않게 하면 된다.
212 |
213 | ```python
214 | class ApiClass:
215 | def __init__(self):
216 | self.__value = 5
217 | # 이전과 달리 비공개로 설정해 자식클래스 속성과의 충돌방지!
218 |
219 | def get(self):
220 | return self.__value
221 |
222 |
223 | class Child(ApiClass):
224 | def __init__(self):
225 | super().__init__()
226 | self._value = 'hello' # 겹치지 않음!
227 |
228 | c = Child()
229 | print(c.get(), 'and', c._value, 'should be different')
230 |
231 | 5 and hello should be different
232 | ```
233 |
234 |
235 |
236 | ## 4. 핵심 정리
237 |
238 | * 파이썬 컴파일러는 비공개 속성을 엄격하게 강요하지 않는다.
239 | * 서브클래스가 내부 API와 속성에 접근하지 못하게 막기보다는 처음부터 내부 API와 속성으로 더 많은 일을 할 수 있게 하자.
240 | * 비공개 속성에 대한 접근을 강제로 제어하지 말고 보호 필드를 문서화해서 서브클래스에 필요한 지침을 제공하자.
241 | * 직접 제어할 수 없는 서브클래스와 이름이 충돌하지 않게 할 때만 비공개 속성을 사용하고, 웬만하면 보호 속성을 사용하자.
242 |
--------------------------------------------------------------------------------
/files/BetterWay28_CustomContainer_collections.abc.md:
--------------------------------------------------------------------------------
1 | ## Better Way 28. 커스텀 컨테이너 타입은 collections.abc의 클래스를 상속받게 만들자
2 |
3 | #### 126쪽
4 |
5 | * Created : 2017/01/04
6 | * Modified: 2019/05/29
7 |
8 |
9 |
10 | ## 1. 컨테이너 상속의 쉬운 예: list
11 |
12 | 파이썬 프로그래밍의 핵심은 데이터를 담은 클래스를 정의하고 이 객체들이 연계되는 방법을 명시하는 일이다. 모든 파이썬 클래스는 일종의 컨테이너로, 속성과 기능을 함께 캡슐화한다. 파이썬은 데이터 관리용 내장 컨테이너 타입(list, tuple, set, dict)도 제공한다.
13 |
14 | **Sequence처럼 쓰임새가 간단한 클래스를 설계할 때는 파이썬의 내장 list 타입에서 상속받으려고 하는 게 당연하다.** 멤버의 빈도를 세는 메소드를 추가로 갖춘 커스텀 list 타입을 생성한다고 해보자.(Sequence는 list, tuple 등 많은 선형 자료구조의 부모 클래스가 되는 추상클래스다.)
15 |
16 |
17 | ```python
18 | class FrequencyList(list):
19 | def __init__(self, members=None):
20 | # list의 생성자는 iterable을 요구하기 때문에
21 | # 새 리스트에 인자가 주어지지 않으면 강제로 빈 리스트를 인자로 넘김
22 | # help(list) 참고
23 | if members is None:
24 | members = []
25 | super().__init__(members)
26 |
27 | def frequency(self):
28 | counts = {}
29 | for item in self: # 주목! self를 바로 받아 아이템에 접근할 수 있다.
30 | counts.setdefault(item, 0)
31 | counts[item] += 1
32 | return counts
33 | ```
34 |
35 | _FrequencyList_ 는 list를 상속받아 리스트 본연의 기능은 모두 수행하면서, 빈도를 세는 _frequency_ 라는 메소드가 추가된 상황이다. **list에서 상속받아 서브클래스를 만들었으므로 list의 표준 기능을 모두 갖춰서 파이썬의 익숙한 시맨틱을 유지한다. 추가한 메소드로 필요한 커스텀 동작을 더할 수 있다.**
36 |
37 | 한번 테스트해보자.
38 |
39 | ```python
40 | import random
41 | from string import ascii_lowercase as LOWERS
42 |
43 |
44 | foo = FrequencyList()
45 |
46 | for _ in range(10):
47 | foo.append(random.choice(LOWERS))
48 |
49 |
50 | print("First element of foo is", foo[0])
51 | print("Frequency of foo is", foo.frequency())
52 |
53 |
54 | First element of foo is h
55 | Frequency of foo is {'h': 1, 'i': 2, 'o': 1, 'y': 1, 'f': 1, 'u': 1, 'z': 1, 'p': 1, 'x': 1}
56 | # 구체적인 결과는 매 사용마다 변할 것임!
57 | ```
58 |
59 | _FrequencyList_ 는 list를 상속받았기 때문에 list가 지원하는 append 메소드와 인덱싱이 모두 가능하다. 거기에 더해 우리가 추가한 _frequency_ 메소드 또한 무난히 잘 작동하는 것을 알 수 있었다.
60 |
61 |
62 |
63 |
64 | ## 2. 컨테이너 상속의 보다 어려운 예
65 |
66 | list 상속만으로도 현실에서 필요한 많은 작업을 무난하게 시행할 수 있다. 하지만 더 복잡한 상황을 가정해보자. 선형 자료구조인 list의 서브 클래스는 아니지만 인덱스로 접근할 수 있게 해서 list처럼 보이는 객체를 제공하고 싶다고 하자.
67 |
68 | 예를 들어 비선형 자료구조인 Binary Tree 클래스에 list나 tuple 같은 시퀀스 시맨틱을 제공하고 싶다고 하자. Binary Tree는 트리 자료구조에서 각 노드가 자식을 최대 왼쪽, 오른쪽 단 두 개만 갖는 자료구조로서 원소 추가, 삭제 동작에서 list보다 더 높은 효율을 보일 수 있다.
69 |
70 | ```python
71 | class BinaryNode:
72 | def __init__(self, value, left=None, right=None):
73 | self.value = value
74 | self.left = left
75 | self.right = right
76 | ```
77 |
78 | 이 클래스가 Sequence처럼 동작하게 하려면 어떻게 해야 할까? 알다시피 파이썬은 특별한 이름을 붙인 인스턴스 메소드로 컨테이너 동작을 구현한다.
79 |
80 | list의 인덱싱을 예로 들어보자.
81 |
82 | ```python
83 | bar = [1, 2, 3]
84 | bar[0]
85 | ```
86 |
87 | list의 첫 번째 원소를 구하는 간단한 인덱싱이다. 하지만 사실 이는 내부적으로 다음과 같은 메소드를 호출하는 문법이다.
88 |
89 | ```python
90 | bar.__getitem__(0)
91 | ```
92 |
93 | 따라서, BinaryNode 클래스가 Sequence처럼 인덱싱을 지원하게 하려면 객체의 트리를 깊이 우선으로 탐색하는 \_\_getitem\_\_ 을 구현하면 된다.
94 |
95 | ```python
96 | class IndexableNode(BinaryNode):
97 | def _search(self, count, index):
98 | # ...
99 | # 비선형 트리를 선형으로 직렬화했을 때의 인덱스 찾기
100 | # (found, count) 반환
101 |
102 | def __getitem__(self, index):
103 | found, _ = self._search(0, index)
104 | if not found:
105 | raise IndexError("Index out of range")
106 | return found.value
107 | ```
108 |
109 | 이 바이너리 트리는 평소처럼 생성하면 된다.
110 |
111 | ```python
112 | tree = IndexableNode(
113 | 10,
114 | left=IndexableNode(
115 | 5,
116 | left=IndexableNode(2),
117 | right=IndexableNode(6, right=IndexableNode(7))
118 | ),
119 | right=IndexableNode(15, left=IndexableNode(11))
120 | )
121 | ```
122 |
123 | 트리는 Sequence와 달리 비선형 자료구조임에도 탐색은 물론이고 list처럼 인덱스로 접근할 수도 있다.
124 |
125 | ```python
126 | print('LRR =', tree.left.right.right.value)
127 | print('Index 0 =', tree[0])
128 | print('Index 1 =', tree[1])
129 | print('11 in the tree?', 11 in tree)
130 | print('17 in the tree?', 17 in tree)
131 | print('Tree is', list(tree))
132 |
133 | LRR = 7
134 | Index 0 = 2
135 | Index 1 = 5
136 | 11 in the tree? True
137 | 17 in the tree? False
138 | Tree is [2, 5, 6, 7, 10, 11, 15]
139 | ```
140 |
141 |
142 |
143 | ## 3. 위 방법의 문제점과 해결책
144 |
145 | 위와 같이 특정 메소드를 정의함으로써 Sequence 시맨틱을 지원할 수 있었다. 하지만 이 방법은 문제가 있는데 **Sequence 시맨틱에서 사용자가 기대하는 메소드는 인덱싱이 다가 아니며, _count_, _index_ 등의 메소드 등이 더 필요하기 때문이다.** 그러면 이 메소드들을 다 정의해줘야 하는 것인가? 커스텀 컨테이너 타입을 정의하는 일은 보기보다 어렵다.
146 |
147 | ```python
148 | # __len__ 메소드 또한 시퀀스에서 사용자들이 흔히 시행할 수 있는 작업이다.
149 | len(tree)
150 |
151 | TypeError: object of type 'IndexableNode' has no len()
152 | ```
153 |
154 | 파이썬 세계의 이런 어려움을 피하기 위해 **내장 collections.abc 모듈은 각 컨테이너 타입에 필요한 일반적인 메소드를 모두 명시하는 추상 기반 클래스들을 정의한다.** 관련 [문서](https://docs.python.org/3/library/collections.abc.html)는 여기서 확인할 수 있다. 이 모듈은 list, tuple, set, dict 들과는 다른 방식의 메소드를 지원하기에 이들을 상속받는 것이 바람직하지 않은 커스텀 컨테이너를 지원하기 위해 있으며 여러 많은 컨테이너 타입에 필요한 일반적인 메소드를 모두 정의하는 추상 기반 클래스를 정의한다.(`abc`가 원래 'abstract base class'의 약자이다.)
155 |
156 | **이 추상 기반 클래스에서 상속받아 서브클래스를 만들면, 만약 깜박 잊고 필수 메소드를 구현하지 않아도 파이썬이 뭔가 잘못되었다고 알려준다.**
157 |
158 | ```python
159 | from collections.abc import Sequence
160 |
161 | class BadType(Sequence):
162 | pass
163 |
164 | foo = BadType()
165 |
166 | TypeError: Can't instantiate abstract class BadType with abstract methods __getitem__, __len__
167 | ```
168 |
169 | 이 모듈 안에 있는 **_Sequence_ 추상클래스는 Sequence 시맨틱에 요구되는 필수 메소드들을 구현하지 않은 채 정의만하고 있는데, 이 _Sequence_ 를 상속하는 클래스를 정의한다는 것은 정의하는 클래스가 Sequence 시맨틱을 모두 지원하겠다는 선언을 하는 것이다.** 따라서 이 메소드들을 하나라도 구현하지 않으면 시맨틱이 충족되지 않았다고 파이썬에서 알려주기 때문에 요구되는 시맨틱을 모두 지원하는지 놓치지 않을 수 있다. 추가로 Sequence에 요구되는 필수 추상 메소드를 구현하면 별도로 작업하지 않아도 Sequence가 지원하는 index와 count 같은 부가적인 메소드를 자동으로 제공한다.
170 |
171 | Set과 MutableMapping처럼 파이썬의 관례에 맞춰 구현해야 하는 특별한 메소드가 많은 더 복잡한 타입을 정의할 때 이런 추상 기반 클래스를 사용하는 이점은 더욱 커진다.
172 |
173 |
174 |
175 | ## 4. 핵심정리
176 |
177 | * 쓰임새가 간단할 때는 list, dict와 같은 파이썬의 컨테이너 타입에서 직접 상속받게 하자.
178 | * 커스텀 컨테이너 타입을 올바르게 구현하는 데 필요한 많은 메소드에 주의를 기울여야 한다.
179 | * 커스텀 컨테이터 타입이 collections.abc에 정의된 인터페이스 클래스를 상속받게 만들어서 클래스가 필요한 인터페이스, 동작과 일치하게 하자.
180 |
--------------------------------------------------------------------------------
/files/BetterWay29_dontusegetter.md:
--------------------------------------------------------------------------------
1 | ## Better Way 29. 게터와 세터 메소드 대신에 일반 속성을 사용하자
2 |
3 | #### 134쪽
4 |
5 | * Created : 2017/01/10
6 | * Modified: 2019/05/30
7 |
8 |
9 |
10 | ## 1. 타 언어의 getter와 setter
11 |
12 |
13 | 자바 수업을 듣던 때가 생각난다. 자바에서는 클래스의 속성에 접근하고 설정하는 getter(이하 '게터'), setter(이하 '세터') 메소드를 작성하는데, 자바의 대표적인 IDE 이클립스에서는 게터와 세터 메소드를 자동 완성하는 기능까지 지원한다.
14 |
15 | 그래서 자바 등의 타 언어를 공부한 사람들은 파이썬에서 클래스를 정의할 때도 흔히 게터와 세터를 지정하고는 한다.
16 |
17 | ```python
18 | class OldWay:
19 | def __init__(self, ohms):
20 | # 'ohms'는 전기저항의 단위로서 'Ω' 기호를 사용함. 기억나지? ㅋㅋ
21 | self._ohms = ohms
22 |
23 | def get_ohms(self):
24 | return self._ohms
25 |
26 | def set_ohms(self, ohms):
27 | self._ohms = ohms
28 |
29 |
30 | r0 = OldWay(30)
31 | assert r0.get_ohms() == 30
32 | r0.set_ohms(10)
33 | assert r0.get_ohms() == 10
34 | ```
35 |
36 | 타 언어 사용자들에게는 익숙할 수 있지만 Pythonic 하지는 않다. 게터와 세터 메소드는 즉석에서 값 변화시키기 같은 연산에서 사용하기 불편하다.
37 |
38 | ```python
39 | r0.set_ohms(r0.get_ohms() + 5)
40 | ```
41 |
42 | 옴 값을 단순히 5 증가시키려는데 함수 호출이 두 번이나 필요하다.
43 |
44 |
45 |
46 | 객체의 속성에 접근하는 이와 같은 유틸리티 메소드는 클래스의 인터페이스를 정의하는 데 도움이 되고, 기능을 캡슐화하고 사용법을 검증하고 경계를 정의하기 쉽게 해준다.
47 |
48 | **이런 요소는 클래스가 시간이 지나면서 발전하더라도 호출하는 쪽 코드를 절대 망가뜨리지 않도록 설계할 때부터 중요한 목표가 된다.** 즉 설계할 때부터 확장성을 고려해야 한다는 것과도 대응된다.
49 |
50 | 하지만 **파이썬에서는 이런 게터, 세터를 굳이 만들 이유가 없다.** 대신 항상 간단한 공개 속성부터 구현하기 시작해야 한다.
51 |
52 |
53 | ```python
54 | class Registor:
55 | def __init__(self, ohms):
56 | self.ohms = ohms
57 | self.voltage = 0
58 | self.current = 0
59 |
60 | r1 = Registor(400)
61 | r1.ohms = 10e3
62 |
63 | # 이렇게 하면 즉석에서 값 증가시키기 같은 연산이 자연스럽고 명확해진다.
64 | r1.ohms += 1e3
65 | ```
66 |
67 |
68 |
69 | ## 2. 파이썬의 게터와 세터: @property와 .setter
70 |
71 | 그런데 파이썬에서도 자바처럼 게터와 세터의 대체가 필요한 경우가 생길 수 있다. **나중에 속성을 설정할 때 특별한 동작이 추가로 일어나야 한다면, @property 데코레이터와 이에 해당하는 setter 속성을 사용하는 방법으로 바꿀 수 있다.**
72 |
73 | 여기서는 앞서 정의한 _Registor_ 의 서브클래스를 정의하며 voltage(전압) 프로퍼티를 할당하면 current(현재 전류) 값이 바뀌게 해본다.
74 |
75 | **제대로 동작하려면 세터와 게터 메소드의 이름이 의도한 프로퍼티 이름과 일치해야 한다.**
76 |
77 | ```python
78 | class VoltageRegistor(Registor):
79 | def __init__(self, ohms):
80 | super().__init__(ohms)
81 | self._voltage = 0
82 |
83 | @property
84 | def voltage(self):
85 | return self._voltage
86 |
87 | @voltage.setter
88 | def voltage(self, voltage):
89 | self._voltage = voltage
90 | self.current = self._voltage / self.ohms
91 | # 전류는 '전압 / 저항'과 같다고 합니다...
92 | ```
93 |
94 | @property가 뭔지 살펴보자. '@'는 데코레이터의 의미로서 나중에 더 살펴본다. **@property를 쓰면 voltage라는 메소드에 '속성'처럼 접근하고 평가값을 구할 수 있다.** 즉 'object.volatage' 처럼 속성처럼 호출하고 그 값을 받아올 수 있다.
95 |
96 | 두 번째로 **_@voltage.setter_ 는 말 그대로 세터처럼 값을 설정하는 것에 더해 근본적으로 함수이기 때문에 추가적인 동작을 넣을 수 있다.** 'object.voltage = 3'처럼 입력하면
97 |
98 | 1. _\_voltage_ 값이 3으로 설정되고
99 | 1. _current_ 라는 전류 값이 그에 맞춰 설정된다.
100 |
101 |
102 | ```python
103 | r2 = VoltageRegistor(100)
104 | assert r2.voltage == 0
105 | r2.voltage = 500
106 | assert r2.voltage == 500
107 |
108 | # voltage가 메소드임에도 속성이나 변수처럼 접근하고 있다.
109 | ```
110 |
111 |
112 |
113 | 다음으로 **_@property_ 에 대응되는 _setter_ 를 설정하면 클래스에 전달된 값들의 타입을 체크하고 값을 검증할 수 있다.** 다음은 모든 저항값이 0 옴보다 큼을 보장하는 클래스를 정의한 것이다.
114 |
115 | ```python
116 | class BoundedRegistance(Registor):
117 | def __init__(self, ohms):
118 | super().__init__(ohms)
119 |
120 | @property
121 | def ohms(self):
122 | return self._ohms
123 |
124 | @ohms.setter
125 | def ohms(self, ohms):
126 | if ohms <= 0:
127 | raise ValueError("Ohms must be over 0.")
128 | self._ohms = ohms
129 | ```
130 |
131 | 지금껏 난 특정한 하나의 값을 검증할 때 __init__과 다른 메소드들에서
132 | 여러 번 검증식을 써야 했다. 하지만 이렇게 하면...
133 |
134 | ```python
135 | r3 = BoundedRegistance(1e3)
136 |
137 |
138 | r3.ohms = 0
139 | ValueError: Ohms must be over 0.
140 |
141 | r4 = BoundedRegistance(-5)
142 | ValueError: Ohms must be over 0.
143 | ```
144 |
145 | 옴이 0 이하인 경우에는 _ValueError_ 를 반환한다. 그런데 신기하게도 **_r4_ 처럼 @setter가 아닌 생성자에 음의 옴 값을 넣어줘도 에러가 발생한다.** 분명 저항 검증식은 인스턴스 생성자가 아닌 _ohms_ 세터 메소드에 넣었는데 말이다. 이게 참 재미있는 일이다. **생성자를 호출하면 부모클래스인 _Registor_ 의 생성자가 호출된다. 이때 _ohms_ 가 전달되는데 생성자 코드 안에 옴을 할당하는 식이 있고, 이 할당식이 자식클래스의 ohms 세터 메소드를 호출한다.**
146 |
147 |
148 |
149 |
150 | 또한 **부모클래스의 속성을 불변으로 만드는 데도 @property를 사용할 수 있다.**
151 |
152 | ```python
153 | class FixedRegistance(Registor):
154 | def __init__(self, ohms):
155 | super().__init__(ohms)
156 |
157 | @property
158 | def ohms(self):
159 | return self._ohms
160 |
161 | @ohms.setter
162 | def ohms(self, ohms):
163 | if hasattr(self, 'ohms'):
164 | raise AttributeError("Can't set attribute")
165 | self._ohms = ohms
166 |
167 | r4 = FixedRegistance(1000)
168 | r4.ohms = 2e3
169 |
170 |
171 | AttributeError: Can't set attribute
172 | ```
173 |
174 | _FixedRegistance_ 클래스에서는 생성자에서 _ohms_ 를 할당한다. 그리고 다음 할당이 있을 때마다 객체는 _ohms_ 속성을 갖고 있기 때문에 전부 _AttributeError_ 를 반환하게 된다. 즉 **세터를 통해 속성 싱글턴(Singleton) 디자인 패턴을 구현한 것이다.**
175 |
176 |
177 |
178 |
179 | ## 3. @property의 단점.
180 |
181 | **@property의 가장 큰 단점은 속성에 대응하는 메소드를 서브클래스에서만 공유할 수 있다는 점이다.** 서로 관련이 없는 클래스는 같은 구현을 공유하지 못한다.
182 |
183 | **하지만 파이썬은 재사용 가능한 프로퍼티 로직을 비롯해 다른 많은 쓰임새를 가능하게 하는 디스크립터(descriptor)도 지원한다. 31장에서 배운다.**
184 |
185 | 마지막으로 **@property 메소드로 게터와 세터를 구현할 때 지나치게 복잡하거나 예상과 다르게 동작하지 않게 해야 한다.** 예를 들면 게터 프로퍼티 메소드에서 다른 속성을 설정하지 말아야 한다.
186 |
187 | ```python
188 | class MysteriousRegistor(Registor):
189 | def __init__(self, ohms):
190 | self._ohms = ohms
191 | self.voltage = 0
192 |
193 | @property
194 | def ohms(self):
195 | self.voltage = self._ohms * self.current
196 | return self._ohms
197 | ```
198 |
199 | 이와 같은 코드는 아주 이상한 동작을 만들어낸다.
200 |
201 | ```python
202 | r7 = MysteriousRegistor(10)
203 | r7.current = 0.01
204 | assert r7.voltage == 0
205 | r7.ohms
206 | assert r7.voltage == 0.1
207 | ```
208 |
209 | 이 코드의 문제점은 우리는 분명 ohms을 건드렸는데 쌩뚱맞게 voltage 속성값이 변화를 겪었다는 점이다. 일반적으로 게터와 세터를 쓰는 유저는 이런 일반적이지 않고, 예측하기 힘든 결과를 바라지 않는다.
210 |
211 | 이런 경우와 더불어 게터와 세터에서
212 |
213 | 1. 모듈을 동적으로 import하거나(\_\_import\_\_, importlib.import\_module),
214 | 1. 느린 헬퍼 함수를 실행하거나,
215 | 1. 비용이 많이 드는 데이터베이스 쿼리를 수행하는 일
216 |
217 | 처럼 일반적으로 예상하지 않은 복잡하거나 부작용을 유발할 수 있는 동작은 피해야 한다.
218 |
219 | **파이썬 사용자는 다른 언어와 마찬가지로 클래스 속성이 빠르고 쉬울 것이라고 예상할 것이다. 더 복잡하거나 느린 작업은 일반 메소드로 하자.**
220 |
221 |
222 |
223 |
224 | ## 4. 핵심 정리
225 |
226 | * 간단한 공개 속성을 사용하여 인스턴스를 정의하고 타 언어의 게터와 세터 메소드는 사용하지 말자.
227 | * 객체의 속성에 접근할 때 특별한 동작을 정의하려면 @property를 사용하자.
228 | * @property 메소드에서 최소 놀람 규칙(rule of least surprise)를 따르고 이상한 부작용은 피하자.
229 | * @property 메소드가 빠르게 동작하도록 만들자. 느리거나 복잡한 작업은 일반 메소드로 하자.
230 |
--------------------------------------------------------------------------------
/files/BetterWay30_Use@property_for_refactoring.md:
--------------------------------------------------------------------------------
1 | ## Better Way 30. 속성을 리팩토링하는 대신 @property를 고려하자
2 |
3 | #### 139쪽
4 |
5 | * Created : 2017/08/15
6 | * Modified: 2019/06/03
7 |
8 |
9 |
10 |
11 | ## 1. 구멍 난 양동이 예제 구현
12 |
13 | [29장](https://github.com/shoark7/Effective-Python/blob/master/files/BetterWay29_dontusegetter.md)에서 확인한 바와 같이 **내장 @property 데코레이터를 이용하면 더 간결한 방식으로 인스턴스 속성에 접근할 수 있다.** 고급 기법이지만 흔히 사용하는 @property 사용법 중 하나는 **단순 숫자 속성을 개별 변수를 두는 대신 즉석에서 계산하는 방식으로 변경하는 것이다. 이는 최근 실제 프로젝트에서 사용했던 기법인데 이렇게 하면 호출하는 쪽을 변경하지 않고도 기존에 클래스를 사용한 곳이 새로운 동작을 하게 해주므로 유용하다.** 또한 시간이 지나면서 인터페이스를 개선할 때 중요한 임시방편이 된다.
14 |
15 | 이에 대한 예로 **구멍 난 양동이(leaky bucket) 알고리즘**을 짜보자. 이 예제는 눈으로 따라갈 때는 한 번에 이해가 안 갔다. 헷갈리면 '콩쥐와 팥쥐'에서 콩쥐가 구멍 난 항아리에 물을 퍼채우는 일화를 생각하자. 물을 꾸준히 채울 수 있지만(fill), 시간이 지남에 따라 자연스럽게 액체가 유실된다.(leak) 구멍이 나 꾸준히 양이 감소하는 양동이에 인풋을 넣어 넘치지 않게 관리하려면
16 |
17 | 1. **감소하는 양보다 적게 넣어야 하고,**
18 | 1. **한 번에 인풋을 넣을 때 양동이의 최대 부피를 넘지 않도록 해야 할 것이다.**
19 |
20 | 여기서는 자세히 파고들기보다는 구현 자체가 의미를 두었다. 나도 몰라서 찾아봤기 때문에 설명했다.
21 |
22 | 먼저 **확장 등을 고려하지 않은 채 초기 수요를 느끼고 구멍 난 양동이의 할당량을 일반 파이썬 객체로 구현하려 한다고 하자.** 다음 Bucket 클래스는 남은 할당량과 이 할당량을 이용할 수 있는 기간을 표현한다.
23 |
24 |
25 | ```python
26 | from datetime import datetime, timedelta
27 |
28 | class Bucket:
29 | def __init__(self, period):
30 | self.period_delta = timedelta(seconds=period)
31 | # 한 번 채우면 사용할 수 있는 시간(다 새기까지)
32 | self.reset_time = datetime.now()
33 | # 양동이를 채운 최근 시간을 기록
34 | self.quota = 0
35 | # 투입물의 양을 기록(quota: 할당량)
36 |
37 | def __repr__(self):
38 | return "Bucket(quota={})".format(self.quota)
39 | ```
40 |
41 |
42 | 이 양동이 객체를 사용하는 함수를 두 개 만들어보자. **하나는 양동이에 새로 물을 퍼넣는 _fill_ 함수, 다른 하나는 퍼내는 _deduct_ 함수다.**
43 |
44 | 먼저 _fill_ 함수는 시간이 오래 지나 물이 모두 샜다면 양동이의 양을 0으로 재설정하는 로직을 구현한다.
45 |
46 | ```python
47 | def fill(bucket, amount):
48 | now = datetime.now()
49 | # 내용물이 모두 빠져나갈 만큼 시간이 오래됐으면,
50 | # 물이 다 빠져나갔을 것이기 때문에 quota를 0으로 두고, 시간 리셋
51 | if now - bucket.reset_time > bucket.period_delta:
52 | bucket.quota = 0
53 | bucket.reset_time = now
54 | bucket.quota += amount
55 | ```
56 |
57 |
58 | 또한 소비하는 _deduct_ 함수에서는 매번 사용할 양을 뺄 수 있는지 확인해야 한다.
59 |
60 |
61 | ```python
62 | def deduct(bucket, amount):
63 | now = datetime.now()
64 |
65 | # 시간이 너무 지나 물이 다 떨어짐. 퍼낼 수 없다.
66 | if now - bucket.reset_time > bucket.period_delta:
67 | return False
68 | # 퍼낼 양이 지금 양동이의 물 양보다 많다.
69 | elif bucket.quota < amount:
70 | return False
71 |
72 | bucket.quota -= amount # amount만큼 물을 퍼냄
73 | return True
74 | ```
75 |
76 |
77 |
78 |
79 | 이 클래스를 사용하기 위해 양동이를 채우고 필요한 만큼 빼보자.
80 |
81 | ```python
82 | bucket = Bucket(60)
83 | fill(bucket, 100)
84 | print(bucket)
85 |
86 | if deduct(bucket, 99):
87 | print("Had 99 quota")
88 | else:
89 | print("Now enough for 99 quota")
90 | print(bucket)
91 |
92 |
93 | Bucket(quota=100)
94 | Had 99 quota
95 | ```
96 |
97 | _fill_ 을 통해 양동이에 100만큼 채워놓고 _deduct_ 를 통해 99만큼을 퍼냈다.
98 |
99 | ```python
100 | # 1만 남은 상태에서 더 소비하기
101 | if deduct(bucket, 3):
102 | print("Had 3 quota")
103 | else:
104 | print("Not enough for 3 quota")
105 | print(bucket)
106 |
107 |
108 | Not enough for 3 quota
109 | Bucket(quota=1)
110 | ```
111 |
112 | 작업 후 물이 1만 남은 상태에서 3을 추가로 퍼내는 작업은 실행되지 않는다. 상식적으로 가진 것보다 더 내놓을 수는 없으니까.
113 |
114 | 여기까지는 문제가 없다. 만약 프로그램의 요구사항이 여기서 멈추고 프로젝트가 중지된다면 이 정도도 나쁘지 않을 수 있다.
115 |
116 |
117 |
118 |
119 | ## 2. @property와 setter를 사용해 리팩토링하기
120 |
121 | 위 구현은 잘 작동은 하지만 아름답지는 못하다. 그 이유는 **양동이의 투입물이 어느 수준에서 시작하는지 모른다는 것이다.**
122 | 양동이는 투입물이 0이 될 때까지 진행 기간 동안 할당량이 줄어든다. 0이 되면 _deduct_ 가 항상 False를 반환한다. 이때 **_deduct_ 를 호출하는 쪽 입장에서 중단된 이유가 Bucket의 할당량이 소진되어서인지, 아니면 처음부터 Bucket에 할당량이 없어서인지 알 수 있으면 좋을 것이다.**
123 |
124 | 다시 말해 사용자 인터페이스가 아쉬운 상황.
125 |
126 | 문제를 해결하기 위해 클래스에서 기간 동안 발생한 _max\_quota_ 와 _quota\_consumed_ 의 변경을 추적하도록 수정한다.
127 |
128 | 또한 맨 처음에 언급한대로, **맨 처음(1장) 짠 실행코드는 수정하지 않고, 리팩토링 이후에도 문제없이 실행가능하도록 할 것이다.** 이것이 정말정말 중요하다.
129 |
130 |
131 |
132 | Bucket 클래스를 앞서 언급한 두 변수를 사용해 개선하자. 이제는 **_quota_ 변수는 @property를 사용해 동적으로 현재 할당량의 수준을 계산하도록 할 것이다.**
133 |
134 | ```python
135 | class Bucket:
136 | def __init__(self, period):
137 | self.period_delta = timedelta(seconds=period)
138 | self.reset_time = datetime.now()
139 | self.max_quota = 0
140 | self.quota_consumed = 0
141 |
142 | def __repr__(self):
143 | return "Bucket(max_quota={}, quota_consumed={})".format(self.max_quota, self.quota_consumed)
144 |
145 |
146 | # quota를 동적으로 계산해 확인한다.
147 | @property
148 | def quota(self):
149 | return self.max_quota - self.quota_consumed
150 |
151 | # quota에 동적으로 변화를 준다.
152 | @quota.setter
153 | def quota(self, amount):
154 | delta = self.max_quota - amount
155 | if amount == 0:
156 | # quota를 0으로 할당하는 것은 새 기간의 할당량을 리셋하는 것
157 | self.quota_consumed = 0
158 | self.max_quota = 0
159 | elif delta < 0:
160 | # 새 기간의 할당량을 채움
161 | assert self.quota_consumed == 0
162 | self.max_quota = amount
163 | else:
164 | # 기간 동안 할당량을 소비함.
165 | assert self.max_quota >= self.quota_consumed
166 | self.quota_consumed += delta
167 | ```
168 |
169 | 투입물의 양과 소비한 양을 추적하기 위한 변수 _max\_quota_ 와 _quota\_consumed_ 변수를 추가했다.
170 |
171 | 그리고 **투입물의 양(quota)를 동적으로 추적하는 @property와 setter 메소드도 같이 작성한다.** 이렇게 작성함으로써 원래 사용코드를 변경하지 않으면서 quota를 추적 가능하게 되었다.
172 |
173 | ```python
174 | bucket = Bucket(60)
175 | print("Initial", bucket)
176 | fill(bucket, 100)
177 | print("Filled", bucket)
178 |
179 | if deduct(bucket, 99):
180 | print("Had 99 quota")
181 | else:
182 | print("Not enough for 99 quota")
183 |
184 | print("Now", bucket)
185 |
186 | if deduct(bucket, 3):
187 | print("Had 3 quota")
188 | else:
189 | print("Not enough for 3 quota")
190 |
191 | print("Still", bucket)
192 |
193 |
194 | Initial Bucket(max_quota=0, quota_consumed=0)
195 | Filled Bucket(max_quota=100, quota_consumed=0)
196 | Had 99 quota
197 | Now Bucket(max_quota=100, quota_consumed=99)
198 | Not enough for 3 quota
199 | Still Bucket(max_quota=100, quota_consumed=99)
200 | ```
201 |
202 | 몇 번을 강조하지만 **@property와 setter를 사용해서 속성을 리팩토링한 장점은 사용자 입장에서 처음에 작성한 bucket.quota를 사용한 코드를 변경하거나, Bucket 클래스가 변경된 사실을 몰라도 된다는 점이다.** Bucket의 새 용법은 잘 동작하며 _max\_quota_ 와 _quota\_consumed_ 에 직접 접근할 수 있다.
203 |
204 |
205 |
206 |
207 | ## 3. 핵심 정리
208 |
209 | * 기존의 인스턴스 속성에 새 기능을 부여하려면 @property를 사용하자
210 | * @property를 사용하여 점점 나은 데이터 모델로 발전시키자
211 | * 그렇다고 과용하지 말자. @property를 너무 많이 사용한다면 클래스와 이를 호출하는 모든 곳을 리팩토링하는 방안을 고려하자.
212 |
--------------------------------------------------------------------------------
/files/BetterWay33_ValidateSubclassWithMetaclass.md:
--------------------------------------------------------------------------------
1 | ## Better Way 33. 메타클래스로 서브클래스를 검증하자
2 |
3 | #### 158쪽
4 |
5 | * Created : 2019/06/07
6 | * Modified: 2019/06/07
7 |
8 |
9 |
10 |
11 | ## 1. 메타클래스 소개
12 |
13 | 33장부터 3장은 메타클래스에 대한 내용이다. 파이썬에서 메타클래스는 무엇인가? 내가 생각하기에 **메타클래스는 클래스를 생성할 때 호출되어 클래스에 대한 규격을 지정하고 입력에 대한 검증 등을 하는 클래스다.** 정의할 때 공통된 작업이 필요한 여러 클래스들은 한 메타클래스를 받아서 그에 맞게 자신을 정의할 수 있다.
14 |
15 | **메타클래스를 응용하는 가장 간단한 사례는 클래스를 올바르게 정의했는지 검증하는 것이다.** 복잡한 클래스 계층을 만들 때 스타일을 강제하거나 메소드를 오버라이드하도록 요구하거나 클래스 속성 사이에 철저한 관계를 두고 싶을 수 있다. **메타클래스는 서브클래스가 정의될 때마다 검증 코드를 실행하는 신뢰할만한 방법을 제공하므로 이럴 때 사용할 수 있다.**
16 |
17 | 보통 클래스 검증 코드는 클래스의 객체가 생성될 때 \_\_init\_\_ 메소드에서 실행된다. **하지만 메타클래스를 검증용으로 사용하면 인스턴스를 생성할 때가 아닌, 클래스를 정의할 때 바로 오류를 잡을 수 있기 때문에 문제를 더 빨리 파악할 수 있다.**
18 |
19 |
20 |
21 | 이에 대해 바로 살펴보기 전에 먼저 메타클래스를 정의하는 방법을 살펴보자.
22 |
23 | **메타클래스는 _type_ 을 상속하여 정의하며, 자체의 \_\_new\_\_ 메소드에서 생성되고 있는 클래스의 정보를 여러 개 받는다.** 클래스를 정의할 때 받는 이 정보들을 통해 클래스를 검증하는 등의 작업을 할 수 있다. 매우 간단한 메타클래스를 코드로 정의해보자.
24 |
25 | ```python
26 | class Meta(type):
27 | def __new__(meta, name, bases, class_dict):
28 | print("New method from Meta called! And I have followings:")
29 | print(" meta: ", meta)
30 | print(" name: ", name)
31 | print(" bases: ", bases)
32 | print(" class_dict: ", class_dict)
33 | return type.__new__(meta, name, bases, class_dict)
34 |
35 | class MyClass(metaclass=Meta):
36 | cls_var = 123
37 |
38 | def foo(self):
39 | pass
40 |
41 |
42 | New method from Meta called! And I have followings:
43 | meta:
44 | name: MyClass
45 | bases: ()
46 | class_dict: {'__module__': '__main__', '__qualname__': 'MyClass', 'cls_var': 123, 'foo': }
47 | ```
48 |
49 | 먼저 메타클래스 _Meta_ 를 정의했다. _type_ 클래스를 상속받고 자체의 \_\_new\_\_ 메소드를 구현하고 있다. 이 메소드는 4개의 인자를 받는데 각 값을 확인하기 위해 print로 값을 출력한다. 출력 후에는 원형 메타클래스의 \_\_new\_\_ 를 호출해서 클래스 정의를 마친다.
50 |
51 | 다음은 _Meta_ 를 메타클래스로 하는 _MyClass_ 클래스를 정의했다. **메타클래스 선언은 클래스 이름 옆 `()` 괄호에 'metaclass' 인자로 지정한다.** 이 클래스는 클래스 변수와 메소드 하나를 정의했다.
52 |
53 | 재밌는 것은 다음에 출력된 화면이다. _Meta_ 에서 화면에 출력한 변수들의 값이 나오는데 이들을 설명하면 다음과 같다.
54 |
55 | * **meta: 메타클래스 자신을 말한다. 메소드에서 _self_, 클래스메소드에서 _cls_ 를 지칭하는 것과 같다.**
56 | * **name: 클래스 자신의 이름을 말한다. 여기서는 _MyClass_**
57 | * **bases: 클래스가 상속받은 부모클래스들을 말한다. 여기서는 상속받지 않았기 때문에 빈 튜플이 나왔다.**
58 | * **class\_dict: 클래스가 정의한, 클래스가 가지게 될, 즉 clss.\_\_dict\_\_에 담기게 될 속성을 담은 _dict_ 다. 여기서는 클래스 변수 _cls\_var_ 와 메소드 _foo_ 가 담겨 있다.**
59 |
60 |
61 | 이때 기억할 것은 **저 출력문들은 인스턴스 생성할 때가 아닌, 클래스 정의 코드를 실행했을 때 출력됐다는 점이다.** 클래스가 정의될 때 메타클래스의 \_\_new\_\_ 메소드가 같이 실행되며 이 메소드가 무사히 마쳐야 클래스도 문제없이 정의되었다고 할 수 있다.
62 |
63 |
64 |
65 |
66 | ## 2. 메타클래스 사용 예제: 클래스 검증하기
67 |
68 | 이제 실제로 메타클래스를 유용한 작업에 사용해보자. 앞서 이야기한 '클래스가 올바르게 정의됐는지 검증하는 작업'을 해볼 예정이다.
69 |
70 | 예를 들어 2차원 다각형을 클래스로 구현한다고 하자. 모든 다각형의 변의 수는 최소 3이다. 즉 그 이하의 변을 정의하는 다각형 클래스는 튕겨내야 하는 것이다. **이렇게 하려면 특별한 검증용 메타클래스를 정의한 후 다각형 클래스 계층의 기반 클래스에 사용하면 된다.**
71 |
72 | 이때 명심할 것은 다각형들의 기반이 되는 추상 다각형에는 검증을 적용하지 말아야 한다. 그 이유에는 일반적으로 **추상 다각형에서는 속성에 _None_ 과 같은 정의되지 않은 값을 설정하기 때문이다.**
73 |
74 | ```python
75 | class ValidatePolygon(type):
76 | def __new__(meta, name, bases, class_dict):
77 | if bases != ():
78 | if class_dict['sides'] < 3:
79 | raise ValueError("Polygon needs 3+ sides")
80 |
81 | return type.__new__(meta, name, bases, class_dict)
82 |
83 | class AbstractPolygon(metaclass=ValidatePolygon):
84 | sides = None
85 |
86 | @classmethod
87 | def sum_interior_angles(cls):
88 | return 180 * (cls.sides - 2)
89 |
90 | class Triangle(AbstractPolygon):
91 | sides = 3
92 | ```
93 |
94 | 위 내용을 구현한 세 개의 클래스를 선언했다. 이때의 계층구조를 먼저 간략하게 설명하고 각 클래스를 살펴보자.
95 |
96 | * _ValidatePolygon_ : 다각형 생성 시 면의 개수가 반드시 3이상이 되도록 검증할 메타클래스이다.
97 | * _AbstractPolygon_ : 다른 모든 다각형의 추상이 되는 추상클래스다. 이 추상클래스를 상속받아 삼각형, 사각형, N각형이 정의된다.
98 | * _Triangle_ : 추상다각형 클래스를 상속받아 만든 삼각형 클래스다.
99 |
100 |
101 |
102 |
103 | 이제 각 함수를 살펴보도록 하자.
104 |
105 | ```python
106 | class ValidatePolygon(type):
107 | def __new__(meta, name, bases, class_dict):
108 | if bases != ():
109 | if class_dict['sides'] < 3:
110 | raise ValueError("Polygon needs 3+ sides")
111 |
112 | return type.__new__(meta, name, bases, class_dict)
113 | ```
114 |
115 | 다각형 클래스를 검증할 메타클래스를 선언했다. 이 클래스가 하는 일은 간단하다. 모든 2차원 다각형은 최소 3개 이상의 변을 가져야 하기 때문에 3개 미만의 변을 가지게 되면 _ValueError_ 을 일으킨다. 이때 변의 개수를 확인할 때 *class\_dict* 를 사용한다. 이 딕셔너리는 클래스에 정의된 메소드, 클래스 변수를 담고 있기에 여기서 `sides` 속성을 찾아 그 값을 검증하고 있다. validation을 통과하면 문제가 없기에 끝낸다.
116 |
117 | 중요한 것은 **_bases_, 즉 부모클래스 튜플이 비어있다면 검증절차를 거치지 않는다는 것이다.** 정의한 클래스 계층 구조에서 부모클래스 튜플이 비어있을 경우는 클래스가 추상클래스일 때다. 이때는 구체적인 변의 개수를 지정하지 않는다.(`추상`의 의미는 '구체성이 없다'와 일치하니까)
118 |
119 |
120 | ```python
121 | class AbstractPolygon(metaclass=ValidatePolygon):
122 | sides = None
123 |
124 | @classmethod
125 | def sum_interior_angles(cls):
126 | return 180 * (cls.sides - 2)
127 | ```
128 |
129 | 추상다각형 클래스를 정의했다. 메타클래스로 _ValidatePolygon_ 클래스를 받고 있음을 확인할 수 있다. 강조했듯이 _sides_ 는 _None_ 으로 정의한다. 클래스메소드로 다각형의 내각의 합을 계산하는 함수를 미리 만들어놓았다. 저 식은 초등학교에서 배웠던 것으로 기억한다.
130 |
131 |
132 | ```python
133 | class Triangle(AbstractPolygon):
134 | sides = 3
135 | ```
136 |
137 | 추상다각형 클래스를 상속받은 삼각형 클래스를 선언했다. 삼각형에 맞게 _sides_ 를 3으로 설정한 것을 알 수 있다.
138 |
139 | 테스트해보면 잘 작동한다.
140 |
141 | ```python
142 | print(Triangle.sum_interior_angles())
143 |
144 | 180
145 | ```
146 |
147 |
148 |
149 | 포스트를 마무리하기 전에 메타클래스 \_\_new\_\_의 간단한 다른 특징을 살펴본다. 바로 **클래스 생성 시에 \_\_new\_\_ 메소드의 호출 순서**에 관한 내용이다. 클래스가 생성될 때 다른 내용들이 실행되기 전에 이 메소드가 먼저 호출될까, 중간에 호출될까, 아니면 정의를 마치면서 마지막에 실행될까? 매우 간단한 예제로 확인하면 되겠다.
150 |
151 | ```python
152 | print('Start')
153 | class Line(AbstractPolygon):
154 | print('Line class definition starts')
155 | sides = 1
156 | print('Line class definition ends')
157 | print('Ended!')
158 |
159 |
160 | Start
161 | Line class definition starts
162 | Line class definition ends
163 |
164 | ValueError: Polygon needs 3+ sides
165 | ```
166 |
167 | 선분 클래스를 정의했다. 선분 자체가 곧 변의 개수가 1이기 때문에 \_\_new\_\_ 메소드에서 _ValueError_ 가 반환될 것은 당연하다. 중요한 것은 순서인데 print문을 통해 확인할 수 있다.
168 |
169 | 에러가 출력되기 전 마지막 문장이 'Line 클래스 정의 종료'이다. 즉 **클래스 선언의 다른 코드들이 다 실행된 후에 \_\_new\_\_ 메소드가 실행되는 것.**
170 |
171 |
172 |
173 |
174 | ## 3. 핵심 정리
175 |
176 | * 서브클래스 타입의 객체를 생성하기에 앞서 서브클래스가 정의 시점부터 제대로 구성되었음을 보장하려면 메타클래스를 사용하자.
177 | * 메타클래스의 \_\_new\_\_ 메소드는 class 문의 본문 전체가 처리된 후에 실행된다.
178 |
--------------------------------------------------------------------------------
/files/BetterWay34_RegisterClassWithMetaclass.md:
--------------------------------------------------------------------------------
1 | ## Better Way 34. 메타클래스로 클래스의 존재를 등록하자
2 |
3 | #### 161쪽
4 |
5 | * Created : 2019/06/10
6 | * Modified: 2019/06/10
7 |
8 |
9 |
10 |
11 | ## 1. JSON 직렬화, 역직렬화 클래스 만들기
12 |
13 | **메타클래스를 사용하는 또 다른 일반적인 사례는 프로그램에 있는 타입을 자동으로 등록하는 것이다. 등록(Registration)은 간단한 식별자(Identifier)를 대응하는 클래스에 매핑하는 역방항 조회(Reverse lookup)를 수행할 때 유용하다.**
14 |
15 | 예를 들어 파이썬 객체를 직렬화한 표현을 JSON으로 구현한다고 해보자. 객체를 얻어와서 JSON 문자열로 변환할 방법이 필요하다.
16 |
17 | 다음은 생성자 메소드의 인자를 저장하고 JSON 딕셔너리로 변환하는 기반 클래스를 범용적으로 정의한 것이다.
18 |
19 | ```python
20 | import json
21 |
22 | class Serializable:
23 | def __init__(self, *args):
24 | self.args = args
25 |
26 | def serialize(self):
27 | return json.dumps({'args': self.args})
28 | ```
29 |
30 | 매우 간단한 클래스로서 서브클래스들의 초기화 인자를 JSON으로 직렬화할 메소드 _serialize_ 를 가지고 있다. 이 클래스를 이용하면 가령 '2차원 점'에 대응하는 간단한 불변 자료구조를 문자열로 쉽게 직렬화할 수 있다.
31 |
32 | ```python
33 | class Point2D(Serializable):
34 | def __init__(self, x, y):
35 | super().__init__(x, y)
36 | self.x = x
37 | self.y = y
38 |
39 | def __repr__(self):
40 | return f'Point2D({self.x}, {self.y})'
41 |
42 | point = Point2D(5, 3)
43 | print('Point :', point)
44 | print('Serialized:', point.serialize())
45 |
46 |
47 | Point : Point2D(5, 3)
48 | Serialized: {"args": [5, 3]}
49 | ```
50 |
51 | JSON 객체를 문자열로 변환하는 직렬화가 잘 작동함을 확인할 수 있다. 이제 반대로 이 문자열을 역직렬화해서 JSON이 표현하는 Point2D 객체를 생성하자. 많은 방법이 있겠지만 여기서는 _Serializable_ 를 상속받는 또 다른 클래스를 정의한다.
52 |
53 | ```python
54 | class Deserializable(Serializable):
55 | @classmethod
56 | def deserialize(cls, json_data):
57 | params = json.dumps(json_data)
58 | return cls(parms['args'])
59 | ```
60 |
61 | _Deserializable_ 을 이용하면 간단한 불변 객체들을 범용적인 방식으로 쉽게 직렬화하고 역직렬화할 수 있다.
62 |
63 | ```python
64 | class BetterPoint2D(Deserializable):
65 | def __init__(self, x, y):
66 | super().__init__(x, y)
67 | self.x = x
68 | self.y = y
69 |
70 | def __repr__(self):
71 | return f'Point2D({self.x}, {self.y})'
72 |
73 | point = BetterPoint2D(7, 5)
74 | print('Before :', point)
75 | data = point.serialize()
76 | print('Serialized:', data)
77 | after = BetterPoint2D.deserialize(data)
78 | print('After :', after)
79 |
80 |
81 | Before : Point2D(7, 5)
82 | Serialized: {"args": [7, 5]}
83 | After : Point2D(7, 5)
84 | ```
85 |
86 | _BetterPoint2D_ 객체를 문자열로 직렬화하고, 반대로 역직렬화까지 성공적으로 변환했다. 이 방법은 잘 작동하지만, **직렬화된 데이터에 대응하는 타입(예를 들어 Point2D, BetterPoint2D)을 미리 알고 있을 때만 동작한다는 문제점이 있다.** 그 이유는 역직렬화 함수가 특정 클래스에 바운딩되어 있기 때문이다.
87 |
88 | 클래스를 써야하는 복잡한 현실상황이라면 이상적으로는 **JSON으로 직렬화되는 많은 클래스를 갖추고 그중 어떤 클래스든 대응하는 파이썬 객체로 역직렬화하는 공통 함수를 하나만 두려고 할 것이다.**
89 |
90 | 이렇게 만들려면 직렬화할 객체의 클래스 이름을 JSON 데이터에 포함하면 된다.
91 |
92 | ```python
93 | class BetterSerializable:
94 | def __init__(self, *args):
95 | self.args = args
96 |
97 | def serialize(self):
98 | return json.dumps({
99 | 'class': self.__class__.__name__,
100 | 'args': self.args,
101 | })
102 | ```
103 |
104 | 다음으로 클래스 이름을 해당 클래스의 객체 생성자에 매핑하고 이 매핑을 관리한다.
105 |
106 | ```python
107 | registry = {}
108 |
109 | def register_class(target_class):
110 | registry[target_class.__name__] = target_class
111 |
112 | def deserialize(data):
113 | parms = json.loads(data)
114 | name = parms['class']
115 | target_class = registry[name]
116 | return target_class(*params['args'])
117 | ```
118 |
119 | 매핑을 관리할 _dict_ 인 _registry_ 와 여기에 클래스를 등록하는 함수와 역직렬화하는 함수를 *register\_class*, _deserialize_ 함수로 각각 만들었다. 이때 기억할 것은 **이 변수와 함수들은 특정 클래스에 바운딩되어 있지 않은 글로벌 이름으로서, 다른 많은 서브클래스들에서 편하게 사용할 수 있다는 점이다.**
120 |
121 | _deserialize_ 가 항상 제대로 동작함을 보장하려면 추후에 역직렬화할 법한 모든 클래스에서 *register\_class* 를 호출해야 한다.
122 |
123 | ```python
124 | class EvenBetterPoint2D(BetterSerializable):
125 | def __init__(self, x, y):
126 | super().__init__(x, y)
127 | self.x = x
128 | self.y = y
129 |
130 | register_class(EvenBetterPoint2D) # 클래스 등록!
131 | ```
132 |
133 | 이제 문자열 JSON 데이터가 어떤 클래스를 담고 있는지 몰라도 문제없이 역직렬화할 수 있다.
134 |
135 | ```python
136 | Before : EvenBetterPoint2D(100, 50)
137 | Serialized: {"class": "EvenBetterPoint2D", "args": [100, 50]}
138 | After : EvenBetterPoint2D(100, 50)
139 | ```
140 |
141 |
142 |
143 | ## 2. 개선점: 메타클래스로 자동으로 등록하기
144 |
145 | 이전 장의 범용적인 역직렬화 방법도 문제점이 있다. 이 방법의 문제는 **서브클래스를 정의할 때 _register\_class_ 를 호출하는 일을 까먹을 수 있다는 점이다.**
146 |
147 | ```python
148 | class Point3D(BetterSerializable):
149 | def __init__(self, x, y, z):
150 | super().__init__(x, y, z)
151 | self.x = x
152 | self.y = y
153 | self.z = z
154 |
155 | # 아뿔싸! 등록하는 것을 까먹어부렸다!
156 | ```
157 |
158 | 이런 실수는 충분히 할 수 있고, 이는 등록을 잊는 클래스의 객체를 런타임에 역직렬화할 때 코드가 중단되는 원인이 된다.
159 |
160 | ```python
161 | point = Point3D(1, 2, -3)
162 | data = point.serialize()
163 | deserialize(data)
164 |
165 |
166 | KeyError: 'Point3D'
167 | ```
168 |
169 | _Point3D_ 클래스를 등록하는 것을 깜빡했기 때문에 범용 역직렬화 함수의 _registry_ 에서 목표 클래스를 찾지 못했다. 이렇게 모든 서브 클래스에서 필요한 기능을 사용자가 매번 호출하는 일은 오류가 일어날 가능성이 높으며, 특히 초보 프로그래머에게는 어렵다.
170 |
171 | 프로그래머가 의도한 대로 **_BetterSerializable_ 을 사용하고, 수동이 아닌 모든 경우에 *register\_class* 가 호출된다고 확신하게 할 수는 없을까?** 메타클래스를 이용하면 서브클래스가 정의될 때 _class_ 문을 가로채는 방법으로 이렇게 만들 수 있다. 메타클래스로 클래스 본문이 끝나자마자 새 타입을 등록하면 된다.
172 |
173 | ```python
174 | class SerializeMeta(type):
175 | def __new__(meta, name, bases, class_dict):
176 | cls = type.__new__(meta, name, bases, class_dict)
177 | register_class(cls)
178 | return cls
179 |
180 | class RegisteredSerializable(BetterSerializable, metaclass=SerializeMeta):
181 | pass
182 | ```
183 |
184 | 메타클래스의 \_\_new\_\_ 메소드에서 클래스를 정의한 후 반환하기 직전 정의된 클래스를 자동 등록한다. 이렇게 하면 **_RegisteredSerializable_ 의 서브클래스를 정의할 때 *register\_class* 가 호출되어 _deserialize_ 가 KeyError를 일으키지 않고 항상 기대한 대로 동작할 것이라고 확신할 수 있다.**
185 |
186 |
187 | ```python
188 | class Vector3D(RegisteredSerializable):
189 | def __init__(self, x, y, z):
190 | super().__init__(x, y, z)
191 | self.x = x
192 | self.y = y
193 | self.z = z
194 |
195 | def __repr__(self):
196 | return f'Vector 3D({self.x}, {self.y}, {self.z})'
197 |
198 | v3 = Vector3D(10, -7, 100)
199 | print('Before :', v3)
200 | data = v3.serialize()
201 | print('Serialized:', data)
202 | after = deserialize(data)
203 | print('After :', after)
204 |
205 |
206 | Before : Vector 3D(10, -7, 100)
207 | Serialized: {"class": "Vector3D", "args": [10, -7, 100]}
208 | After : Vector 3D(10, -7, 100)
209 | ```
210 |
211 | 메타클래스를 이용해 클래스를 등록하면 상속 트리가 올바르게 구축되어 있는 한 클래스 등록을 놓치지 않는다. 앞에서 본 것처럼 직렬화에 잘 동작하며 ORM, 플러그인 시스템, 시스템 후크 등에도 적용할 수 있다.
212 |
213 |
214 |
215 |
216 | ## 3. 핵심 정리
217 |
218 | * 클래스 등록은 모듈 방식의 파이썬 프로그램을 만들 때 유용한 패턴이다.
219 | * 메타클래스를 이용하면 프로그램에서 기반 클래스로 서브클래스를 만들 때마다 자동으로 등록 코드를 실행할 수 있다.
220 | * 메타클래스를 이용해 클래스를 등록하면 등록 호출을 절대 빠뜨리지 않으므로 오류를 방지할 수 있다.
221 |
--------------------------------------------------------------------------------
/files/BetterWay35_UseDocstringWithMetaclass.md:
--------------------------------------------------------------------------------
1 | ## Better Way 35. 메타클래스로 클래스 속성에 주석을 달자
2 |
3 | #### 167쪽
4 |
5 | * Created : 2019/06/11
6 | * Modified: 2019/06/11
7 |
8 |
9 |
10 | ## 1. 일반 디스크립터로 속성을 할당할 때의 문제점: 반복
11 |
12 | **메타클래스로 구현할 수 있는 기능 중 하나는 클래스를 정의한 이후에, 하지만 실제로 사용하기 전에 프로퍼티를 수정하거나 주석을 붙이는 것이다.** 보통 이 기법을 디스크립터와 함께 사용하여, 클래스에서 디스크립터를 어떻게 사용하는지 자세히 조사한 정보를 디스크립터에 제공한다.
13 |
14 | 예를 들어, 고객 데이터베이스의 로우를 표현하는 새 클래스를 정의한다고 하자. 테이블의 각 칼럼(Column)에 대응하는 클래스의 프로퍼티가 있어야 한다. 따라서 프로퍼티를 칼럼 이름과 연결하는 데 사용할 디스크립터 클래스를 다음과 같이 정의한다.
15 |
16 | ```python
17 | class Field:
18 | def __init__(self, name):
19 | self.name = name
20 | self.internal_name = '_' + self.name
21 |
22 | def __get__(self, instance, instance_type):
23 | if instance is None:
24 | return self
25 | return getattr(instance, self.internal_name, '')
26 |
27 | def __set__(self, instance, value):
28 | setattr(instance, self.internal_name, value)
29 | ```
30 |
31 | _Field_ 디스크립터에 저장할 칼럼 이름이 있으면 내장 함수 _setattr_ 과 _getattr_ 을 사용해서 모든 인스턴스별 상태를 인스턴스 딕셔너리에 보호 필드로 직접 저장할 수 있다. 처음에는 이 방법이 메모리 누수를 피하려고 _weakref_ 로 디스크립터를 만드는 [방법](https://github.com/shoark7/Effective-Python/blob/master/files/BetterWay31_UseDescriptorForReusablePropertyMethod.md)보다 훨씬 편리해보인다.
32 |
33 | ```python
34 | class Customer:
35 | first_name = Field('first_name')
36 | last_name = Field('last_name')
37 | prefix = Field('prefix')
38 | suffix = Field('suffix')
39 | ```
40 |
41 | 클래스를 사용하는 방법은 간단하다.
42 |
43 | ```python
44 | foo = Customer()
45 | print('Before:', repr(foo.first_name), foo.__dict__)
46 | foo.first_name = 'Parkito'
47 | print('After :', repr(foo.first_name), foo.__dict__)
48 |
49 | Before: '' {}
50 | After : 'Parkito' {'_first_name': 'Parkito'}
51 | ```
52 |
53 | 다 좋다. 잘 작동한다. 하지만 사소하지만 괘념케 하는 것이 하나 있으니, _Customer_ 클래스에서 클래스 변수로 디스크립터를 설정할 때, **각 변수의 이름을 두 번 적어야 하는 반복이 있다.** 왜 이렇게 해야만 하는 것일까?
54 |
55 | 문제는 **_Customer_ 클래스 정의에서 연산 순서가 왼쪽에서 오른쪽으로 읽는 방식과 반대라는 점이다.** 먼저 _Field_ 생성자는 *Field('first_name')* 형태로 호출한다. 다음 이 호출의 반환 값이 _Customer.field_name_ 에 할당된다. 일반적인 변수 할당과 똑같다. 그러므로 Field에서는 자신이 어떤 클래스 속성에 할당될지 미리 알 방법이 없다.
56 |
57 |
58 |
59 |
60 | ## 2. 개선: 메타클래스로 속성 이름 할당하기
61 |
62 | 중복성을 제거하려면 메타클래스를 사용하면 된다. 메타클래스를 이용하면 _class_ 문을 직접 후킹하여 _class_ 본문이 끝나자마자 원하는 동작을 처리할 수 있다.
63 |
64 | 다음 예제에서는 **필드 이름을 수동으로 여러 번 지정하지 않고 메타클래스를 사용하여 _Field.name_ 과 _Field.internal\_name_ 을 디스크립터에 자동으로 할당한다.**
65 |
66 | ```python
67 | class Meta(type):
68 | def __new__(meta, name, bases, class_dict):
69 | for key, value in class_dict.items():
70 | if isinstance(value, Field):
71 | value.name = key
72 | value.internal_name = '_' + key
73 | cls = type.__new__(meta, name, bases, class_dict)
74 | return cls
75 | ```
76 |
77 | 다음은 메타클래스를 사용하는 기반 클래스를 정의한 코드다. 데이터베이스 레코드를 표현하는 클래스가 모두 이 클래스를 상속하게 해서 모두 메타클래스를 사용하게 해야 한다.
78 |
79 | ```python
80 | class DatabaseRow(metaclass=Meta):
81 | pass
82 | ```
83 |
84 | 메타클래스를 사용하게 해도 필드 디스크립터는 변경이 거의 없다. 유일한 차이는 더는 생성자에 중복되는 이름 인자를 넘길 필요가 없다는 점이다. 대신 필드 디스크립터의 속성은 위의 *Meta.\_\_new\_\_* 메소드에서 설정된다.
85 |
86 | ```python
87 | class Field:
88 | def __init__(self): # 인자를 직접 넘기지 않아도 된다!
89 | self.name = None
90 | self.internal_name = None
91 |
92 | # ... 이전과 동일
93 | ```
94 |
95 |
96 |
97 | 이제 이 모두를 활용해서 새로운 고객 데이터베이스 레코드 클래스를 만들어보자.
98 |
99 | ```python
100 | class BetterCustomer(DatabaseRow):
101 | first_name = Field()
102 | last_name = Field()
103 | prefix = Field()
104 | suffix = Field()
105 |
106 |
107 | foo = BetterCustomer()
108 | print('Before:', repr(foo.first_name), foo.__dict__)
109 | foo.first_name = 'Parkito'
110 | print('After :', repr(foo.first_name), foo.__dict__)
111 |
112 | Before: '' {}
113 | After : 'Parkito' {'_first_name': 'Parkito'}
114 | ```
115 |
116 | 반복을 제거했고, 동작도 이전과 완전히 동일함을 확인할 수 있다.
117 |
118 |
119 |
120 |
121 | ## 3. 핵심 정리
122 |
123 | * 메타클래스를 이용하면 클래스가 완전히 정의되기 전에 클래스 속성을 수정할 수 있다.
124 | * 디스크립터와 메타클래스는 선언적 동작과 런타임 내부 조사(introspection)용으로 강력한 조합을 이룬다.
125 | * 메타클래스와 디스크립터를 연계하여 사용하면 메모리 누수와 _weakref_ 모듈 사용을 모두 피할 수 있다.
126 |
--------------------------------------------------------------------------------
/files/BetterWay37_UseThreadForIO.md:
--------------------------------------------------------------------------------
1 | ## Better Way 37. 스레드를 블로킹 I/O 용으로 사용하고 병렬화용으로는 사용하지 말자
2 |
3 | #### 178쪽
4 |
5 | * Created : 2017/01/24
6 | * Modified: 2019/06/14
7 |
8 |
9 |
10 | ## 1. 파이썬의 GIL
11 |
12 | 파이썬의 표준 구현을 `CPython`이라고 한다. CPython은 파이썬 프로그램을 두 단계로 실행한다.
13 |
14 | 1. 소스 텍스트를 바이트코드(bytecode)로 파싱하고 컴파일한다.
15 | 2. 스택 기반 인터프리터(PVM, Python Virtual Machine)으로 바이트코드를 실행한다.
16 |
17 | 바이트코드 인터프리터는 파이썬 프로그램이 실행되는 동안 지속되고, 일관성 있는 상태를 유지한다. 파이썬은 그 무시무시한 **GIL**(Global Interpreter Lock)으로 일관성을 유지한다.
18 |
19 | 본질적으로 **GIL은 상호 배제 잠금(mutex)이며 CPython이 선점형 멀티스레딩의 영향을 받지 않게 막는다.** 선점형 멀티스레딩(preemptive multithreading)은 한 스레드가 다른 스레드를 인터럽트해서 프로그램의 제어를 얻는 것을 말한다. GIL은 이런 인터럽트를 막아주며 모든 바이트코드가 올바르게 작동함을 보장한다. 쉽게 말해 파이썬은 스레드가 본질적으로 하나만 작동하기 때문에 다른 언어처럼 병렬화의 용도로 쓰기 d어렵다.
20 |
21 | GIL은 중요한 부작용을 갖고 있는데 자바 같은 언어로 작성한 프로그램에서 여러 스레드를 실행하는 건 프로그램이 동시에 여러 CPU 코어를 사용하는 것을 의미한다. 파이썬도 멀티스레드를 지원하지만 **GIL은 한 번에 한 스레드만 실행한다.** 다시 말해 스레드가 병렬 연산을 해야 하거나 파이썬 프로그램의 속도를 높여야 하는 상황이라면 실망하게 된다.
22 |
23 | 예를 들어, 파이썬으로 연산 집약적인 작업을 한다고 하자. 여기서는 단순한 정수 인수분해 알고리즘을 테스트해본다.
24 |
25 | ```python
26 | from time import time
27 |
28 | def factorize(number):
29 | for i in range(1, number + 1):
30 | if number % i == 0:
31 | yield i
32 |
33 | numbers = [1921931, 144872, 384539, 345983]
34 | start = time()
35 | for number in numbers:
36 | list(factorize(number))
37 | end = time()
38 |
39 | print('It took', end - start)
40 |
41 |
42 | It took 0.6044101715087891
43 | ```
44 |
45 | 스레드 사용없이 0.6초가 걸렸다. 다른 언어에서는 당연히 이런 연산에 멀티스레드를 이용한다. 멀티스레드를 이용하면 컴퓨터의 모든 CPU를 최대한 활용할 수 있기 때문이다. 이 작업을 파이썬으로 해보자. 같은 연산을 스레드를 사용한다.
46 |
47 | ```python
48 | from threading import Thread
49 |
50 | class FactorizeThread(Thread):
51 | def __init__(self, number):
52 | super().__init__() # Thread를 초기화
53 | self.number = number
54 |
55 | def run(self): # thread의 start 메소드를 사용하면 run이 trigger된다.
56 | self.factors = list(factorize(self.number))
57 | ```
58 |
59 | 이제 실제 테스트를 해본다.
60 |
61 | ```python
62 | start = time()
63 | threads = [] for number in numbers:
64 | thread = FactorizeThread(number)
65 | thread.start()
66 | threads.append(thread)
67 |
68 | for thread in threads:
69 | thread.join() # 모든 스레드가 끝나기를 기다린다. 그 다음 end를 계산한다.
70 | end = time()
71 |
72 | print('It took', end - start)
73 |
74 |
75 | It took 0.6390671730041504
76 | ```
77 |
78 | 약 0.64초로 황당하게도 스레드를 안 쓸 때보다 시간이 더 걸렸다. 숫자별로 스레드 하나를 사용하면 스레드를 생성하고 실행 순서를 조율하는 부담을 감안할 때 4배 미만의 속도 향상을 기대했을 것이다. 이 코드를 내 노트북처럼 듀얼코어머신에서 실행하면 2배 정도의 속도를 기대했을 것이다. 이로써 GIL이 표준 CPython 인터프리터에서 실행하는 프로그램에 미치는 영향을 알 수 있다.
79 |
80 | CPython이 멀티코어를 활용하게 하는 방법은 여러 가지지만, 표준 Thread에는 작동하지 않기 때문에 노력이 필요하다. _multiprocess_ 모듈, _concurrent.futures_ 등.
81 |
82 |
83 |
84 |
85 | ## 2. 그럼에도 스레드가 존재하는 이유
86 |
87 | 그러면 '애초에 파이썬 스레드는 존재 자체가 무슨 의미일까'라고 묻는 게 당연하다. 여기에는 크게 두 가지 이유가 있을 수 있다.
88 |
89 | 1. 파이썬 멀티스레드를 사용하면 동시에 여러 작업을 하는 것처럼 보이게 할 수 있다.
90 | > 동시에 동작하는 태스크를 관리하는 코드를 직접 구현하는 것은 어렵다.
91 | > 스레드를 이용하면 함수를 마치 병렬로 시행하는 것처럼 보이게 할 수 있다.
92 | > 비록 한 번에 한 스레드만 진행하지만, CPython은 스레드가 어느 정도 공평하게 실행됨을 보장한다.
93 |
94 | 2. **특정 유형의 시스템 콜(System Call)에서 일어나는 블로킹 I/O를 다루기 위해서다.**
95 | > **시스템 콜**(System call)이란 **파이썬 프로그램이 외부 환경과 상호 작용하도록 운영체제 커널에 요청하는 것**을 의미한다.
96 | > 블로킹 I/O로는 파일 읽기/쓰기, 네트워크와의 상호작용, 디스플레이 같은 장치와의 통신이 있다.
97 | > 즉 파이썬 _open_ 함수로 파일을 여는 것은 바로 `커널을 통해 파일을 여는 것이다.`
98 | > **스레드는 운영체제가 이런 요청에 응답하는 데 드는 시간을 프로그램과 분리하므로 블로킹 I/O 처리에 유용하다.**
99 |
100 |
101 | 이번 장에서는 2번째 이유에 집중해서 예제를 만들어 본다.
102 |
103 |
104 |
105 | 원격 제어가 가능한 헬리콥터에 직렬포트로 신호를 보내고 싶다고 하자. 이번 예제는 느림 시스템 콜을 담당하는 [select](https://docs.python.org/3/library/select.html) 모듈을 사용할 것이다. 이 내장모듈은 UNIX의 _select_ 시스템 콜에 대한 직접적인 인터페이스인 **select.select** 함수를 제공한다.
106 |
107 | 이 함수를 이용해서 동기식 직렬 포트를 사용할 때 일어나는 상황과 비슷하게 하려고 운영체제에 0.1초간 블록한 후, 제어를 프로그램에 돌려달라고 요청한다.
108 |
109 | ```python
110 | import select
111 |
112 | def slow_systemcall():
113 | select.select([], [], [], 0.1) # 자세한 내용은 select 모듈을 찾아보기 바란다.
114 |
115 | # 이 시스템 콜을 연속해서 실행하면 시간이 *선형*으로 증가한다.
116 | start = time()
117 | for _ in range(5):
118 | slow_systemcall()
119 | end = time()
120 |
121 | print('It took', end - start)
122 |
123 |
124 | It took 0.5031735897064209
125 | ```
126 |
127 | 0.1초의 통신을 5번 해서 0.5초가 걸린 것을 알 수 있다. 비동기 작업이 아니기 때문에 함수 실행횟수에 선형적으로 비례해 시간이 걸리는 것을 확인할 수 있다.
128 |
129 | **이 방법의 문제는 *slow\_systemcall* 함수가 실행되는 동안 프로그램이 다른 일을 할 수 없다는 점이다.** 프로그램의 메인 스레드는 시스템 콜 _select_ 때문에 실행이 막혀 있다. 신호를 헬리콥터에 보내는 동안 헬리콥터의 다른 이동을 계산해야 한다. 그렇지 않으면 헬리콥터가 충돌할 것이다. 블로킹 I/O를 사용하며 동시에 연산도 해야 한다면 시스템 콜을 스레드로 옮기는 방법을 고려해야 한다.
130 |
131 | 다음 코드는 *slow\_systemcall* 함수를 별도의 스레드에서 여러 번 호출하여 실행한다. 이렇게 하면 동시에 여러 직렬 포트(및 헬리콥터)와 통신할 수 있게 되고, 메인 스레드는 필요한 계산이 무엇이든 수행하도록 남겨둘 수 있다.
132 |
133 | ```python
134 | start = time()
135 | threads = []
136 | for _ in range(5):
137 | thread = Thread(target=slow_systemcall)
138 | thread.start()
139 | threads.append(thread)
140 |
141 | for thread in threads:
142 | thread.join()
143 | end = time()
144 |
145 | print('It took', end - start)
146 |
147 |
148 | It took 0.10243105888366699
149 | ```
150 |
151 | 전체 실행시간이 0.1초로 이번에는 스레드의 병렬처리가 성공했다.
152 |
153 | 정리하면, GIL은 스레드를 사용해도 파이썬 코드가 병렬로 실행하지 못하도록 한다. 하지만 시스템 콜에서는 이런 부정적인 영향이 없다. 이는 **파이썬 스레드가 실행 대상이 단순 코드가 아닌 시스템 콜이라면 GIL을 풀고 시스템 콜의 작업이 끝나는 대로 GIL을 다시 얻기 때문.**
154 |
155 | 스레드 이외에도 내장 모듈 asyncio처럼 블로킹 I/O를 다루는 다양한 수단이 있고, 이런 대체 수단에는 중요한 이점이 있다. 하지만 이런 옵션을 선택하면 실행 모델에 맞춰 코드를 재작성해야 하는 추가 작업이 필요하다.
156 |
157 | 스레드를 이용하는 방법은 프로그램의 수정을 최소화하면서도 블로킹 I/O를 병렬로 수행하는 가장 간단한 방법이다.
158 |
159 |
160 |
161 | ## 3. 핵심 정리
162 | * 파이썬 스레드는 GIL 때문에 여러 CPU 코어에서 병렬로 바이트코드를 실행할 수 없다.
163 | * GIL에도 불구하고 파이썬 스레드는 동시에 여러 작업을 하는 것처럼 보여주기 쉽게 해주므로 여전히 유용하다.
164 | * **여러 시스템 콜을 병렬로 수행하려면 파이썬 스레드를 사용하자. 블로킹 I/O를 수행할 수 있다.**
165 |
--------------------------------------------------------------------------------
/files/BetterWay38_UseLockForRaceConditionInThread.md:
--------------------------------------------------------------------------------
1 | ## Better Way 38. 스레드에서 데이터 경쟁을 막으려면 Lock을 사용하자
2 |
3 | #### 183쪽
4 |
5 | * Created : 2019/06/17
6 | * Modified: 2019/06/17
7 |
8 |
9 |
10 | ## 1. GIL은 데이터의 경쟁을 보호하지 못한다.
11 |
12 | 파이썬의 GIL을 배우고 나면, 나같은 신참 프로그래머는 **파이썬에서 상호 배제 잠금(MUTEX)을 사용하지 않아도 될 것이라 생각할지도 모른다.** 파이썬에서 스레드가 여러 CPU 코어에서 병렬로 실행하는 것을 GIL이 이미 막았다면 프로그램의 자료구조에도 잠금이 설정되어 있을 것이다. 충분히 생각해봄직 하다.
13 |
14 | 하지만 실제로는 그렇지 않다. GIL로 인해 파이썬 스레드가 한 번에 하나만 실행되지만, **파이썬 인터프리터에서 자료구조를 다루는 스레드 연산은 두 바이트코드 명령어 사이에서 인터럽트될 수 있다. 특히 여러 스레드에서 동시에 같은 객체에 접근한다면 이런 가정은 위험하다.** 자료구조의 불변성이 인터럽트 때문에 언제든지 깨질 수도 있다는 의미이며 프로그램은 오류가 있는 상태로 남는다.
15 |
16 | 예를 들어, 전체 센서 네트워크에서 밝기 단계를 샘플링하는 경우처럼 병렬로 여러 대상을 카운트하는 프로그램을 작성한다고 해보자. 시간에 따른 밝기 샘플의 전체 개수를 알고 싶다면 새 클래스로 개수를 모으면 된다.
17 |
18 |
19 | ```python
20 | class Counter:
21 | def __init__(self):
22 | self.count = 0
23 |
24 | def increment(self, offset):
25 | self.count += offset
26 | ```
27 |
28 | 센서에서 읽는 작업에서는 블로킹 I/O가 필요하므로 각 센서별로 고유한 작업 스레드를 만들어 해결하면 될 것 같다.([37장](https://github.com/shoark7/Effective-Python/blob/master/files/BetterWay37_UseThreadForIO.md) 참조) 각 센서 측정값을 읽고 나면 작업 스레드는 읽으려는 최대 개수에 이를 때까지 카운터를 증가시킨다.
29 |
30 | ```python
31 | def worker(sensor_index, how_many, counter):
32 | for _ in range(how_many):
33 | # ... 센서에서 읽는 I/O
34 | # ... sensor_index를 사용해 네트워크 내 특정 센서를 지목할 듯
35 |
36 | counter.increment(1)
37 | ```
38 |
39 | 이제 스레드를 만들어서 센서별로 작업 스레드를 시작하고 읽기를 모두 마칠 때까지 기다리는 함수를 만들자.
40 |
41 | ```python
42 | from threading import Thread
43 |
44 | def run_threads(func, how_many, counter):
45 | SIZE = 5
46 | threads = []
47 |
48 | for i in range(SIZE):
49 | args = (i, how_many, counter)
50 | thread = Thread(target=func, args=args)
51 | threads.append(thread)
52 | thread.start()
53 |
54 | for thread in threads:
55 | thread.join()
56 | ```
57 |
58 | 스레드 5개를 병렬로 실행하는 일은 간단해 보이므로 결과가 명확하 것으로 보인다.
59 |
60 |
61 | ```python
62 | how_many = 10 ** 5
63 | counter = Counter()
64 | run_threads(worker, how_many, counter)
65 | print(f'Counter should be 500,000 and the real result is {counter.count:,}')
66 |
67 |
68 | Counter should be 500,000 and the real result is 328,223
69 | ```
70 |
71 | 센서를 읽는 작업을 50만번이나 했기 때문에 실제 카운터도 50만이 찍히리라 무난히 생각할 수 있다. 하지만 예상과는 다르게 약 33만으로 더 적게 나왔다. 왜 그럴까?
72 |
73 | 파이썬 인터프리터는 모든 스레드가 거의 동등한 처리 시간 동안 실행하게 하려고 모든 실행 중인 스레드 사이에서 공평성을 유지한다. **파이썬은 공평성을 유지하려고 실행 중인 스레드를 잠시 중지하고 차례로 다른 스레드를 재개한다. 문제는 파이썬이 스레드를 정확히 언제 중지할지 모른다는 점이다.** 스레드는 심지어 원자적(atomic)인 연산으로 보이는 작업 중간에서 멈출 수도 있다.
74 |
75 |
76 | ```python
77 | self.count += offset
78 | ```
79 |
80 | 앞선 카운터에서 _offset_ 만큼 카운트를 증가시키는 코드다. 언뜻 보기에 이 작업은 단일한, 원자적인 작업 같지만 '+=' 연산자는 실제로는 다음과 같이 풀어질 수 있다.
81 |
82 | ```python
83 | value = getattr(counter, 'count')
84 | result = value + offset
85 | setattr(counter, 'count', result)
86 | ```
87 |
88 | 카운터를 증가시키는 파이썬 스레드는 이 연산들 사이에서 중지될 수 있다. 만약 연산이 끼어든 상황 때문에 value 이전 값이 카운터에 할당되면 문제가 된다. 다음은 두 스레드 A, B 사이의 안 좋은 상호작용을 보여주는 예다.
89 |
90 | ```python
91 | value_a = getattr(counter, 'count')
92 | value_b = getattr(counter, 'count')
93 | result_b = value_b + 1
94 | setattr(counter, 'count', reult_b)
95 | result_a = value_a + 1
96 | setattr(counter, 'count', result_a)
97 | ```
98 |
99 | 스레드 A는 스레드 B에서 카운터 증가를 실행하는 모든 작업을 없앤다. 이런 일이 앞선 예제에서 발생해서 실제 값보다 적게 나오게 된 것이다.
100 |
101 |
102 |
103 | ## 2. 해결책: Lock 사용하기
104 |
105 | **파이썬은 이와 같은 데이터 경쟁(race)과 다른 방식의 자료구조 오염을 막으려고 내장 모듈 threading에 강력한 도구들을 갖춰놓고 있다. 가장 간단하고 유용한 도구는 상호 배제 잠금 기능을 제공하는 Lock 클래스를 사용하는 것이다.**
106 |
107 | 잠금을 이용하면 여러 스레드가 동시에 접근하더라도 Counter 클래스의 현재 값을 보호할 수 있다. 한 번에 한 스레드만 잠금을 얻을 수 있다. 여기에 `with` statement를 사용해서 만들어보자.
108 |
109 |
110 | ```python
111 | from threading import Lock
112 |
113 | class LockingCounter:
114 | def __init__(self):
115 | self.lock = Lock()
116 | self.count = 0
117 |
118 | def increment(self, offset):
119 | with self.lock:
120 | self.count += offset
121 | ```
122 |
123 | 이제 전처럼 작업 스레드를 실행하지만 이번에는 _LockingCounter_ 를 사용한다.
124 |
125 | ```python
126 | how_many = 10 ** 5
127 | counter = LockingCounter()
128 | run_threads(worker, how_many, counter)
129 | print(f'Counter should be 500,000 and the real result is {counter.count:,}')
130 |
131 |
132 | Counter should be 500,000 and the real result is 500,000
133 | ```
134 |
135 | 결과가 예상한 것과 정확히 일치한다. Lock를 보유한 스레드에 _offset_ 을 추가하는 작업이 점유되기 때문에 중간에 작업이 강탈당하지 않는다. 대신 시간이 좀더 걸린다.
136 |
137 |
138 |
139 |
140 | ## 3. 핵심 정리
141 |
142 | * 파이썬에 GIL이 있다고 해도 프로그램 안에서 실행되는 스레드 간의 데이터 경쟁으로부터 보호할 책임은 프로그래머에게 있다.
143 | * 여러 스레드가 잠금 없이 같은 객체를 수정하면 프로그램의 자료구조가 오염된다.
144 | * 내장 모듈 threading의 Lock 클래스는 파이썬의 표준 상호 배제 잠금 구현이다.
145 |
--------------------------------------------------------------------------------
/files/BetterWay42_Use_functoolswraps.md:
--------------------------------------------------------------------------------
1 | ## Better Way 42. functools.wraps로 함수 데코레이터를 정의하자
2 |
3 | #### 216쪽
4 |
5 | * Created : 2017/08/14
6 | * Modified: 2019/06/21
7 |
8 |
9 |
10 |
11 | ## 1. 데코레이터 사용의 문제점
12 |
13 | 파이썬에는 함수에 적용할 수 있는 데코레이터(decorator)라는 특별한 문법이 있다.(이에 대한 설명은 생략한다. 데코레이터에 대한 개념 부족 시 다른 포스트를 참고한다.) 데코레이터는 감싸고 있는 함수를 호출하기 전후에 추가로 코드를 실행할 수 있게 한다. 이는 활용 가능선이 무궁무진한데, 입력, 반환값에 접근, 디버깅, 시맨틱 강조 등 등 여러 상황에 유용하다.
14 |
15 | 예를 들어 함수를 호출할 때 인수와 반환 값을 출력하고 싶다고 하자. 특히, 재귀호출에서 함수 호출의 스택을 디버깅할 때 도움이 된다. 그럼 이런 데코레이터를 정의해보자.
16 |
17 |
18 | ```python
19 | # 1. 함수 호출 시 인수와 반환 값을 출력
20 | def trace(func):
21 | def wrapper(*args, **kwargs):
22 | result = func(*args, **kwargs)
23 | print("{}({}, {}) -> {}".format(func.__name__, args, kwargs, result))
24 | return result
25 | return wrapper
26 | ```
27 |
28 | 잘 알다시피 '@' 기호로 데코레이터를 함수에 적용한다. 데코레이터를 적용할 함수로 유명한 피보나치 함수를 만들어보자.
29 |
30 | ```python
31 | # 1. trace를 통해 재귀함수의 스택 추적
32 |
33 | @trace
34 | def fibonacci(n):
35 | """n번 째 피보나치 수를 반환한다."""
36 | if n in (0, 1):
37 | return n
38 | return fibonacci(n - 2) + fibonacci(n - 1)
39 | ```
40 |
41 | **`@` 기호는 감싸고 있는 함수를 인수로 사용하여 해당 데코레이터를 호출한 후 반환값을 같은 스코프에 있는 원래 이름에 할당하는 코드에 상응한다.** 즉, 위의 데코레이터를 사용한 함수식은 다음 코드와 동일하다.
42 |
43 | ```python
44 | fibonacci = trace(fibonacci)
45 | ```
46 |
47 | 이 데코레이터 함수를 호출하면 fibonacci 실행 전후에 wrapper 코드를 실행하여 재귀 스택의 각 단계마다 인수와 반환 값을 출력한다.
48 |
49 | ```python
50 | fibonacci(3)
51 |
52 | fibonacci((1,), {}) -> 1
53 | fibonacci((0,), {}) -> 0
54 | fibonacci((1,), {}) -> 1
55 | fibonacci((2,), {}) -> 1
56 | fibonacci((3,), {}) -> 2
57 | ```
58 |
59 | 3번째 피보나치 수를 구하자 각 재귀호출의 결과가 한 줄씩 차치하며 출력되는 것을 확인할 수 있다.
60 |
61 |
62 |
63 | 이 코드는 잘 작동하지만 의도하지 않은 부작용을 일으킨다. **데코레이터에서 반환한 값, 그러니까 함수의 이름이 fibonacci가 아니다.**
64 |
65 | ```python
66 | print(fibonacci)
67 |
68 | .wrapper at 0x7fdb458b3400>
69 | ```
70 |
71 | 그러니까 **'fibonacci'라는 변수명에 할당된 함수 이름이 실제로는 'wrapper'인 것이다. 문제의 원인은 어렵지 않은데, 데코레이터의 작동방식에서 알 수 있듯이 함수를 작성 후 데코레이터를 씌우면 데코레이터가 반환한 wrapper 함수가 함수의 이름이 되기 때문이다.** 이 동작은 디버거나 객체 직렬화처럼 객체 내부를 조사하는 도구를 사용할 때 문제가 될 수 있다.
72 |
73 | 좀더 살펴보면 데코레이터를 적용한 fibonacci 함수에는 help 내장 함수가 쓸모가 없다.
74 |
75 |
76 |
77 | ```python
78 | help(fibonacci)
79 |
80 | Help on function wrapper in module __main__:
81 |
82 | wrapper(*args, **kwargs)
83 |
84 | # 'fibonacci'에 대한 사용설명서에서 뜬금없이 wrapper에 대한 도움말을 제시함.
85 | # 사용자는 멘붕;
86 | ```
87 |
88 |
89 |
90 |
91 | ## 2. 해결책
92 |
93 | **해결책은 내장 모듈 functools의 wraps 헬퍼 함수를 사용하는 것이다.** 이 함수는 데코레이터를 작성하는 데 이용하는 데코레이터로, **이 함수를 wrapper 함수에 적용하면 내부 함수에 있는 중요 메타데이터가 모두 외부함수로 복사된다.**
94 |
95 |
96 | ```python
97 | from functools import wraps
98 |
99 | def trace(func):
100 | @wraps(func)
101 | def wrapper(*args, **kwargs):
102 | result = func(*args, **kwargs)
103 | print("{}({}, {}) -> {}".format(func.__name__, args, kwargs, result))
104 | return result
105 | return wrapper
106 | ```
107 |
108 | 사용법은 흔히 wrapper라고 이름 짓는 반환함수에 wraps를 데코레이터로 씌우는 것이다. 이제 help 함수를 실행하면 우리가 원한 결과를 얻을 수 있다.
109 |
110 | ```python
111 | help(fibonacci)
112 |
113 |
114 | Help on function fibonacci in module __main__:
115 |
116 | fibonacci(n)
117 | n번 째 피보나치 수를 반환한다.
118 | ```
119 |
120 | help를 호출한 예는 데코레이터가 어떤 식으로 미묘한 문제를 일으키는지 보여주는 사례 중 하나일 뿐이다. 파이선 함수에는 여러 표준 속성(\_\_name\_\_, \_\_module\_\_ 등)이 있으며, 언어에서 함수들의 인터페이스를 유지하려면 이 속성들을 반드시 보호해야 한다. 이때 wraps를 사용하면 언제나 올바른 동작을 얻을 수 있다.
121 |
122 |
123 |
124 |
125 | ## 3. 핵심 정리
126 |
127 | * 데코레이터는 런타임에 한 함수로 다른 함수를 수정할 수 있게 해주는 파이썬 문법이다.
128 | * 데코레이터를 사용하면 디버거와 같이 객체 내부를 조사하는 도구가 이상하게 동작할 수도 있다.
129 | * 직접 데코레이터를 정의할 때 이런 문제를 피하려면 내장 모듈 functools의 wraps 데코레이터를 이용하자.
130 |
--------------------------------------------------------------------------------
/files/BetterWay43_UseContextlib.md:
--------------------------------------------------------------------------------
1 | ## Better Way 43. 재사용 가능한 try/finally 동작을 만들려면 contextlib과 with 문을 고려하자
2 |
3 | #### 218쪽
4 |
5 | * Created : 2017/08/17
6 | * Modified: 2019/06/22
7 |
8 |
9 |
10 | ## 1. contextmanager 소개
11 |
12 | **파이썬의 'with' 문은 코드를 특별한 컨텍스트(context)에서 실행함을 나타내는 데 사용한다.** 예를 들어 [38장](https://github.com/shoark7/Effective-Python/blob/master/files/BetterWay38_UseLockForRaceConditionInThread.md)에서 살펴본 것처럼 'with'문에 MUTEX를 사용하여 잠금이 설정되어 있는 동안만 들여 쓴 코드를 쓰는 예제를 확인했다.
13 |
14 | ```python
15 | import threading
16 |
17 | lock = threading.Lock()
18 | with lock:
19 | print("Lock is held by me!")
20 | ```
21 |
22 | **Lock 클래스가 with문을 제대로 지원하는 덕분에 위의 코드는 다음의 try / finally 구문에 상응한다.**
23 |
24 | ```python
25 | lock.acquire()
26 | try:
27 | print("Lock is help by me!")
28 | finally:
29 | lock.release()
30 | ```
31 |
32 | 위의 두 코드가 같은 일을 한다면 어떤 방법이 더 좋은 practice일까? 일반적으로 같은 성능이면 코드를 줄일 수 있으면 좋다. 그런 의미에서 try / finally 구문에서 반복되는 코드를 작성할 필요가 없는 with 문 버전이 더 낫다.
33 |
34 | **내장 모듈 contextlib를 사용하면 객체와 함수를 with 문에 사용할 수 있게 만들기가 쉽다.** 이 모듈은 간단한 함수를 with 문에 사용할 수 있게 해주는 `contextmanager` 데코레이터를 포함한다. 이 데코레이터를 이용하는 방법이 \_\_enter\_\_, \_\_exit\_\_라는 특별한 메소드를 담은 새 클래스를 정의하는 표준 방법보다 훨씬 쉽다.
35 |
36 |
37 |
38 |
39 | 이번에 사용할 예제는 내장 `logging` 모듈로 만들어보자. 로그는 상황에 따라 다양한 수준의 로그를 남기는데 가끔씩은 코드의 특정 영역에 더 많은 디버깅 로그를 넣고 싶다고 해보자. 여기서는 로깅 심각성 수준(severity level) 두 개로 로그를 남기는 함수를 정의한다.
40 |
41 | 이 포스트에서는 `logging` 모듈의 자세한 내용은 생략하도록 하겠다. 필요한 분들은 [공식 문서](https://docs.python.org/3/library/logging.html)를 확인하기 바란다.
42 |
43 |
44 | ```python
45 | def my_function():
46 | logging.debug("Some bug happended")
47 | logging.error("Something very bad;")
48 | logging.debug("Another small bug got caught!")
49 | ```
50 |
51 | 따로 값을 주지 않았을 때, 로깅 프로그램의 기본 로그 수준은 'WARNING'이다. 따라서 함수를 실행하면 에러 로그만 출력되고 디버그 로그는 출력되지 않는다.
52 |
53 | ```python
54 | import logging
55 |
56 | def my_function():
57 | logging.debug("Some bug happended")
58 | logging.error("Something very bad;")
59 | logging.debug("Another small bug got caught!")
60 |
61 | my_function()
62 |
63 |
64 | ERROR:root:Something very bad; # debug수준 에러는 무시됨.
65 | ```
66 |
67 |
68 |
69 | 컨텍스트 매니저를 정의하여 이 함수의 로그 수준을 임시로 높일 수 있다. 이 헬퍼 함수는 with 블록에서 코드를 실행하기 전에 로그 심각성 수준을 높이고 실행 후에는 다시 낮춘다.
70 |
71 |
72 | ```python
73 | from contextlib import contextmanager
74 |
75 | @contextmanager
76 | def debug_logging(level):
77 | logger = logging.getLogger()
78 | old_level = logger.getEffectiveLevel()
79 | logger.setLevel(level)
80 |
81 | try:
82 | yield
83 | finally:
84 | logger.setLevel(old_level)
85 | ```
86 |
87 | 여기서 `yield` 식을 주목하자. 어떤 반환값도 없는 것을 볼 수 있는데 **이 'contextmanager' 데코레이터에서는 yield 지점이 'with' 블록의 내용이 실행되는 지점이다.** with 블록에서 일어나는 모든 예외를 yield 표현식이 다시 일으키므로 헬퍼 함수로 처리할 수 있다.
88 |
89 | 이제 같은 로깅 함수를 *debug\_logging* 컨텍스트에서 호출한다. 이번에는 with 블록 안에 있는 디버그 메시지가 모두 화면에 출력된다. 같은 함수를 with 블록 외부에서 실행하면 디버깅 메시지가 출력되지 않는다.
90 |
91 | ```python
92 | with debug_logging(logging.DEBUG):
93 | print("Inside:")
94 | my_function()
95 |
96 | print("\nAfter:")
97 | my_function()
98 |
99 |
100 | Inside:
101 | DEBUG:root:Some bug happended
102 | ERROR:root:Something very bad;
103 | DEBUG:root:Another small bug got caught!
104 |
105 | After:
106 | ERROR:root:Something very bad;
107 | ```
108 |
109 |
110 |
111 |
112 | ## 2. `as`로 타깃 지정하기
113 |
114 | 파이썬에서 파일을 열고 닫을 때 'as'로 파일 객체를 지정했던 것을 기억할 것이다.
115 |
116 | ```python
117 | with open("somefile.txt") as fd: # fd라는 파일 객체 선언
118 | pass
119 | ```
120 |
121 | 이것을 또한 contextmanager를 통해 지정할 수 있다. 이 기능을 이용하면 with 블록 안에 있는 코드에서 직접 컨텍스트와 상호작용할 수 있다.
122 |
123 | 함수에서 'as' 타깃에 값을 제공할 수 있게 하려면 컨텍스트 매니저에서 yield를 사용하여 값을 넘겨주기만 하면 된다.
124 |
125 | 예를 들어, 다음은 Logger 인스턴스를 가져와서 심각성 수준을 설정한 후 yield 인스턴스를 as에 전달하도록 정의한 예다.
126 |
127 | ```python
128 | @contextmanager
129 | def log_level(level, name):
130 | logger = logging.getLogger(name)
131 | old_level = logger.getEffectiveLevel()
132 | logger.setLevel(level)
133 |
134 | try:
135 | yield logger
136 | finally:
137 | logger.setLevel(old_level)
138 | ```
139 |
140 | with 블록에서 로깅 심각성 수준을 충분히 낮게 설정했으니 as 값으로 debug 같은 로깅 메소드를 호출하면 출력이 나올 것이다. logging 모듈의 기본 로깅 심각성 수준은 WARNING이므로 logging 모듈을 직접 사용하면 아무것도 출력되지 않는다.
141 |
142 | ```python
143 | @contextmanager
144 | def log_level(level, name):
145 | logger = logging.getLogger(name)
146 | old_level = logger.getEffectiveLevel()
147 | logger.setLevel(level)
148 |
149 | try:
150 | yield logger
151 | finally:
152 | logger.setLevel(old_level)
153 |
154 |
155 | with log_level(logging.DEBUG, 'my-log') as logger:
156 | logger.debug("This is my debug message!")
157 | logging.debug("This will not be printed")
158 |
159 |
160 | DEBUG:my-log:This is my debug message! # logger의 로그만 출력됨.
161 | ```
162 |
163 | with 문이 종료한 후에 'my-log'라는 Logger의 디버그 로깅 메소드를 호출하면, 기본 로깅 심각성 수준으로 되돌아간 뒤라서 아무것도 출력되지 않는다. 물론 오류 로그 메시지는 항상 출력된다.
164 |
165 | ```python
166 | logger = logging.getLogger('my-log')
167 | logger.debug("Debug message will not print")
168 | logger.error("Something really bad happened")
169 |
170 |
171 | ERROR:my-log:Something really bad happened
172 | ```
173 |
174 |
175 |
176 | ## 3. 핵심 정리
177 |
178 | * with 문을 이용하면 try / finally 블록의 로직을 재사용할 수 있고, 코드를 깔끔하게 만들 수 있다.
179 | * 내장 모듈 contextlib의 contextmanager 데코레이터를 이용하면 직접 작성한 함수를 with 문에서 쉽게 사용할 수 있다.
180 | * 컨텍스트 매니저에서 넘겨준 값은 with 문의 as 부분에 할당된다. 컨텍스트 매니저에서 값을 반환하는 방법은 코드에서 특별한 컨텍스트에 직접 접근하려는 경우에 유용하다.
181 |
--------------------------------------------------------------------------------
/files/BetterWay45_UseDatetimeForLocalTime.md:
--------------------------------------------------------------------------------
1 | ## Better Way 45. 지역 시간은 time이 아닌 datetime으로 표현하자
2 |
3 | #### 231쪽
4 |
5 | * Created : 2019/06/24
6 | * Modified: 2019/06/24
7 |
8 |
9 |
10 | ## 1. time vs datetime 모듈
11 |
12 | 협정 세계시(UTC, Coordinated Universal Time)는 시간대에 의존하지 않는 표준 시간 표현이다. UTC는 유닉스 기원 이후로 지나간 초로 시간을 표현하는 컴퓨터에서 잘 작동한다. 하지만 사람에게는 잘 안 맞는다.
13 |
14 | 사람이 사용하는 시간은 현재 자신이 있는 위치를 기준으로 한다. 사람들은 'UTC 15:00 - 7시'가 아니라 '정오' 혹은 '아침 8'시라고 말한다. **프로그램에서 시간을 처리해야 한다면 사람이 이해하기 쉽게 UTC와 지역 시간 사이에서 변환해야 한다.**
15 |
16 | 파이썬은 두 가지 시간대 변환 방법을 제공한다. 내장 모듈 time을 사용하는 이전 방법은 치명적인 오류가 일어날 가능성이 크다. 내장 모듈 datetime을 사용하는 새로운 방법은 커뮤니티에서 만든 `pytz` 패키지의 도움을 받아 훌륭하게 동작한다.
17 |
18 | datetime이 최선의 선택이고, time을 사용하지 말아야 하는 이유를 완전히 이해하는 것이 이번 장의 내용이다.
19 |
20 |
21 |
22 |
23 | ## 2. time 모듈 사용하기
24 |
25 | 내장 모듈 time의 localtime 함수는 유닉스 타임스탬프(UTC에서 유닉스 기원, epoch 이후 지난 초)를 호스트 컴퓨터의 시간대(나는 Asia/Seoul)와 일치하는 지역 시간으로 변환한다.
26 |
27 | ```python
28 | from time import localtime, strftime, time
29 |
30 | now = time()
31 | print('now is', now)
32 |
33 | local_tuple = localtime(now)
34 | time_format = '%Y-%m-%d %H:%M:%S'
35 | time_str = strftime(time_format, local_tuple)
36 | print(time_str)
37 |
38 | now is 1561372402.3313987
39 | 2019-06-24 19:33:22
40 | # 현재 저녁 7시이다;
41 | ```
42 |
43 | 때로는 지역 시간으로 사용자 입력을 받아서 UTC 시간으로 변환하는 것처럼 반대로 처리해야 하는 경우도 있다. 이럴 때는 `strptime` 함수로 시간 문자열을 파싱한 후에 `mktime` 함수로 지역 시간을 유닉스 타임스탬프로 변환하면 된다.
44 |
45 | ```python
46 | from time import mktime, strptime
47 |
48 | time_tuple = strptime(time_str, time_format)
49 | utc_now = mktime(time_tuple)
50 | print(utc_now)
51 |
52 | 1561372402.0
53 | ```
54 |
55 | 일단 단순 변환은 문제없이 동작한다. 하지만 **한 시간대의 지역 시간을 다른 시간대의 지역 시간으로 변환하고 싶다고 할 때는 이야기가 다르다.**
56 |
57 | **time, localtime, strptime 함수의 반환 값을 직접 조작해서 시간대를 변환하는 건 좋지 못한 생각이다.** 시간대는 지역 규칙에 따라 모든 시간을 변경한다. 이 과정은 직접 처리하기엔 너무 복잡하며, 특히 전세계 모든 도시의 비행기의 출발, 도착 시간을 처리한다면 더욱 복잡해진다.
58 |
59 | 많은 운영체제에서 시간대 변경을 자동으로 관리하는 설정 파일을 갖추고 있다. **문제는 플랫폼에 의존적인 time 모듈의 특성이다.** 실제 동작은 내부의 C 함수가 호스트 운영체제와 어떻게 동작하느냐에 따라 결정된다. 이와 같은 동작 때문에 파이썬의 time 모듈의 기능을 신뢰하기 어렵다.
60 |
61 | 따라서 이런 목적으로는 time 모듈을 사용하지 말아야 한다. time을 사용해야 한다면 UTC와 호스트 컴퓨터의 지역 시간을 변환하는 목적으로만 사용해야 한다. 다른 형태의 변환에는 datetime 모듈을 사용해야 한다.
62 |
63 |
64 |
65 | ## 3. datetime과 pytz 모듈로 시간대 변환하기
66 |
67 | 파이썬에서 시간을 표현하는 두 번째 방법은 내장 모듈 datetime의 datetime 클래스를 사용하는 것이다. time 모듈과 마찬가지로 datetime은 UTC에서의 현재 시각을 지역 시간으로 변경하는 데 사용할 수 있다.
68 |
69 | 다음은 현재 시각을 UTC로 얻어와서 Asia, Seoul 지역 시간으로 변경하는 코드다.
70 |
71 |
72 | ```python
73 | from datetime import datetime, timezone
74 |
75 | now = datetime.now()
76 | now_utc = now.replace(tzinfo=timezone.utc)
77 | now_local = now_utc.astimezone()
78 | print(now_local)
79 |
80 | 2019-06-25 05:16:37.479484+09:00
81 | # 서울은 UTC 기준 +9임
82 | ```
83 |
84 | datetime 모듈로도 지역 시간을 다시 UTC의 유닉스 타임스탬프로 쉽게 변경할 수 있다.
85 |
86 |
87 | ```python
88 | time_str = '2019-06-24 20:18:30'
89 | now = datetime.strptime(time_str, time_format)
90 | time_tuple = now.timetuple()
91 | utc_now = mktime(time_tuple)
92 | print(utc_now)
93 |
94 | 1561375110.0
95 | ```
96 |
97 | datetime 모듈은 time 모듈과 달리 한 지역 시간을 다른 지역 시간으로 신뢰성 있게 변경한다. 하지만 tzinfo 클래스와 관련 메소드를 이용한 시간대 변환 기능만 제공한다. 빠진 부분은 UTC 이외의 시간대 정의다.
98 |
99 | 다행히도 파이썬 커뮤니티에서는 이 허점을 pypi에서 다운로드할 수 있는 pytz 모듈로 해결하고 있다. pytz는 필요한 모든 시간대에 대한 정의를 담은 전체 데이터베이스를 포함한다.
100 |
101 | pytz를 효과적으로 사용하려면 항상 지역 시간을 UTC로 먼저 변경해야 한다. 그러고 나서 UTC 값에 필요한 datetime 연산(오프셋 지정 등)을 수행한다. 그런 다음 마지막 단계로 지역 시간으로 변환한다.
102 |
103 | 예를 들어 다음은 서울 기준 시간을 미국 샌프란시스코 기준시로 변환하는 코드다.
104 |
105 | ```python
106 | import pytz
107 |
108 | time_seoul = '2019-06-24 20:28:30'
109 | naive_time = datetime.strptime(time_seoul, time_format)
110 | seoul = pytz.timezone('Asia/Seoul')
111 | localized = seoul.localize(naive_time)
112 | utc_dt = pytz.utc.normalize(localized.astimezone(pytz.utc))
113 |
114 | print(utc_dt)
115 |
116 | 2019-06-25 00:28:30+00:00
117 | ```
118 |
119 | 서울 로컬 시간을 표준시로 변경했으니 이를 사용해서 샌프란시스코 지역 시간으로 변환해보자.
120 |
121 | ```python
122 | pacific = pytz.timezone("US/Pacific")
123 | sf_dt = pacific.normalize(utc_dt.astimezone(pacific))
124 | print(sf_dt)
125 |
126 | 2019-06-24 04:28:30-07:00
127 | ```
128 |
129 | datetime과 pytz를 이용하면 이런 변환이 호스트 컴퓨터에서 구동하는 운영체제와 상관없이 모든 환경에서 동일하게 동작한다.
130 |
131 |
132 |
133 |
134 | ## 4. 핵심 정리
135 |
136 | * 서로 다른 시간대를 변환하는 데는 time 모듈을 사용하지 말자.
137 | * pytz 모듈과 내장 모듈 datetime으로 서로 다른 시간대 사이에서 시간을 신뢰성 있게 변환하자.
138 | * 항상 UTC로 시간을 표현하고, 시간을 표시하기 전에 마지막 단계로 UTC 시간을 지역 시간으로 변환하자.
139 |
--------------------------------------------------------------------------------
/files/BetterWay46_UseBuiltinAlgorithmsAndDataStructures.md:
--------------------------------------------------------------------------------
1 | ## Better Way 46. 내장 알고리즘과 자료구조를 사용하자
2 |
3 | #### 237쪽
4 |
5 | * Created : 2019/06/28
6 | * Modified: 2019/06/28
7 |
8 |
9 |
10 | ## 1. 기본 자료구조의 한계
11 |
12 | 많은 데이터를 처리하는 프로그램을 구현할 때 기본 내장 자료구조(list, tuple, dict, set)을 사용하다보면 결국 성능의 한계에 다다르게 된다. 이는 이 자료구조들의 결함 때문이라기 보다는 기본 내장 자료구조이기 때문에 성능을 위해 최적화되지 않고 lightweight한 성격을 지니기 때문이다. 즉 **이들은 사용하기 쉽고 무게도 가볍지만, 많은 데이터를 다루고 성능이 중요한 상황일 때 관련 알고리즘들의 성능이 매우 뛰어나지는 않다.** 파이썬의 다른 내장 자료구조나 알고리즘 등도 이런 성격을 띤다.
13 |
14 | 하지만 때로는 성능을 중요시해야 하는 경우가 생기고 다행히도 파이썬 표준 라이브러리는 필요한 만큼 많은 알고리즘과 자료구조를 갖추고 있다. 꼭 성능 때문이 아니더라도 이런 공통 알고리즘과 자료구조를 사용하면 삶이 더 윤택해진다.
15 |
16 | 오늘 이 시간에는 파이썬이 갖추고 있는 수많은 다른 자료구조 중에서도 특히 쓸모 있는 몇 가지를 살펴보도록 한다.
17 |
18 |
19 |
20 | ## 2. Double Ended Queue
21 |
22 | 내장 collections 모듈의 [deque](https://docs.python.org/3/library/collections.html#collections.deque) 자료구조는 Double Ended Queue다. 즉, 양쪽 끝이 열려 있는 큐로서, 이 자료구조는 큐의 양쪽 끝에서 아이템을 삽입하거나 삭제할 때 항상 일정한 시간이 걸리는 연산을 제공한다. 이와 같은 기능은 FIFO 큐를 만들 때 이상적이다.
23 |
24 | ```python
25 | from collections import deque
26 |
27 | fifo = deque()
28 | fifo.append(1)
29 | print(fifo.popleft())
30 |
31 | 1
32 | ```
33 |
34 | 물론 이런 작업은 내장 `list`를 써도 얼마든지 할 수 있다. 하지만 **리스트가 자신의 오른쪽 끝에 원소를 추가, 삭제하는 작업은 O(1)에 해결할 수 있는 데 반해, 왼쪽 끝에 원소를 추가, 삭제하는 데는 O(n)의 시간복잡도가 든다.** 이는 리스트의 중요한 한계로 따라서 list를 FIFO 큐로 쓰는 것은 성능이 중요할 때는 좋은 선택이 아니다.
35 |
36 |
37 |
38 |
39 | ## 3. 정렬된 딕셔너리
40 |
41 | 알다시피, 내장 `dict`는 키가 정렬되어 있지 않다. 즉, 같은 키와 값을 담은 dict를 순회해도 다른 순서가 나올 수 있다는 의미다. 이런 동작은 딕셔너리의 빠른 해시테이블을 구현하는 방식이 만들어낸 뜻밖의 부작용이다.
42 |
43 | **collections 모듈의 [OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict) 클래스는 키가 삽입된 순서를 유지하는 특별한 딕셔너리다.** OrderedDict는 내장 dict의 서브클래스로서 이름에서부터 유추할 수 있듯이 OrderedDict의 키를 순회하는 것은 예상 가능한 동작이다. 따라서 모든 코드를 확정하고 만들 수 있으므로 테스팅과 디버깅을 아주 간단하게 할 수 있다.
44 |
45 |
46 | ```python
47 | from collections import OrderedDict
48 |
49 | a = OrderedDict()
50 | a['foo'] = 1
51 | a['bar'] = 2
52 |
53 | b = OrderedDict()
54 | b['foo'] = 'red'
55 | b['bar'] = 'blue'
56 |
57 |
58 | for v1, v2 in zip(a.values(), b.values()):
59 | print(v1, v2)
60 |
61 |
62 | 1 red
63 | 2 blue
64 | ```
65 |
66 |
67 |
68 | ## 4. 기본값이 설정된 딕셔너리
69 |
70 | 딕셔너리의 중요한 사용처 중 하나는 빈도를 관리하고 추적하는 작업이다. 이때 한 가지 문제점이 따라오는데, **딕셔너리에서 어떤 키가 이미 존재한다고 가정할 수 없다는 점이다.** 이 문제 때문에 딕셔너리에 저장된 카운터를 증가시키는 것처럼 간단한 작업도 코드가 지저분해진다.
71 |
72 | ```python
73 | sentence = "I wanna be a doctor"
74 | counter = {}
75 |
76 | for c in sentence:
77 | if c.isalpha():
78 | # 다음 두 줄이 깔끔하지 못하다
79 | if c not in counter:
80 | counter[c] = 0
81 | counter[c] += 1
82 |
83 | print(counter)
84 |
85 | {'I': 1, 'w': 1, 'a': 3, 'n': 2, 'b': 1, 'e': 1, 'd': 1, 'o': 2, 'c': 1, 't': 1, 'r': 1}
86 | ```
87 |
88 | **collections의 [defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict) 자료구조는 키가 존재하지 않으면 자동으로 기본값을 저장하도록 하여 이런 작업을 간소화한다. 할 일은 그저 키가 없을 때마다 기본값을 반환할 함수를 제공하는 것뿐이다.**
89 |
90 | ```python
91 | from collections import defaultdict
92 |
93 | sentence = "I wanna be a doctor"
94 | counter = defaultdict(int)
95 |
96 | for c in sentence:
97 | if c.isalpha():
98 | counter[c] += 1
99 |
100 | print(counter)
101 |
102 | defaultdict(, {'I': 1, 'w': 1, 'a': 3, 'n': 2, 'b': 1, 'e': 1, 'd': 1, 'o': 2, 'c': 1, 't': 1, 'r': 1})
103 | ```
104 |
105 | **defaultdict는 인자로 함수를 받는데, 만약 찾는 키가 없으면 제공받은 함수의 결과가 키의 기본값으로 설정된다.** int 함수를 인자없이 쓰면 0이 반환되기 때문에 딕셔너리에 키가 없으면 자동으로 0이 할당된 뒤 다시 1이 추가된다.(_counter[c] += 1_)
106 |
107 |
108 |
109 |
110 | ## 5. 힙 큐
111 |
112 | **힙(heap)은 우선순위 큐(priority queue)를 유지하는 유용한 자료구조다.** 이 자료구조는 자료구조학의 진정한 효자로서 잘 모르면 [관련 문서](https://en.wikipedia.org/wiki/Heap_(data_structure))를 꼭 확인하기 바란다.
113 |
114 | 힙과 관련된 알고리즘을 파이썬에서도 제공하는데, [heapq](https://docs.python.org/3/library/heapq.html) 모듈은 표준 list 타입으로 힙을 생성하는 heappush, heappop, nsmallest 등의 함수를 제공한다.
115 |
116 | ```python
117 | from heapq import heappop, heappush
118 |
119 | a = []
120 | heappush(a, 5)
121 | heappush(a, 3)
122 | heappush(a, 7)
123 | heappush(a, 4)
124 |
125 | print(heappop(a), heappop(a), heappop(a), heappop(a))
126 |
127 | 3 4 5 7
128 | ```
129 |
130 | **파이썬의 heap은 최소값 힙(mean heap)으로서, 값을 하나씩 빼낼 때마다 최소값을 O(log n)의 시간복잡도로 추출할 수 있다.** 표준 파이썬 리스트로 같은 동작을 수행하면 시간이 선형적으로 증가하기 때문에 heap은 최소값(또는 최대값)을 추가, 삭제하는 연산에 효율적이다.
131 |
132 |
133 |
134 | ## 6. 바이섹션
135 |
136 | list에서 아이템을 검색하는 작업을 index 메소드를 사용해서 할 때 리스트의 길이에 비례한 선형적 시간이 걸린다.
137 |
138 |
139 | ```python
140 | x = list(range(10 ** 6))
141 | i = x.index(991234)
142 | ```
143 |
144 | [bisect 모듈](https://docs.python.org/3/library/bisect.html)은 정렬된 아이템 시퀀스를 대상으로한 효율적인 바이너리 검색을 제공하는 bisect\_left 같은 함수를 제공한다. bisect\_left가 반환한 인덱스는 시퀀스에 들어간 값의 삽입 지점이다.
145 |
146 | ```python
147 | from bisect import bisect_left
148 |
149 | x = list(range(10 ** 6))
150 | i = bisect_left(x, 991234)
151 | print(i)
152 |
153 | 991234
154 | ```
155 |
156 | **바이너리 검색의 복잡도는 로그 형태로 증가한다.** 다시 말해 아이템 백만 개를 담은 리스트를 bisect로 검색할 때 걸리는 시간은 아이템 14개(log2 1000000)를 담은 리스트를 index 메소드로 순차 검색할 때 걸리는 시간과 거의 같다.
157 |
158 |
159 |
160 |
161 | ## 7. 이터레이터 도구
162 |
163 | **내장 모듈 [itertools](https://docs.python.org/3/library/itertools.html)는 이터레이터를 구성하거나 이터레이터와 상호작용하는 데 유용한 함수를 다수 포함한다.** 파이썬 2에서는 이런 기능을 모두 이용하진 못하지만, 모듈 문서에 있는 예제를 참고하면 간단하게 만들 수 있다. 더 자세한 정보는 itertools 모듈에 헬핑을 하면 확인해볼 수 있다.
164 |
165 | itertools에 있는 함수는 크게 세 가지 범주로 나눌 수 있다.
166 |
167 | * 이터레이터 연결
168 | - chain: 여러 이터레이터를 순차적인 이터레이터 하나로 결합한다.
169 | - cycle: 이터레이터의 아이템을 영원히 반복한다.
170 | - tee: 이터레이터 하나를 병렬 이터레이터 여러 개로 나눈다.
171 | - zip\_longest: 길이가 서로 다른 이터레이터들에도 잘 동작하는 내장 함수 zip의 변형이다.
172 | * 이터레이터에서 아이템 필터링
173 | - islice: 복사 없이 이터레이터를 숫자로 된 인덱스로 슬라이스한다.
174 | - takewhile: 서술 함수(predicate function)가 True를 반환하는 동안 이터레이터의 아이템을 반환한다.
175 | - dropwhile: 서술 함수가 처음으로 False를 반환하고 나면 이터레이터의 아이템을 반환한다.
176 | - filterfalse: 서술 함수가 False를 반환하는 이터레이터의 모든 아이템을 반환한다. 내장 함수 filter의 반대기능을 한다.
177 | * 이터레이터에 있는 아이템들의 조합
178 | - product: 이터레이터에 있는 아이템들의 카르테시안 곱을 반환한다. 깊게 중첩된 리스트 컴프리헨션에 대한 훌륭한 대안이다.
179 | - permutations: 이터레이터에 있는 아이템을 담은 길이 N의 순서 있는 순열을 반환한다.
180 | - combinations: 이터레이터에 있는 아이템을 중복되지 않게 담은 길이 N의 순서 없는 조합을 반환한다.
181 |
182 |
183 |
184 | itertools에는 이것말고도 다른 기능이 더 많고 나도 다 써보지도 않았다. 하지만 몇몇은 실제 개발에서 정말 유용할 수 있어서 이 내장 모듈은 살펴볼 가치가 있다.
185 |
186 |
187 |
188 | ## 8. 핵심 정리
189 |
190 | * 알고리즘과 자료구조를 표현하는 데는 파이썬의 내장 모듈을 사용하자.
191 | * 이 기능들을 직접 재구현하지는 말자. 올바르게 만들기가 어렵기 때문이다.
192 |
--------------------------------------------------------------------------------
/files/BetterWay47_UseDecimalForPrecision.md:
--------------------------------------------------------------------------------
1 | ## Better Way 47. 정밀도가 중요할 때는 decimal을 사용하자
2 |
3 | #### 243쪽
4 |
5 | * Created : 2019/06/29
6 | * Modified: 2019/06/29
7 |
8 |
9 |
10 | ## 1. 부동 소수점 연산의 문제점
11 |
12 | 파이썬은 숫자 데이터를 다루는 코드를 작성하기에 아주 뛰어난 언어다. 파이썬의 정수 타입은 현실적인 크기의 값을 모두 표현할 수 있다. 다른 언어처럼, 32 비트, 64비트에 얽매이지 않는다. 배정밀도 부동 소수점 타입은 IEEE 754 표준을 따른다. 심지어 파이썬 언어는 허수 값을 표현하는 내장 타입(complex)도 갖고 있다. 그러나 이것만으로 모든 상황을 충족시키지는 못한다.
13 |
14 | 예를 들어, 고객에게 부과할 국제 전화 요금을 계산한다고 하자. 고객이 몇 분, 몇 초간 통화했는지 알고 있다. 또한 서울에서 울릉도를 건너 통화했을 때의 요율 등도 정해져 있다. 그렇다면 요금을 얼마나 지불해야 할까?
15 |
16 | 부동 소수점 연산을 통한 계산은 일견으로는 합리적으로 보인다.
17 |
18 |
19 | ```python
20 | rate = 1.45
21 | seconds = 10 * 60 + 42
22 | cost = rate * seconds / 60
23 |
24 | print(cost)
25 |
26 | 15.514999999999999
27 | ```
28 |
29 | 부동 소수점의 전형적인 문제가 드러났다. **바로 소수점간 연산시 소수값이 지리멸렬하게 남는다는 것.** 뭐 그래도 괜찮다. 우리는 소수점을 반올림하는 `round`라는 내장 함수를 가지고 있으니까.
30 |
31 |
32 | ```python
33 | print(round(cost, 2))
34 |
35 | 15.51
36 | ```
37 |
38 |
39 |
40 | 이에 더해 연결 비용이 훨씬 저렴한 곳 사이에서 일어나는 아주 짧은 통화도 지원해야 한다고 하자. 다음은 분당 0.05원 요율로 5초 동안 일어난 통화의 요금을 계산한 것이다.
41 |
42 | ```python
43 | rate = 0.05
44 | seconds = 5
45 | cost = rate * seconds / 60
46 |
47 | print(cost)
48 |
49 | 0.004166666666666667
50 | ```
51 |
52 | 부동 소수점의 결과가 너무 작아서 반올림하면 0이 된다. 이렇게 계산하면 안 된다.
53 |
54 | ```python
55 | print(round(cost, 2))
56 |
57 | 0.0
58 | ```
59 |
60 | 확실히 뭔가 해결책이 필요하다.
61 |
62 |
63 |
64 | ## 2. 해결책: Decimal 사용하기
65 |
66 | 해결책은 **내장 모듈 [decimal](https://docs.python.org/3/library/decimal.html)의 Decimal 클래스를 사용하는 것이다. Decimal 클래스는 기본적으로 소수점이 28자리인 고정 소수점 연산을 제공하며 필요하다면 더 늘릴 수도 있다.** Decimal을 사용하면 IEEE 754 부동 소수점 수의 정확도 문제를 피해갈 수 있다. 또한 반올림 연산을 더 세밀하게 제어할 수도 있다.
67 |
68 | 앞선 예제의 울릉도 문제를 Decimal로 다시 계산해보자.
69 |
70 |
71 | ```python
72 | from decimal import Decimal
73 |
74 | rate = Decimal('1.45')
75 | seconds = Decimal(str(10 * 60 + 42))
76 | cost = rate * seconds / Decimal('60')
77 | print(cost)
78 |
79 | 15.515
80 | ```
81 |
82 | 아까와 달리 정확한 값이 나온다.
83 |
84 | Decimal 클래스에는 원하는 반올림 동작에 따라 필요한 소수점 위치로 정확하게 반올림하는 내장 함수가 있다.
85 |
86 | ```python
87 | from decimal import ROUND_UP
88 |
89 | rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
90 | print(rounded)
91 |
92 | 15.52
93 | ```
94 |
95 | 이 방식으로 quantize 메소드를 사용하면 짧고 저렴한 통화에 해당하는 적은 통화료도 적절하게 처리할 수 있다. 다음 예에서 통화 요금을 Decimal로 나타낸 비용이 여전히 0.01원 이하임을 알 수 있다.
96 |
97 | ```python
98 | rate = Decimal('0.05')
99 | seconds = Decimal('5')
100 | cost = rate * seconds / Decimal('60')
101 | print(cost)
102 |
103 | 0.004166666666666666666666666667
104 | ```
105 |
106 | 하지만 다음과 같이 정량화(quantize)하면 0.01원으로 반올림된다.
107 |
108 | ```python
109 | rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
110 | print(rounded)
111 |
112 | 0.01
113 | ```
114 |
115 | Decimal이 고정 소수점 수에도 잘 동작하지만 아직도 정확도 면에서는 제약이 있다. 예를 들어 1/3은 근사값으로 표현되는 것 등이 그렇다. 정확도에 제한이 없는 유리수를 표현하려면 내장 모듈 fractions의 Fraction 클래스를 사용해야 한다.
116 |
117 |
118 |
119 | ## 3. 핵심 정리
120 |
121 | * 파이썬은 현실적으로 모든 유형의 숫자 값을 표현할 수 있는 내장 타입과 클래스를 모듈로 제공한다.
122 | * Decimal 클래스는 화폐 연산처럼 정밀도가 높고 정확한 반올림이 필요한 상황에 안성맞춤이다.
123 |
--------------------------------------------------------------------------------
/files/BetterWay48_PypiModules.md:
--------------------------------------------------------------------------------
1 | ## Better Way 48. 커뮤니티에서 만든 모듈을 어디서 찾아야 하는지 알아두자
2 |
3 | #### 247쪽
4 |
5 | * Created : 2019/06/29
6 | * Modified: 2019/06/29
7 |
8 |
9 |
10 | ## 1. PyPI를 통한 패키지 설치
11 |
12 | 파이썬에는 설치 후 프로그램에서 사용할 수 있는 모듈들의 [중앙 저장소](https://pypi.org/)가 있다. 나와 여러분 같은 사람들, 즉 파이썬 커뮤니티에서 이러한 모듈을 만들고 관리한다. 파이썬 패키지 인덱스(PyPI, Python Package Index)는 여러분이 원하는 목적에 가까운 코드를 찾을 수 있는 좋은 방법이다.
13 |
14 | 패키지 인덱스를 사용하려면 명령줄 도구 `pip`을 사용해야 한다. pip은 파이썬 3.4와 그 이후 버전에는 기본적으로 설치되어 있다. 파이썬 3.4 이전 버전에서는 파이썬 패키징 웹사이트([packaging.python.org](https://packaging.python.org/))에서 pip 설치 설명서를 찾아보면 된다.
15 |
16 | 일단 설치한 후에는 pip으로 새 모듈을 설치하는 방법은 간단하다. 예를 들어 'Better way 45'에서 사용한 pytz 모듈을 설치한다고 하자.
17 |
18 | ```sh
19 | $ pip3 install pytz
20 |
21 |
22 | Collecting pytz
23 | Using cached https://files.pythonhosted.org/packages/3d/73/fe30c2daaaa0713420d0382b16fbb761409f532c56bdcc514bf7b6262bb6/pytz-2019.1-py2.py3-none-any.whl
24 | Installing collected packages: pytz
25 | Successfully installed pytz-2019.1
26 | ```
27 |
28 | 위와 같은 방법으로 서드파티 모듈을 손쉽게 설치할 수 있다. 구체적인 결과는 나와 다를 수 있다. 나는 해당 이미 설치되어 있었고 지우고 다시 설치했기 때문에 캐시된 곳에서 바로 받아올 수 있었다.
29 |
30 |
31 |
32 | 이 예에서는 패키지의 파이썬 3버전을 설치하려고 pip3 명령을 사용했다. 많은 경우에 컴퓨터에 파이썬 2와 파이썬 3가 동시에 설치되어 있을 수 있다. 이때 **pip 대신 `pip3`을 통해 모듈을 설치하면 파이썬 3에 명시적으로 모듈이 설치된다.** 인기 있는 패키지의 대다수는 파이썬 버전 2, 3에서 모두 사용할 수 있다.
33 |
34 | **프로젝트별 가상환경 관리는 매우 중요한데 파이썬에서는 [pyenv](https://github.com/pyenv/pyenv)를 많이 사용한다.** 나도 여기에 더해 [pipenv](https://docs.pipenv.org/en/latest/)까지 사용하고 있다.
35 |
36 | PyPI의 각 모듈에는 자체의 소프트웨어 라이센스가 있다. 대부분의 패키지와 특히 인기 있는 패키지는 무료 또는 오픈소스다. 대부분의 경우 이런 라이센스는 프로그램에 모듈의 복사본을 포함하는 것을 허용한다.
37 |
38 |
39 |
40 | ## 2. 핵심 정리
41 |
42 | * 파이썬 패키지 인덱스(PyPI)는 파이썬 커뮤니티에서 만들고 유지하는 풍부한 공통 패키지를 유지하고 있다.
43 | * **pip은 PyPI에서 패키지를 설치하는 데 사용하는 명령줄 도구다.**
44 | * pip은 파이썬 3.4와 그 이후 버전에는 기본으로 설치되어 있다. 이전 버전에서는 직접 설치해야 한다.
45 | * PyPI 모듈의 대부분은 무료이자 오픈 소스 소프트웨어다.
46 |
--------------------------------------------------------------------------------
/files/BetterWay49_WriteDocstring.md:
--------------------------------------------------------------------------------
1 | ## Better Way 49. 모든 함수, 클래스, 모듈에 docstring을 작성하자
2 |
3 | #### 250쪽
4 |
5 | * Created : 2017/08/16
6 | * Modified: 2019/07/01
7 |
8 |
9 |
10 | ## 1. docstring 소개
11 |
12 |
13 | 파이썬에서 문서화는 언어의 동적 특성 때문에 매우 중요하다. 파이썬은 코드 블록에 문서를 첨부하는 기능을 기본적으로 지원한다. 파이썬은 다른 언어들과 달리 프로그램 실행 중에 소스 코드에 첨부한 문서에 직접 접근할 수 있다.
14 |
15 | 이를 우리는 **docstring**이라고 부르며 이 문서는 모듈, 함수, 클래스, 메소드 정의에 기술한다. **docstring을 작성하면 이는 객체의 \_\_doc\_\_ 이라는 특수한 속성이 된다.**
16 |
17 |
18 |
19 |
20 | 예를 한 번 살펴보자. **함수의 def 문 바로 다음에 docstring을 직접 작성하여 문서를 추가할 수 있다.**
21 |
22 | ```python
23 | def is_palindrome(word):
24 | """단어가 Palindrome(회문)인지에 대한 참/거짓을 반환한다."""
25 | return word == word[::-1]
26 | ```
27 |
28 | 함수의 \_\_doc\_\_이라는 특별한 속성에 접근하면 파이썬 프로그램 자체에 포함된 docstring을 확인할 수 있다.
29 |
30 | ```python
31 | print(repr(is_palindrome.__doc__))
32 |
33 |
34 | >>>
35 | 'Return True if the given word is a palindrome.'
36 | ```
37 |
38 | **또한 내장 함수 help를 통해서도 확인할 수 있다.**
39 |
40 | ```python
41 | >>> help(is_palindrome)
42 |
43 | Help on function is_palindrome in module __main__:
44 |
45 | is_palindrome(word)
46 | 단어가 Palindrome(회문)인지에 대한 참/거짓을 반환한다.
47 | ```
48 |
49 | 이는 함수나 클래스 등의 사용법이나 인터페이스 접근을 쉽게 한다. docstring은 앞서 확인했듯이 클래스 등의 정의 바로 밑에 서술하는데, 여느 언어와 마찬가지로 파이썬에서 문서화는 꽤나 높은 중요도를 갖는다.
50 |
51 |
52 |
53 | docstring은 패키지, 모듈, 클래스, 함수, 메소드에 붙일 수 있다. 이와 같은 연결은 파이썬 프로그램을 컴파일하고 실행하는 과정의 일부다. docstring과 \_\_doc\_\_ 속성을 지원하는 덕분에 다음 세 가지 효과를 얻는다.
54 |
55 | 1. **문서의 접근성 덕분에 대화식 개발이 쉽다. 내장 함수 help로 클래스 등을 실시간으로 조사할 수 있다.** 따라서 파이썬 대화식 인터프리터(쉘이나 Ipython 노트북)과 같은 도구에서 개발을 상대적으로 쉽게 할 수 있다.
56 | 2. 문서를 정의하는 표준 방법이 있으면 텍스트를 더 쉽게 이해할 수 있는 포맷(이를테면 HTML)으로 변환하는 도구를 쉽게 만들 수 있다. 그래서 [Sphinx](http://www.sphinx-doc.org/en/master/) 같은 훌륭한 문서 생성 도구가 생겨났다.
57 | 3. 파이썬의 일급 클래스, 접근성, 잘 정리된 문서는 사람들이 문서를 더 많이 작성할 수 있도록 한다. 파이썬 커뮤니티의 멤버들은 문서화가 중요하다고 강하게 믿고 있다. 이는 대부분의 오픈 소스 파이썬 라이브러리가 잘 작성된 문서를 갖추고 있음을 의미한다.
58 |
59 |
60 | docstring을 작성할 때 몇 가지 지침이 있다. 자세한 정보는 [PEP 257](https://www.python.org/dev/peps/pep-0257/)을 꼭 읽어보자.
61 |
62 |
63 |
64 | ## 2. 각 수준에 따른 문서화 Tip
65 |
66 | 이번에는 모듈, 클래스, 함수와 메소드 순으로 각 수준에 대한 문서화 tip에 대해 살펴본다.
67 |
68 | ### 2.1 모듈 문서화
69 |
70 | 각 모듈은 최상위 docstring을 가지고 있어야 한다. **최상위 docstring이란 소스 파일에서 첫 번째 문장에 있는 문자열이다.** 아마 문서화를 하지 않아본 이들은 소스코드 첫 문장이 import 문으로 시작하는 경우가 많을텐데 이제는 첫 문장을 docstring으로 시작하도록 하자. 모듈.\_\_doc\_\_로(또는 _help(모듈)_ 로) 모듈의 docs를 읽을 수 있다.
71 |
72 | **첫 문장은 모듈의 목적을 기술하는 한 문장으로 구성하며 그 이후의 문단은 사용자가 알아야 하는 모듈의 동작을 자세히 기술한다. 또한 모듈 내의 중요한 클래스나 함수를 강조한다.**
73 |
74 | ```python
75 | """Library for testing words for various linguistic patterns.
76 |
77 | Testing how words relate to each other can be tricky sometimes!
78 | This module provides easy ways to determine when words you've
79 | found have several special properties.
80 |
81 | Available functions:
82 | - palindrome: Determine if a word is a palindrome.
83 | - check_anagram: Determine if two words are anagrams.
84 | """
85 | import datetime
86 | # ...
87 | ```
88 |
89 | 모듈의 docstring을 작성한 후에는 문서를 계속 업데이트하는 게 중요하다. 내장 모듈 doctest는 docstring에 포함된 사용 예제를 실행하기 쉽게 해줘서 작성한 소스코드와 문서가 시간이 지나면서 여러 버전으로 나뉘지 않게 해준다.
90 |
91 |
92 | ### 2.2. 클래스 문서화
93 |
94 | 각 클래스는 클래스 수준의 docstring이 있어야 한다. 첫 번째 줄은 클래스의 목적을 기술하는 한 문장으로 구성한다. 그 이후의 문단에는 클래스의 동작과 관련한 중요한 내용을 기술한다.
95 |
96 | 클래스의 중요한 공개 속성과 메서드는 클래스 수준에서 강조해야 한다. 또한 서브클래스가 보호 속성, 슈퍼클래스의 메서드와 올바르게 상호 작용하는 방법을 안내해야 한다.
97 |
98 | ```python
99 | class Player:
100 | """Represent a player of the game.
101 |
102 | Subclasses may override the 'tick' method to provide
103 | custom animations for the player's movement depending
104 | on their power level, etc.
105 |
106 | Public attributes:
107 | - power: Unused power-ups(float between 0 and 1)
108 | - coins: Coins found during the level(integer)
109 | """
110 | ```
111 |
112 |
113 | ### 2.3 함수 & 메소드 문서화
114 |
115 | 각 공개 함수와 메소드는 docstring이 필요하다. **첫 번째 줄은 함수가 수행하는 일을 한 문장으로 설명한다. 그 다음 문단부터는 함수의 특별한 동작이나, 인수에 대해 설명한다. 반환 값도 언급한다.** 호출하는 쪽에서 함수 인터페이스의 일부로 처리해야 하는 예외도 설명한다.
116 |
117 |
118 | ```python
119 | def find_anagrams(word, dictionary):
120 | """Find all anagrams for a word.
121 |
122 | This function only runs as fast as the test
123 | for membership in the 'dictionary' container. It will
124 | be slow if the dictionary is a list and fast if it's a set.
125 |
126 | Args:
127 | word: String of the target word.
128 | dictionary: Container with all strings that are known to be actual words.
129 |
130 | Returns:
131 | List of anagrams that were found. Empty if none were found.
132 | """
133 | Do something...
134 | ```
135 |
136 | 함수 docstring을 작성할 때 작성할 때 알아둬야 할 몇 가지 경우가 있다.
137 |
138 | * **함수가 인자는 받지 않고 값만 반환하면 한 줄짜리 설명만으로 충분하다.**
139 | * 함수가 아무것도 반환하지 않으면 'return None' 대신 반환값을 언급하지 않는 것이 좋다.
140 | * 함수가 일반적인 동작에서는 예외를 일으키지 않는다고 생각한다면 이에 대해서는 언급하지 않는다.
141 | * 함수가 받는 인수의 개수가 가변적이거나 키워드 인자를 사용한다면, 문서의 인수 목록에 \*args, \*\*kwargs를 사용해서 그 목적을 설명한다.
142 | * 함수가 기본값이 있는 인수를 받으면 해당 기본값을 설명한다.
143 | * 함수가 제너레이터라면 제너레이터가 순회할 때 무엇을 넘겨주는지 설명한다.
144 | * 함수가 코루틴이라면 코루틴이 무엇을 넘겨주는지, yield 표현식으로부터 무엇을 얻는지 언제 순회가 끝나는지를 설명한다.
145 |
146 |
147 |
148 |
149 | ## 3. 핵심 정리
150 |
151 | * 모든 모듈, 클래스, 함수를 docstring으로 문서화하자. 코드 업데이트마다 관련 문서도 업데이트하자.
152 | * 모듈: 모든 사용자가 알아둬야 할 모듈의 내용과 중요한 클래스와 함수를 설명한다.
153 | * 클래스: _class_ 문 다음의 docstring에서 클래스의 동작, 중요한 속성, 서브클래스의 동작을 설명한다.
154 | * 함수와 메서드: _def_ 문 다음의 docstring에서 모든 인수, 반환 값, 일어나는 예외, 다른 동작을 문서화한다.
155 |
--------------------------------------------------------------------------------
/files/BetterWay50_UsePackage.md:
--------------------------------------------------------------------------------
1 | ## Better Way 50. 모듈을 구성하고 안정적인 API를 제공하려면 패키지를 사용하자
2 |
3 | #### 255쪽
4 |
5 | * Created : 2019/07/01
6 | * Modified: 2019/07/01
7 |
8 |
9 |
10 | ## 1. Package란?
11 |
12 | 프로그램의 코드가 커지면 자연히 코드의 구조를 재구성하기 마련이다. 예를 들면 큰 함수를 더 작은 함수로 분할한다. 자료구조를 헬퍼 클래스로 리팩토링하거나 기능을 서로 의존적인 여러 모듈로 분할하기도 한다.
13 |
14 | 언젠가는 너무 많은 모듈이 있어서 모듈들을 이해하기 쉽게 하려고 프로그램에 다른 계층을 추가해야 하는 시점이 온다. 파이썬은 이런 목적으로 Package(이하 "패키지")를 제공한다. 패키지란 쉽게 말해 **다른 모듈들을 포함하는 모듈이다.**
15 |
16 | 패키지를 만드는 방법은 간단하다. **대부분은 디렉토리 안에 \_\_init\_\_.py 라는 빈 파일을 넣는 방법으로 패키지를 정의한다. \_\_init\_\_.py가 있으면 해당 디렉토리에 있는 다른 파이썬 파일은 디렉토리에 상대적인 경로로 임포트할 수 있다.**
17 |
18 | 예를 들어 프로그램의 디렉토리 구조가 다음과 같다고 하자.
19 |
20 |
21 | > main.py
22 | > mypackage/\_\_init\_\_.py
23 | > mypackage/models.py
24 | > mypackage/utils.py
25 |
26 | 이 구조에서 utils 모듈을 임포트하려면 패키지 디렉토리 이름을 포함하는 절대 모듈 이름을 사용해야 한다.
27 |
28 | ```python
29 | # main.py
30 |
31 | from mypackage import utils
32 | ```
33 |
34 | 패키지 디렉토리가 다른 패키지(예를 들면 mypackage.foo.bar) 안에 있을 때는 계속 이런 패턴을 따른다.
35 |
36 | 패키지가 제공하는 기능은 파이썬 프로그램에서 두 가지 주요 목적이 있다.
37 |
38 |
39 |
40 |
41 | ## 2. Package의 기능 1: 네임스페이스
42 |
43 | **패키지의 첫 번째 용도는 분리된 네임스페이스(namespace)로 모듈들을 분할하는 것이다.** 이 기능은 파일 이름이 같은 여러 모듈이 서로 다른 절대경로를 갖게 해준다. 예를 들어 다음은 이름이 utils.py로 같은 두 모듈에서 각각 속성을 임포트하는 프로그램이다. 모듈을 단순 이름이 아닌 각각의 절대경로로 접근하므로 이름 충돌이 일어나지 않는다.
44 |
45 |
46 | ```python
47 | # main.py
48 | from analysis.utils import log_base2_bucket
49 | from frontend.utils import stringify
50 |
51 | bucket = stringify(log_base2_bucket(33))
52 | ```
53 |
54 | 디렉토리 안에 utils.py 라는 모듈이 동시에 존재하지만 패키지 구조 안에서 다른 절대경로를 갖기 때문에 충돌이 일어나지 않았다.
55 |
56 | 이 방법은 패키지에서 정의한 함수, 클래스, 서브모듈의 이름들이 같을 때는 동작하지 않는다. 예를 들어 analysis.utils와 frontend.utils 모듈에 있는 inspect 함수를 사용하고 싶다고 하자. 속성을 직접 임포트하면 두 번째 import문이 현재 영역에 있는 insepct의 값을 덮어쓰기 때문에 제대로 동작하지 않는다.
57 |
58 |
59 | ```python
60 | # main2.py
61 | from analysis.utils import inspect
62 | from frontend.utils import inspect # 윗 줄의 inspect를 덮어씀!
63 | ```
64 |
65 | 이런 상황에서 해결책은 `as` 키워드를 사용하여 현재 영역에 임포트하는 대상의 이름을 변경하는 것이다.
66 |
67 | ```python
68 | from analysis.utils import inspect as analysis_inspect
69 | from frontend.utils import inspect as frontend_inspect
70 |
71 | value = 33
72 | if anaylysis(value) == frontend_insepct(value):
73 | print('Inspection equal!')
74 | ```
75 |
76 | 많이 써봤을 `as` 키워드는 import 문으로 임포트한 대상의 이름을 변경하는 데 사용한다. 모듈도 가능하다. as 키워드를 사용하면 네임스페이스가 붙은 코드에 접근하기 쉽고 대상을 사용할 때 실체를 명확하게 인식할 수 있다.
77 |
78 |
79 |
80 |
81 | ## 3. Package의 기능 2: 안정적인 API
82 |
83 | 파이썬에서 패키지의 두 번째 용도는 외부 사용자에게 명확하고 안정적인 API를 제공하는 것이다.
84 |
85 | 오픈소스 패키지처럼 다양하게 사용할 목적으로 API를 작성할 때 릴리스 간의 변경 없이 안정적인 기능을 제공하고 싶다고 하자. 그러려면 **외부 사용자에게서 내부 코드 구조를 숨겨야 한다.** 이렇게 해두면 기존 사용자의 코드를 망가뜨리지 않고 패키지의 내부 모듈을 리팩토링하고 개선할 수 있다.
86 |
87 | **파이썬에서는 모듈이나 패키지에 \_\_all\_\_ 이라는 특별한 속성으로 API 사용자에게 드러나는 외부영역을 제한한다. \_\_all\_\_의 값은 공개 API의 일부로 외부에 제공하려는 문자열 형태의 모듈 이름을 모두 담은 리스트다. 패키지를 사용하는 코드에서 `from foo import *`를 실행하면 foo 패키지의 모든 기능들이 임포트되는 것이 아닌, `foo.__all__`에 있는 속성만 import된다. 'foo'에 \_\_all\_\_이 없으면 속성 이름이 밑줄로 시작하지 않는 공개 속성만 임포트된다.**
88 |
89 | 예를 만들어보자. 움직이는 발사체 간의 충돌을 계산하는 패키지를 제공한다고 하자. 다음은 mypackage의 models 모듈을 정의하여 발사체를 표현하는 코드다.
90 |
91 | ```python
92 | # models.py
93 | __all__ = ['Projectile',]
94 |
95 |
96 | class Projectile:
97 | def __init__(self, mass, velocity):
98 | self.mass = mass
99 | self.velocity = velocity
100 | ```
101 |
102 | 또한 mypackage에 utils 모듈을 정의하여 Projectile 인스턴스로 발사체 간의 충돌을 시뮬레이트하는 동작을 수행한다.
103 |
104 | ```python
105 | # utils.py
106 | from . models import Projectile
107 |
108 | __all__ = ['simulate_collision',]
109 |
110 | def _dot_product(a, b):
111 | pass
112 |
113 | def simulate_collision(a, b):
114 | pass
115 | ```
116 |
117 | 이제 **두 모듈의 공개 API를 모두 mypackage 모듈에서 바로 사용 가능한 속성의 집합으로 제공하려고 한다.** 이렇게 하면 mypackage.models 혹은 mypackage.utils에서 임포트하는 대신, mypackage로부터 항상 직접 임포트할 수 있다. 이 방식은 mypackage의 내부 구조를 변경(예를 들면 models.py가 삭제되는 경우)하더라도 API를 사용하는 코드가 계속 동작함을 보장한다.
118 |
119 | **파이썬 패키지로 이렇게 동작하게 하려면 mypackage 디렉토리에 있는 \_\_init\_\_.py 파일을 수정해야 한다.** 이 파일은 실제로 임포트될 때 mypackage 모듈의 내용이 된다. 따라서 \_\_init\_\_.py로 임포트하는 것을 제한하여 mypackage의 API를 명시적으로 설정할 수 있다. 이미 두 내부 모듈은 \_\_all\_\_을 명시하고 있으므로, 단순히 내부 모듈에서 모든 것을 임포트하고 \_\_init\_\_.py 모듈의 \_\_all\_\_ 속성을 이에 맞춰 업데이트하면 mypackage의 공개 인터페이스를 외부에 드러낼 수 있다.
120 |
121 |
122 | ```python
123 | # __init__.py
124 | from . models import *
125 | from . utils import *
126 |
127 | __all__ = []
128 | __all__ += models.__all__
129 | __all__ += utils.__all__
130 | ```
131 |
132 | 다음은 API의 사용자가 내부 모듈에 접근하지 않고 mypackage로부터 기능을 직접 임포트하는 코드다.
133 |
134 |
135 | ```python
136 | # api_consumer.py
137 | from mypackage import *
138 |
139 | a = Projectile(1.5, 3)
140 | b = Projectile(4, 1.7)
141 | after_a, after_b = simulate_collision(a, b)
142 | ```
143 |
144 | **위의 코드에서 사용한 Projectile이나 simulate\_collision은 원래 mypackage 내부에 있는 모듈에 포함된 속성들이다. 하지만 패키지의 \_\_init\_\_.py 파일에서 기능들을 직접 임포트함으로써 패키지 수준에서 기능을 끌어다쓸 수 있었다.**
145 |
146 | **이때 기억할 것은 mypackage.utils.\_dot\_product 같은 내부 전용 함수는 \_\_all\_\_에 포함되지 않으므로 mypackage API 사용자는 사용할 수 없다는 것이다.** \_\_all\_\_에서 빠진다는 것은 `from mypackage import *` 문으로 임포트되지 않음을 의미한다. 따라서 내부 전용 이름을 효과적으로 숨길 수 있다.
147 |
148 | 이런 모든 방법은 명시적이고 안정적인 API를 제공해야 할 때 잘 동작한다. 하지만 직접 만든 모듈 사이에서 사용하려고 API를 구축한다면 \_\_all\_\_ 기능이 필요 없을 것이므로 사용하지 말아야 한다. 패키지가 제공하는 네임스페이스는 의미 있는 인터페이스를 유지하며 많은 양의 코드로 협업하는 개발팀에도 일반적으로 충분한다.
149 |
150 |
151 |
152 |
153 | ## 4. 핵심 정리
154 |
155 | * 파이썬의 패키지는 다른 모듈을 포함하는 모듈이다. 패키지를 이용하면 고유한 절대 모듈 이름으로 코드를 분리하고, 충돌하지 않는 네임스페이스를 구성할 수 있다.
156 | * 간단한 패키지는 다른 소스 파일을 포함하는 디렉토리에 \_\_init\_\_.py 파일을 추가하는 방법으로 정의한다. \_\_init\_\_.py를 제외한 파일들은 디렉토리 패키지의 자식 모듈이 된다. 패키지 디렉토리는 다른 패키지를 포함할 수도 있다.
157 | * \_\_all\_\_ 이라는 특별한 속성에 공개하려는 이름을 나열하여 모듈의 명시적인 API를 제공할 수 있다.
158 | * 공개할 이름만 패키지의 \_\_init\_\_.py 파일에서 임포트하거나 내부 전용 멤버의 이름을 밑줄로 시작하게 만들면 패키지의 내부 구현을 숨길 수 있다.
159 | * 단일 팀이나 단일 코드베이스로 협업할 때는 외부 API용으로 \_\_all\_\_을 사용할 필요가 없을 것이다.
160 |
--------------------------------------------------------------------------------
/files/BetterWay51_DefineRootException.md:
--------------------------------------------------------------------------------
1 | ## Better Way 51. 루트 Exception을 정의해서 API로부터 호출자를 보호하자
2 |
3 | #### 262쪽
4 |
5 | * Created : 2019/07/02
6 | * Modified: 2019/07/02
7 |
8 |
9 |
10 | ## 1. 모듈 전용 Error를 정의한다는 것
11 |
12 | 모듈의 API를 정의할 때 여러분이 던지는 예외는 인터페이스의 일부로 정의한 함수와 클래스만이다. 파이썬은 언어와 표준 라이브러리용 내장 예외 계층을 갖추고 있다. 오류를 보고할 때 자신만의 새 타입을 정의하는 대신 내장 예외 타입을 사용할 가능성이 크다. 예를 들어, 함수에 올바르지 않은 파라미터가 넘어오면 ValueError 예외를 일으킬 수 있다.
13 |
14 | ```python
15 | def determine_weight(volume, density):
16 | if density <= 0:
17 | raise ValueError("밀도는 0 이하일 수 없습니다.")
18 | ```
19 |
20 | 이런 상황에서 몇몇 경우에는 ValueError를 사용하는 것을 이해할 수 있지만 **API용으로는 자신만의 예외 계층을 정의하는 방법이 더 강력하다.** 예외 계층을 정의하려면 모듈 내에서 루트 exception을 제공하면 된다. 그런 다음 해당 모듈에서 일어나는 다른 예외가 모두 루트 예외로부터 상속받게 한다.
21 |
22 | ```python
23 | class Error(Exception):
24 | """Base exception class for this module."""
25 | pass
26 |
27 | class InvalidDensityError(Error):
28 | """There was a problem with a provided density value."""
29 | pass
30 | ```
31 |
32 | 알다시피 **_Exception_ 은 파이썬의 모든 내장 에러 클래스의 최상위 부모 클래스다.** 오류 세계에서의 _object_ 라고 할 수 있다. 이 예외를 상속하는 _Error_ 클래스를 생성함으로써 현재 작성하는 모듈 또는 프레임워크만을 위한 최상위 에러 클래스를 정의했다.
33 |
34 | 모듈에 루트 예외를 두면 API 사용자들이 목적을 두고 일으킨 모든 예외를 잡아낼 수 있다. 예를 들어 API 사용자는 루트 예외를 잡아내는 try / except 문으로 함수를 호출할 수 있다.
35 |
36 | ```python
37 | def determine_weight(volume, density):
38 | if density <= 0:
39 | raise InvalidDensityError("밀도는 0 이하일 수 없습니다.")
40 | # 에러클래스를 사용자 정의 클래스로 변경
41 | ```
42 |
43 |
44 | ```python
45 | try:
46 | weight = my_module.determine_weight(1, -1)
47 | except my_module.Error as e:
48 | print("Unexpected error:", e)
49 |
50 | Unexpected error: 밀도는 0 이하일 수 없습니다.
51 | ```
52 |
53 | 이 try / except는 API의 예외가 너무 빨리 멀리 퍼져나가서 호출하는 프로그램을 중단하는 일을 막는다. 이 구문은 호출하는 코드를 API로부터 보호한다. 이런 보호는 세 가지 유용한 효과를 낸다.
54 |
55 |
56 | 1. **루트 예외가 있으면 호출자가 API를 사용할 때 문제점을 이해할 수 있다.**
57 | - 호출자가 API를 올바르게 사용한다면 개발자가 의도적으로 일으킨 다양한 예외를 잡아낼 수 있어야 한다.
58 | - 그러한 예외를 처리할 수 없다면 개발자가 작성한 모듈의 루트 예외를 잡아서 보호하는 except 블록까지 전파된다. 이 except 블록은 API 사용자가 예외를 주목하게 하여 해당 예외 타입을 적절히 처리하는 코드를 추가하게 만든다.
59 |
60 | ```python
61 | try:
62 | weight = my_module.determine_weight(1, -1)
63 | except my_module.InvalidDensityError:
64 | weight = 0
65 | except my_module.Error as e:
66 | print("Unexpected error:", e)
67 |
68 | # 에러의 계층구조를 확인
69 | ```
70 |
71 | 2. **API 모듈의 코드에 있는 버그를 찾는 데 도움이 된다.**
72 | - 코드에서 모듈 계층 안에 정의한 예외만 의도적으로 일으킨다면, 해당 모듈에서 일어난 다른 타입의 예외(예를 들어 내장 _Exception_)는 모두 의도하지 않은 것이 틀림없다. 이런 예외가 곧 버그다.
73 |
74 | try / except 문을 사용한다고 해서 API 모듈에 있는 모든 버그로부터 사용자들을 보호하지는 못한다. **API 사용자를 보호하려면 호출자가 파이썬의 _Exception_ 기반 클래스를 잡아내는 다른 except 블록을 추가해야 한다.**
75 |
76 | ```python
77 | try:
78 | weight = my_module.determine_weight(1, -1)
79 | except my_module.InvalidDensityError:
80 | weight = 0
81 | except my_module.Error as e:
82 | print("Unexpected error:", e)
83 | except Exception as e: # 예상 못한 예외를 모두 잡는다.
84 | print('Bug in the code:', e)
85 | raise
86 | ```
87 |
88 | 3. **마지막 효과는 API의 미래를 대비할 수 있다.**
89 | - 시간이 지나 특정 환경에서 더 구체적인 예외를 제공하려고 API를 확장할 수도 있다. 예를 들어 밀도를 음수로 넘기는 오류 상황을 알리는 _Exception_ 서브클래스를 추가할 수도 있다.
90 |
91 | ```python
92 | class NegativeDensityError(InvalidDensityError):
93 | """Provided density value was negative."""
94 |
95 | def determine_weight(volume, density):
96 | if density <= 0:
97 | raise NegativeDensityError("밀도는 0 이하일 수 없습니다.")
98 | ```
99 |
100 | 밀도값이 부적절할 때 반환하는 에러를 더 확장해서(상속해서) _InvalidDensityError_ 에서 음수의 입력에 대응하는 보다 구체적인 _NegativeDensityError_ 를 만들었다. 그리고 *determine\_weight* 함수가 밀도값이 0 이하일 때 새로 만든 에러를 반환하도록 했다.
101 |
102 | 이때, **이전에 작성한 try / excpet 문에서 _InvalidDensityError_ 에러는 _NegativeDensityError_ 의 부모 클래스이기 때문에 문제없이 에러를 잡아내므로 이전과 똑같이 동작한다.** 나중에 호출자가 새 예외 타입을 특별한 경우로 처리하도록 결정하고 그에 따라 동작을 변경할 수 있다.
103 |
104 | ```python
105 | try:
106 | weight = my_module.determine_weight(1, -1)
107 | except my_module.NegativeDensityError as e:
108 | raise ValueError('Must supply positive density') from e
109 | exept my_module.InvalidDensityError:
110 | weight = 0
111 | except my_module.Error as e:
112 | print("Bug in the code:", e)
113 | except Exception as e:
114 | print("But in the API code:", e)
115 | raise
116 | ```
117 |
118 | 마지막 두 줄에 집중해보자. *my\_module.Error* 까지는 개발자 입장에서 모두 예측하고 정의한 에러이기 때문에 사용자 코드에 문제가 있다고 이해할 수 있다.
119 |
120 | 하지만 마지막 _Exception_ 에서는 개발자가 의도 못한 에러이기 때문에 개발자의 실수라고 봐야하고 이는 곧 API의 버그를 의미한다.
121 |
122 |
123 |
124 | `확장`의 강력함은 더 추가할 수 있다. 루트 예외 바로 아래에 더 많은 예외를 제공하여 API의 미래를 대비할 수 있다. 예를 들어 무게, 부피, 밀도 계산과 관련이 있는 오류 집합을 각각 만들고 싶다고 하자.
125 |
126 |
127 | ```python
128 | # my_module.py
129 | class WeightError(Error):
130 | """Base class for weight calculation erros."""
131 | pass
132 |
133 | class VolumeError(Error):
134 | """Base class for volume calculation erros."""
135 | pass
136 |
137 | class DensitytError(Error):
138 | """Base class for density calculation erros."""
139 | pass
140 | ```
141 |
142 | **구체적인 예외는 이런 일반적인 예외로부터 상속해서 만든다.** 각 중간 예외는 루트 예외처럼 동작한다. 이 방법을 이용하면 많은 기능을 기반으로 한 API 코드로부터 호출하는 코드를 쉽게 분리할 수 있다. 이 방법이 모든 호출자가 매우 구체적인 Exception 서브클래스를 각각 캐치하는 방법보다 훨씬 낫다.
143 |
144 |
145 |
146 | ## 2. 핵심 정리
147 |
148 | * 작성 중인 모듈에 루트 예외를 정의하면 API로부터 API 사용자를 보호할 수 있다.
149 | * 모듈을 위해 정의한 루트 예외를 잡으면 API를 사용하는 코드에 숨은 버그를 찾는 데 도움이 될 수 있다.
150 | * 파이썬 Exception 기반 클래스를 잡으면 API 구현에 있는 버그를 찾는 데 도움이 될 수 있다.
151 | * 중간 루트 예외를 이용하면 API를 사용하는 코드에 영향을 주지 않고 나중에 더 구체적인 예외를 추가할 수 있다.
152 |
--------------------------------------------------------------------------------
/files/BetterWay52_HowToGetRidOfCircularDependency.md:
--------------------------------------------------------------------------------
1 | ## Better Way 52. 순환 의존성을 없애는 방법을 알자
2 |
3 | #### 266쪽
4 |
5 | * Created : 2019/07/02
6 | * Modified: 2019/07/02
7 |
8 |
9 |
10 | ## 1. 파이썬에서 순환 의존성의 문제
11 |
12 | 불가피하게 다른 이들과 협력하는 동안 모듈 사이의 `순환 의존성`(Circular Dependency)을 발견하기 마련이다. 심지어 혼자서 작업할 때 발생하기도 한다. 일례로 내가 Django 개발을 할 때 서로 다른 app의 views.py 사이에서 순환 의존성을 발견한 기억이 있다.
13 |
14 | 오늘은 순환 의존성과 그에 대한 해결책을 살펴보도록 한다. 예를 들어 문서를 저장할 위치를 선택하는 대화상자를 보여주는 GUI 어플리케이션을 만든다고 해보자. 대화상자로 표시할 데이터는 이벤트 핸들러의 인수로 설정할 수 있다. 하지만 대화상자는 올바르게 렌더링하는 방법을 알아내려고 사용자 설정과 같은 전역 상태도 읽어야 한다. 즉, **두 개의 모듈이 상대방에 대한 정보를 모두 필요로 하는 것. 전형적인 순환 의존성의 상황이다.**
15 |
16 | 먼저 다음 대화상자는 전역 설정에서 기본으로 문서를 저장할 위치를 얻어온다.
17 |
18 | ```python
19 | # dialog.py
20 | import app
21 |
22 |
23 | class Dialog:
24 | def __init__(self, save_dir):
25 | self.save_dir = save_dir
26 |
27 |
28 | save_dialog = Dialog(app.prefs.get('save_dir'))
29 |
30 |
31 | def show():
32 | pass
33 | ```
34 |
35 | 문제는 **프로그램에서 사용자의 선호도와 관련된 _prefs(preferences)_ 객체를 포함하는 app 모듈도 프로그램을 시작할 때 대화상자를 보여주려고 dialog 모듈을 임포트한다는 것이다.**
36 |
37 |
38 | ```python
39 | # app.py
40 | import dialog
41 |
42 |
43 | class Prefs:
44 | # ...
45 | def get(self, name):
46 | # ...
47 | pass
48 |
49 | prefs = Prefs()
50 | dialog.show()
51 | ```
52 |
53 | 메인 프로그램에서 app 모듈을 임포트하려고 하면 예외가 일어난다. 순환 의존성이 발생했다.
54 |
55 | ```python
56 | import app
57 |
58 | AttributeError: module 'app' has no attribute 'prefs'
59 | ```
60 |
61 | **여기서 정확히 어떤 일이 일어나는지, 그리고 에러의 구체적인 내용, 왜 하필 _app_ 모듈에서 _prefs_ 속성이 없다는 _AttributeError_ 가 raise 됐는지 파악해야 한다.** 우리는 app.py에서 분명이 prefs 변수를 정의했기 때문이다.
62 |
63 | 이를 위해서는 먼저 파이썬의 모듈 임포트 절차를 자세히 알아봐야 한다. 다음은 파이썬에서 깊이 우선 방식으로 모듈을 임포트하면서 실제로 하는 일이다.
64 |
65 | 중요하니 제대로 살펴보자.
66 |
67 |
68 |
69 | 1. **_sys.path_ 에 들어 있는 경로들에서 요구한 모듈이 존재하는지 검색한다.**
70 | * sys 내장 모듈의 path는 list로서 파이썬 인터프리터가 임포트할 모듈들을 검색하는 실제 경로들을 담고 있다. 모든 임포트할 모듈은 이 리스트 안에 있는 경로에 위치해야 한다.
71 | 2. **모듈을 찾았으면 모듈에서 코드를 로드하고 코드가 컴파일되게 한다.**
72 | 3. **모듈과 관련된 기능에 접근가능하게 할 빈 모듈 객체를 생성한다.**
73 | 4. **모듈 객체를 sys.modules에 삽입한다.**
74 | * sys.modules는 dict로서 현재 임포트되어 사용가능한 모듈들에 대해 이름을 key, 실제 경로를 value로 해서 저장하고 있다.
75 | 5. **빈 객체에 코드를 실행하면서 모듈의 내용(함수, 클래스, 상수)을 정의한다.(모듈의 변수가 된다.)**
76 |
77 |
78 |
79 | **순환 의존성의 문제는 모듈의 속성이 해당 속성의 코드를 실행하기 전에는 정의되지 않는다는 점이다.(과정 5 이후에 정의 완료) 하지만 모듈을 sys.modules에 삽입한 후에는 import 문에서 즉시 로드할 수 있다.(과정 4 이후)**
80 |
81 | 이에 대한 이해를 바탕으로 앞선 코드의 에러를 파악해보자.
82 |
83 | 1. `import app`를 입력하면 app 모듈은 다른 것을 정의하기 전에 첫 번째 줄에 있는 `import dialog`를 먼저 실행한다. 그러면 메인 스레드는 dialog 모듈로 이동해서 관련 내용을 정의하려 한다.
84 | 1. dialog 모듈에서 `import app`을 다시 호출한다. **근데 이때 app은 아직 실행 중이므로(현재 dialog를 임포트하는 중), app 모듈은 그냥 비어 있다.(과정 4)**
85 | 1. **app 모듈에 prefs를 정의하는 코드가 아직 실행되지 않았기 때문에(app에 대한 과정 5가 완료되지 않음) _AttributeError_ 가 발생하게 된다.**
86 |
87 |
88 |
89 | 이 문제를 해결하는 **최선의 방법은 코드를 리팩토링해서 _prefs_ 자료구조가 의존성 트리의 아래에 오게 하는 것이다.** 그러면 app과 dialog는 같은 유틸리티 모듈을 임포트하므로 순환 의존성을 피한다. 하지만 항상 이처럼 명쾌하게 분리할 수는 없으며, 얻는 장점에 비해 리팩토링 작업에 너무 많은 노력이 들 수도 있다.
90 |
91 | 따라서 순환 의존성을 없앨 수 있는 다른 대안 3가지를 살펴보도록 하자.
92 |
93 |
94 |
95 |
96 | ## 2. 순환 의존성을 해결하는 세 가지 방법
97 |
98 | ### 2.1. 임포트 재정렬
99 |
100 | 첫 번째 방법은 임포트 순서를 변경하는 것이다. 예를 들어 app 모듈의 끝에서 dialog 모듈을 임포트한다면 그 내용이 실행되고 AttributeError가 사라진다.
101 |
102 | ```python
103 | # app.py
104 | class Prefs:
105 | def get(self, name):
106 | pass
107 |
108 | prefs = Prefs()
109 | import dialog # 임포트 위치 변경
110 | dialog.show()
111 | ```
112 |
113 | dialog.py는 그대로 두고, app.py에서 `import dialog`문만 아래로 내린다. 이 방법은 dialog 모듈이 나중에 로드될 때 app에 대한 재귀 임포트 부분에서 app.prefs가 이미 정의된 상태이기 때문에 에러를 없앤다.
114 |
115 | 이 방법으로 AttributeError를 피할 수 있지만, 이는 PEP 8 스타일 가이드를 어기는 것이다. **PEP 8에서는 항상 파이썬 파일의 가장 위에서 임포트하라고 제안한다.** 코드를 처음 접하는 사람이 모듈 의존성을 명확하게 파악하게 하기 위해서다. 또한 의존하는 모듈이 무엇이든 맨 위 스코프에 있게 하여 그 아래에 있는 모든 코드에서 사용할 수 있게 해준다.
116 |
117 | 파일에서 나중에 임포트하는 방법은 불안정하기도 하고 코드의 순서를 약간만 바꿔도 모듈이 동작하지 않는 원인이 된다. 따라서 순환 의존성 문제를 해결하려고 임포트 순서를 변경하는 방법은 피해야 한다.
118 |
119 |
120 |
121 |
122 | ### 2.2. 임포트, 설정, 실행
123 |
124 | 두 번째 해결책은 임포트하는 시점에 모듈에서 부작용을 최소화하는 것이다. 모듈에는 함수, 클래스, 상수만 정의해야 한다. 임포트 시점에 실제 함수를 실행하는 일은 피해야 한다. 그런 다음 각 모듈은 다른 모듈이 모두 임포트되고 나서 한 번만 실행할 configure 함수를 제공하는 방식이다.
125 |
126 | **configure의 목적은 다른 모듈의 속성에 접근해서 각 모듈의 상태를 준비하는 것이다. 모든 모듈이 임포트된 후(과정 5 완료) configure를 실행하므로 모든 속성이 반드시 정의되어 있어야 한다.**
127 |
128 | 이번에는 configure를 호출할 때 prefs 객체만 접근하도록 dialog 모듈을 재정의한다.
129 |
130 | ```python
131 | # dialog.py
132 | import app
133 |
134 | class Dialog:
135 | def __init__(self, save_dir=None):
136 | self.save_dir = save_dir
137 |
138 | save_dialog = Dialog()
139 |
140 | def show():
141 | pass
142 |
143 | def configure():
144 | save_dialog.save_dir = app.prefs.get('save_dir')
145 | ```
146 |
147 | **_Dialog_ 클래스의 생성자 인자의 기본값을 _None_ 으로 둔 것을 눈여겨보자. 구체적인 저장경로 설정은 나중에 설정 단계(configure) 함수에서 진행할 것이기 때문에 저장경로가 빈 상태에서도 대화창이 생성 가능하게 한다.**
148 |
149 | 다음으로 임포트할 때 아무 기능도 실행하지 않도록 app 모듈을 재정의한다.
150 |
151 | ```python
152 | # app.py
153 | import dialog
154 |
155 | class Prefs:
156 | def get(self, name):
157 | pass
158 |
159 | prefs = Prefs()
160 | dialog.show()
161 |
162 |
163 | def configure():
164 | # 무슨 일을 함.
165 | pass
166 | ```
167 |
168 | **마지막으로 이들을 종합해 실행할 main 모듈을 만들어서 별개의 실행 단계 세 개(모든 것을 임포트하는 단계, 설정하는 단계, 액션을 실행하는 단계)로 구성한다.**
169 |
170 | ```python
171 | # 1. 임포트 단계
172 | import app
173 | import dialog
174 |
175 | # 2. 설정 단계
176 | app.configure()
177 | dialog.configure()
178 |
179 | # 3. 액션 시작 단계
180 | dialog.show()
181 | ```
182 |
183 | **이 방법은 많은 상황에서 잘 동작하며 [의존성 주입](https://en.wikipedia.org/wiki/Dependency_injection)(Dependency injection)과 같은 패턴을 가능하게 한다.** 하지만 명시적인 configure 단계가 가능한 형태로 코드를 구성하는 건 어려울 수 있다. 모듈에 별개의 두 단계를 두면 설정에서 객체의 정의가 분리되기 때문에 코드를 이해하기 어렵게 하기도 한다.
184 |
185 |
186 |
187 | ### 2.3. 동적 임포트
188 |
189 | 마지막으로 가장 간단한 방법은 **import 문을 함수나 메소드에서 사용하는 것이다.** 이 방법은 프로그램이 실행하는 동안 모듈 임포트가 발생하며, 프로그램이 처음 시작해서 모듈을 초기화하는 동안에는 발생하지 않으므로 `동적 임포트`(dynamic import)라고 한다.
190 |
191 | 동적 임포트를 사용해서 dialog 모듈을 재정의하자. 초기화 시전에 dialog 모듈이 app을 임포트하는 대신 dialog.show 함수로 런타임에 app 모듈을 임포트한다.
192 |
193 |
194 | ```python
195 | # dialog.py
196 | class Dialog:
197 | def __init__(self, save_dir=None):
198 | self.save_dir = save_dir
199 |
200 |
201 | save_dialog = Dialog()
202 |
203 |
204 | def show():
205 | import app # 동적 import
206 | save_dialog.save_dir = app.prefs.get('save_dir')
207 | ```
208 |
209 | app 모듈은 원래 예제와 같다. 맨 위에서 dialog를 임포트하고 아래에서 dialog.show()를 호출한다.
210 |
211 | 이 방법은 앞에서 설명한 '임포트, 설정, 실행' 단계와 비슷한 효과를 낸다. **차이는 이 방법을 이용하면 모듈을 정의하고 임포트하는 방식을 구조적으로 변경할 필요가 없다는 것이다.** 단순히 다른 모듈에 접근해야 하는 시점까지 순환 임포트를 미루는 방식이다. 그 시점에 이르면 다른 모듈은 모두 이미 초기화되어 있다고 확신할 수 있다.(과정 5 완료)
212 |
213 | **일반적으로 이런 동적 임포트는 피하는 게 좋다.** 그 이유는 import 문의 `비용` 때문이다. 이 비용은 무시못할 정도이며, 특히 복잡한 루프에서는 좋지 않다. 동적 임포트는 실행을 미루는 동작으로 런타임에 상당히 심각한 실패를 야기한다. 예를 들어 SyntaxError 예외가 프로그램이 실행을 시작한 이후에 일어난다. 하지만 이런 단점이 보통 전체 프로그램을 대체하거나 재구성하는 것보다 낫기는 하다.
214 |
215 |
216 |
217 | 정리하자면 순환 의존성은 서로 다른 모듈이 각자 초기화를 마치지 않은 상태에서 상대방을 호출할 때 발생하며 그에 대한 쉬운 해결책은 크게 다음과 같다:
218 |
219 | 1. import 문 위치 조정
220 | 1. 임포트 단계를 임포트, 설정, 실행 단계로 세분화
221 | 1. 동적 임포트
222 |
223 |
224 |
225 |
226 |
227 |
228 | ## 3. 핵심 정리
229 |
230 | * 순환 의존성은 두 모듈이 임포트 시점에 서로 호출할 때 일어난다. 이런 의존성은 프로그램이 시작할 때 동작을 멈추는 원인이 된다.
231 | * 순환 의존성을 없애는 가장 좋은 방법은 순환 의존성을 의존성 트리의 아래에 있는 분리된 모듈로 리팩토링하는 것이다.
232 | * 동적 임포트는 리팩토링과 복잡도를 최소화해서 모듈 간의 순환 의존성을 없애는 가장 간단한 해결책이다.
233 |
--------------------------------------------------------------------------------
/files/BetterWay53_UseVirtualEnvironment.md:
--------------------------------------------------------------------------------
1 | ## Better Way 53. 의존성을 분리하고 재현하려면 가상환경을 사용하자
2 |
3 | #### 273쪽
4 |
5 | * Created : 2019/07/03
6 | * Modified: 2019/07/03
7 |
8 |
9 |
10 |
11 | ## 1. 전역 라이브러리 관리의 문제점
12 |
13 | 더 크고 복잡한 프로그램을 구축할 때는 파이썬 커뮤니티에서 만든 다양한 패키지를 사용하기 마련이다.(ex: Django, Flask) 결국 pip을 사용해 이들을 설치하는 자신을 발견하게 될 것이다.
14 |
15 | 문제는 **pip이 기본으로 새 패키지를 전역 위치에 설치한다는 점이다.** 그래서 시스템에 있는 모든 파이썬 프로그램이 설치된 모듈의 영향을 받는다. 이론적으로는 이게 문제가 되지 않는다. 패키지를 설치하고 임포트하지 않는다면 프로그램에 어떻게 영향을 미칠 수 있을까?
16 |
17 | 문제는 `의존성 전이`(transitive dependencies), 즉 설치한 패키지가 의존하는 패키지 때문에 생긴다. 예를 들어 소프트웨어 문서화 도구인 [Sphinx](http://www.sphinx-doc.org/en/master/) 패키지를 설치한 후 pip을 통해 Sphinx가 어떤 패키지에 의존하는지 알 수 있다.
18 |
19 | ```shell
20 | $ pip show Sphinx
21 |
22 | name: Sphinx
23 | Version: 2.1.2
24 | # 생략...
25 | Location: /home/sunghwanpark/.local/share/virtualenvs/test-dependency-2VwzX83x/lib/python3.6/site-packages
26 | Requires: Jinja2, sphinxcontrib-devhelp, sphinxcontrib-qthelp, docutils, # 너무 길어 생략
27 | Required-by:
28 | ```
29 |
30 | 마찬가지로 파이썬 웹 마이크로 프레임워크인 [flask](http://flask.pocoo.org/)에도 같은 작업을 할 수 있다.
31 |
32 | ```Name: Flask
33 | $ pip show flask
34 |
35 | Version: 1.0.3
36 | # 생략...
37 | Location: /home/sunghwanpark/.local/share/virtualenvs/test-dependency-2VwzX83x/lib/python3.6/site-packages
38 | Requires: click, Jinja2, Werkzeug, itsdangerous
39 | Required-by: shell
40 | ```
41 |
42 | **이 둘 모듈에서 공통점은 모두 'Jinja2'라는 라이브러리에 의존하고 있다는 점이다.**
43 |
44 | **충돌은 시간이 지남에 따라 Sphinx와 flask가 의존하는 버전이 갈리면서 일어난다.** 아마 지금은 둘 다 같은 버전의 Jinja2를 요구해서 모든 게 괜찮을지도 모른다. 하지만 6개월 뒤나 1년 후에는 Jinja2는 라이브러리 사용자의 기능을 망가뜨리는 변경을 적용한 새로운 버전을 릴리스할 수도 있다. 이때 `pip install --upgrade` 명령으로 Jinja2의 전역 버전을 업데이트하면 flask는 동작하지만 Sphnix는 제대로 동작하지 않을지도 모른다.
45 |
46 | 이런 문제의 원인은 파이썬이 전역 버전 모듈을 한 번에 하나만 두기 때문이다. 설치한 패키지 중 하나는 새로운 버전을 사용해야 하고 다른 패키지는 이전 버전을 사용해야 한다면 시스템이 제대로 동작하지 않을 것이다.
47 |
48 | 이런 문제는 패키지 유지보수 작업자가 릴리스 간에 API 호환성을 유지하려고 할 때도 발생한다. 라이브러리의 새로운 버전은 API를 사용하는 코드가 의존하는 동작을 약간 변경할 수도 있다. 시스템 사용자들이 패키지 하나를 새로운 버전으로 업그레이드하고 나머지는 업그레이드하지 않으면 의존성이 깨진다. 그러면 끊임없이 위험이 따른다.
49 |
50 | **이러한 어려움은 서로 분리된 로컬 컴퓨터에서 작업하는 다른 개발자들과 협력할 때 더 커진다.** 보통 머신마다 파이썬 버전과 설치한 패키지의 버전이 서로 약간씩이라도 다르다고 생각하는 것이 합리적이기 때문이다. 이런 차이 때문에 코드베이스가 한 프로그래머의 머신에서는 완벽하게 실행되지만 다른 사람의 머신에서는 완전히 망가지는 실망스러운 상황이 생긴다.
51 |
52 |
53 |
54 | ## 2. 해결책: 가상환경을 통한 프로젝트별 패키지 관리
55 |
56 | 이런 모든 문제의 해결책은 가상환경(virtual environment)을 제공하는 도구를 사용하는 것이다. 관련된 기능을 제공하는 많은 프로그램이 있지만 여기서는 파이썬의 내장 도구를 살펴본다. 다른 해결책은 [pyenv](https://github.com/pyenv/pyenv), [pipenv](https://docs.pipenv.org/en/latest/) 등이 있지만 여기서 사용할 pyvenv는 3.4 버전 이후부터 설치없이 사용할 수 있어서 살펴볼만하다. 이전 버전에서는 `pip install virtualenv`로 별도의 패키지를 설치한 후 `virtualenv` 명령줄 도구로 사용해야 한다.
57 |
58 | `pyvenv`는 독립된 버전의 파이썬 환경을 생성할 수 있게 해준다. **pyvenv를 이용하면 같은 시스템에서 같은 이름으로 설치된 패키지의 여러 버전을 충돌없이 사용할 수 있다.** 이 방법으로 같은 컴퓨터에서 서로 다른 여러 프로젝트에서 작업하고 많은 도구를 쓸 수 있다.
59 |
60 | **`pyvenv`는 명시적인 버전의 패키지들과 의존 패키지들을 완전히 분리된 디렉토리 구조로 설치하여 독립된 환경을 구성한다.** 덕분에 코드에 알맞은 파이썬 환경을 재현할 수 있다. 이런 가상환경 구성은 앞에서 다룬 문제를 피하는 올바른 방법이다.
61 |
62 |
63 |
64 | pyvenv를 사용해보자. 먼저 로컬 컴퓨터에 설치된 전역 파이썬의 버전과 프로그램의 위치를 살펴보자.
65 |
66 | ```shell
67 | # 전역 파이썬 위치 확인
68 | # 구체적인 값은 머신마다 다름
69 | $ which python
70 |
71 | /home/sunghwanpark/.pyenv/shims/python
72 | ```
73 |
74 | ```shell
75 | # 전역 파이썬 버전 확인
76 | # 구체적인 값은 머신마다 다름
77 | $ python --version
78 |
79 | 3.6.3.
80 | ```
81 |
82 | 전역 파이썬의 위치와 버전을 확인했다. 이는 이후 `pyvenv`를 사용하면서 디렉토리별, 즉 프로젝트별로 서로 다른 파이썬의 위치와 버전을 갖는 것을 확인하기 위해 출력했다.
83 |
84 | 또 전역 스코프에에 'pytz'라는 모듈이 설치되어 있는지도 확인한다.
85 |
86 | ```shell
87 | $ python -c 'import pytz'
88 |
89 | # (결과는 출력되지 않음)
90 | ```
91 |
92 | python에 `-c` 옵션은 그 다음에 받는 문자열로 된 파이썬 코드를 실행하겠다는 의미이다. 지금은 단순히 'pytz' 모듈을 임포트만 하기 때문에 따로 출력물이 없었다. 단, 해당 모듈이 설치되어 있지 않았다면 에러가 발생했을 것이다. **즉, 현재 내 머신의 파이썬 전역 스코프에 'pytz' 모듈은 설치되어 있는 상태다.**
93 |
94 |
95 |
96 | 이제 pyvenv를 사용해서 myproject라는 새 가상환경을 생성해보자. **각 가상환경은 자신만의 독립적인 디렉토리에 있어야 한다.** 명령의 결과는 디렉토리와 파일의 트리 형태로 나타난다.
97 |
98 | ```shell
99 | $ pyvenv /tmp/myproject
100 | $ cd /tmp/myproject
101 | $ ls
102 |
103 | bin include lib lib64 pyvenv.cfg
104 | ```
105 |
106 | pyvenv 명령을 통해 하나의 프로젝트 단위가 되는 디렉토리를 생성했다. 그 안에는 미리 설정된 디렉토리와 파일들이 생성됐다. **가상환경을 시작하려면 쉘에서 source 명령으로 bin/activate 스크립트를 실행한다.** activate는 가상환경에 맞게 모든 환경변수를 수정한다. 또한 현재 무엇을 하고 있는지 확실히 알 수 있게 쉘 프롬프트를 수정하여 가상환경 이름('myproject')을 포함하게 한다.
107 |
108 | ```shell
109 | $ source bin/activate
110 |
111 | (myproject) $
112 | # '$' 앞에 가상환경 이름이 붙었다.
113 | ```
114 |
115 | 가상환경을 시작하면(사용자가 따로 설정을 안 했을 때) `$` 앞에 가상환경 이름이 붙는다. 이를 통해 **개발자는 현재 쉘이 전역 파이썬 스코프를 사용하지 않고 프로젝트별 가상환경을 유지, 관리하고 있으며(여기서는 'myproject'), 이 가상환경에 모듈을 설치하고, 사용할 것이라고 알 수 있다.** 참고로 `source`는 파이썬 명령어는 아니고, 유닉스 쉘의 쉘 스크립트를 실행시키는 명령어다. 'bin/activate'를 에디터로 확인해보면 쉘 명령어 모음을 확인할 수 있다.
116 |
117 | 이렇게 가상환경이 활성화되면 파이썬의 경로가 가상 환경 디렉토리로 이동했음을 알 수 있다.
118 |
119 |
120 | ```shell
121 | (myproject) $ which python
122 |
123 | /tmp/myproject/bin/python
124 | ```
125 |
126 | 이는 외부 시스템에 대한 변경은 가상 환경에 영향을 미치지 않음을 보장한다. 외부 시스템이 기본 파이썬 버전을 3.7로 업그레이드하더라도 방금 만든 가상환경은 여전히 기존 버전을 유지한다.
127 |
128 | pyvenv로 생성한 가상환경은 pip과 setuptools 말고는 설치된 패키지가 없이 시작한다. 외부 시스템에 전역 모듈로 설치한 pytz 패키지를 사용하려고 하면 가상환경에서는 이 패키지가 설치되어 있지 않아 실패한다.
129 |
130 | ```shell
131 | (myproject) $ python -c 'import pytz'
132 |
133 | Traceback (most recent call last):
134 | File "", line 1, in
135 | ModuleNotFoundError: No module named 'pytz'
136 | ```
137 |
138 | 아까와 달리 'pytz'가 설치되어 있지 않다고 출력된다. 이는 가상환경과 전역환경이 서로 다른 환경을 유지한다는 증거이기도 하다. 이제 가상환경에 pytz 모듈을 설치해보자.
139 |
140 | ```shell
141 | (myproject) $ pip install pytz
142 | ```
143 |
144 | 설치한 후에는 아까와 같은 테스트 임포트 명령으로 패키지가 가상환경에서 잘 동작함을 알 수 있다.
145 |
146 | ```shell
147 | (myproject) $ python -c 'import pytz'
148 |
149 | # 에러가 출력되지 않음
150 | ```
151 |
152 |
153 |
154 | **가상환경에 대한 작업을 완료하고 기본 시스템(전역 스코프)으로 돌아가려면 `deactivate` 명령을 실행하면 된다.** 이 명령은 python 명령줄 도구의 위치를 포함한 환경을 시스템 기본 환경으로 복원한다.
155 |
156 |
157 | ```shell
158 | (myproject) $ deactivate
159 | $ which python
160 |
161 | /home/sunghwanpark/.pyenv/shims/python
162 | ```
163 |
164 | 다시 가상환경으로 복귀하고 싶다면 이전과 마찬가지로 실행하면 된다.
165 |
166 |
167 |
168 | ## 3. 의존성 재현
169 |
170 | 가상환경을 갖추고 있다면 pip으로 필요한 패키지를 계속 설치할 수 있다. 언젠가는 이 환경을 다른 머신으로 복사하고 싶을 수 있다. 예를 들어 프로덕션 서버에 테스트 서버에서 관리하던 가상환경을 재현하고 싶다고 하자. 혹은 자기 머신에 다른 사람의 환경을 복사하여 다른 사람이 작성한 코드를 실행하고 싶을 수도 있다.
171 |
172 | pyvenv를 위시한 다른 가상환경 프로그램에서는 이런 상황에 쉽게 대처할 수 있다. **`pip freeze` 명령을 사용하면 모든 명시적인 패키지 의존성을 파일에 저장할 수 있다. 관례상 이 파일의 이름은 `requirements.txt`로 지정한다.** 이 파일은 기억하면 좋은데, 여러 파이썬 git repository에서 많이 발견되기 때문이다.
173 |
174 | ```shell
175 | (myproject) $ pip freeze > requirements.txt
176 | (myproject) $ cat requirements.txt
177 |
178 | pytz==2019.1
179 | ```
180 |
181 | 이제 다른 경로, 또는 다른 머신에서 이 가상환경을 그대로 복원한다고 하자. 이전과 마찬가지로 pyvenv로 새 디렉토리를 생성한 후 activate한다.
182 |
183 | ```shell
184 | $ pyvenv otherproject
185 | $ cd otherproject
186 | $ source bin/activate
187 | ```
188 |
189 | 새로 만든 가상환경에는 기본 모듈 외에는 설치되어 있지 않다.
190 |
191 | ```
192 | (otherproject) $ pip list
193 |
194 | pip (9.0.1)
195 | setuptools (28.8.0)
196 | ```
197 |
198 | myproject에서 만든 **requirements.txt 파일을 `pip install`를 통해 실행해서 모든 패키지를 설치한다.**
199 |
200 | ```shell
201 | (otherproject) $ pip install -r requirements.txt
202 |
203 | # requirements.txt는 myproject에서 복사해왔다고 가정
204 | ```
205 |
206 | 해당 명령어를 입력하면 myproject의 모든 패키지를 원 버전에 맞게 완벽하게 설치할 수 있다. 저 명령어의 `-r` 옵션은 'recursive'의 약자로 requirements.txt에 나열되어 있는 모든 패키지를 반복적으로 설치하겠다는 뜻이 된다. 설치가 문제없이 완료되면 프로젝트의 모든 환경이 다른 환경으로 완벽하게 복제됐다.
207 |
208 | **`requirements.txt`를 사용하면 버전 제어 시스템을 사용해 다른 사람들과 협력하기에 좋다.** 변경한 코드를 커밋할 때 패키지 의존성 목록이 수정되어 정확하게 변경됨을 보장할 수 있다.
209 |
210 |
211 |
212 | ## 4. 핵심 정리
213 |
214 | * 가상환경은 pip을 사용하여 같은 머신에서 같은 패키지의 여러 버전을 충돌 없이 설치할 수 있게 해준다.
215 | * 가상환경을 만드는 프로그램은 많지만 내장 pyvenv를 사용하면 추가 설치없이 가상환경을 관리할 수 있다.
216 | * pip freeze로 환경에 대한 모든 요구 사항을 덤프할 수 있다. `requirements.txt` 파일을 `pip install -r` 명령의 인수로 전달하여 환경을 재현할 수 있다.
217 | * 파이썬 3.4 이전 버전에서는 pyvenv 도구를 별도로 설치해야 했고 명령줄 도구의 이름도 pyvenv가 아닌 virtualenv다.
218 |
--------------------------------------------------------------------------------
/files/BetterWay54_ConsiderModuleScopeForDeployment.md:
--------------------------------------------------------------------------------
1 | ## Better Way 54. 배포 환경을 구성하는 데는 모듈 스코프 코드를 고려하자
2 |
3 | #### 282쪽
4 |
5 | * Created : 2019/07/03
6 | * Modified: 2019/07/03
7 |
8 |
9 |
10 |
11 | ## 1. 모듈 내 코드를 통한 환경 제어
12 |
13 | 배포 환경(deployment environment)은 프로그램을 실행하는 구성을 말한다. 모든 프로그램에는 적어도 하나의 배포 환경과 제품 환경(production environment)이 있다. **프로그램을 작성하는 첫 번재 목적은 제품 환경에서 프로그램이 동작하도록 하고 결과를 얻는 것이다.**
14 |
15 | 프로그램을 작성하거나 수정하려면 개발에 사용 중인 컴퓨터에서 프로그램이 동작하게 해야 한다. 개발 환경(development environment)의 설정은 제품 환경과는 많이 다르다. 예를 들어 리눅스 워크스테이션(개발 환경)으로 슈퍼컴퓨터용 프로그램(제품 환경)을 작성할 수도 있다.
16 |
17 | 제품 환경과 개발 환경의 차이를 구분하는 것이 중요하다. **보통 제품 환경에서는 개발 환경에서 재현하기 어려운 많은 외부 기능을 요구하기 마련이다.**
18 |
19 | 예를 들어 프로그램이 웹 서버 컨테이너에서 실행되고 데이터베이스에 접근해야 한다고 하자. 이는 프로그램의 코드를 수정할 때마다 서버 컨테이너를 실행해야 하고, 데이터베이스를 적절하게 설정해야 하며, 프로그램에는 접근용 패스워드가 필요함을 의미한다. 한 줄을 변경하더라도 프로그램이 올바르게 동작하는지 검증하는 일을 하려고 한다면 상당한 노력이 필요하다. **즉, 제품 환경과 개발 환경을 구분하는 것은 정말 중요하다.**
20 |
21 | 이런 문제를 해결하는 가장 좋은 방법은 시작할 때 프로그램의 일부를 오버라이드해서 배포 환경에 따라 서로 다른 기능을 제공하는 것이다. 예를 들면, 서로 다른 \_\_main\_\_ 모듈을 두 개 만들어서 하나는 제품용으로, 다른 하나는 개발용으로 사용할 수 있다.
22 |
23 | ```python
24 | # dev_main.py, 개발용
25 | TESTING = True
26 | import db_connection
27 | db = db_connection.Database()
28 | ```
29 |
30 | ```
31 | # prod_main.py, 제품용
32 | TESTING = False
33 | import db_connection
34 | db = db_connection.Database()
35 | ```
36 |
37 | 이 두 파일의 유일한 차이점은 TESTING 상수의 값뿐이다. 프로그램에서 다른 모듈들은 \_\_main\_\_ 모듈을 임포트하고 TESTING 값으로 자체의 속성을 정의하는 방법을 결정한다.
38 |
39 | ```python
40 | # db_connection.py
41 |
42 | import __main__
43 |
44 | class TestingDatabase:
45 | pass
46 |
47 | class RealDatabase:
48 | pass
49 |
50 |
51 | if __main__.TESTING:
52 | Database = TestingDatabase
53 | else:
54 | Database = RealDatabase
55 | ```
56 |
57 | 여기서 알아야 할 중요한 동작은 **(함수나 메소드 내부가 아닌) 모듈 스코프에서 동작하는 코드는 일반 파이썬 코드라는 점이다.** 모듈 수준에서 if 문을 이용하여 모듈이 이름을 정의하는 방법을 결정할 수 있다. 이 방법으로 모듈을 다양한 배포 환경에 알맞게 만들 수 있다. 또한 데이터베이스 설정 등이 필요 없을 때 재현해야 하는 수고를 덜 수 있다. 목(mock)이나 가짜 구현을 주입하여 대화식 개발과 테스트를 용이하게 할 수도 있다.
58 |
59 |
60 |
61 | 이 방법을 외부의 전제를 우회하는 목적 이외에도 사용할 수 있다. 예를 들어, 프로그램이 호스트 플랫폼에 따라 다르게 동작해야 한다면 모듈의 최상위 구성요소를 정의하기 전에 `sys` 모듈을 조사하면 된다.
62 |
63 | ```python
64 | # db_connection.py
65 | import sys
66 |
67 | class Win32Database:
68 | pass
69 |
70 |
71 | class PosixDatabase:
72 | pass
73 |
74 |
75 | if sys.platform.startswith('win32'):
76 | Database = Win32Database
77 | else:
78 | Database = PosixDatabase
79 | ```
80 |
81 | 이와 유사하게 os.environ에 들어 있는 환경변수를 기반으로 모듈을 정의할 수도 있다.
82 |
83 |
84 |
85 |
86 | ## 2. 핵심 정리
87 |
88 | * 종종 프로그램을 여러 배포 환경에서 실행해야 하며, 각 환경마다 고유한 전제와 설정이 있다.
89 | * 모듈 스코프에서 일반 파이썬 문장을 사용해서 모듈 컨텐츠를 다른 배포 환경에 맞출 수 있다.
90 | * 모듈 컨텐츠는 'sys'와 'os' 모듈을 이용한 호스트 조사 내역 같은 외부 조건의 결과물이 될 수 있다.
91 |
--------------------------------------------------------------------------------
/files/BetterWay55_UseReprForDebug.md:
--------------------------------------------------------------------------------
1 | ## Better Way 55. 디버깅 출력용으로는 repr 문자열을 사용하자
2 |
3 | #### 285쪽
4 |
5 | * Created : 2017/08/20
6 | * Modified: 2019/07/04
7 |
8 |
9 |
10 |
11 | ## 1. repr 함수의 특징
12 |
13 | 파이썬 프로그램을 디버깅할 때 우리가 자주 쓰는 print는 생각보다 많은 일을 한다. 보통 파이썬 내부를 일반 속성으로 쉽게 접근할 수 있다. print로 프로그램이 실행 중에 상태가 어떻게 변하는지 출력하여 어떤 부분이 제대로 동작하지 않는지 보기만 하면 된다.
14 |
15 | **print 함수는 사람이 읽기 쉬운 문자열 버전으로 결과를 출력한다.** 예를 들어 기본 문자열을 출력하면 따옴표 문자로 감싸지 않은 문자열의 내용을 출력한다.
16 |
17 |
18 | ```python
19 | print('foo bar')
20 |
21 | foo bar
22 | ```
23 |
24 | 출력 내용은 '%s' 포맷 문자열과 `%` 연산자로 출력하거나, 또는 _'{}'.format_ 과 같은 연산자로 출력한 내용과 동일하다.
25 |
26 | ```python
27 | print('foo bar') # 전통적인 방법
28 | print('{}'.format('foo bar')) # 파이썬 3문법
29 |
30 | foo bar
31 | foo bar
32 | ```
33 |
34 |
35 |
36 | **문제는 사람이 읽기 쉬운 문자열로는 값의 실제 타입이 무엇인지 명확하게 파악하기 힘들다는 것이다.** 예를 들어 print의 기본 출력으로는 숫자 5와 문자열 '5'를 구분할 수 없다.
37 |
38 | ```python
39 | print(5)
40 | print('5')
41 |
42 | 5
43 | 5
44 | ```
45 |
46 | **print로 디버깅한다면 이런 종류의 차이는 문제가 된다.** 디버깅 중에는 대부분 객체의 repr 버전을 보려고 한다. 내장 함수 `repr`은 객체의 출력가능한 표현을 반환하며, 이 표현은 객체를 가장 명확하게 이해할 수 있는 문자열 표현이어야 한다.
47 |
48 | ```python
49 | a = '\x07'
50 | print(a)
51 | print(repr(a))
52 |
53 | # 아무 것도 출력되지 않음.(ascii code 07 - bell)
54 | '\x07' # repr은 출력 가능한 형태를 출력하기 때문에 문자열 표현이 출력됨
55 | ```
56 |
57 | repr이 반환한 값을 내장 함수 eval에 전달하면 원래의 파이썬 객체와 동일한 결과가 나와야 한다.
58 |
59 | ```python
60 | b = eval(repr(a))
61 | assert a == b
62 | ```
63 |
64 | print로 디버깅할 때는 타입의 차이가 명확히 드러나도록 값을 출력하기 전에 repr을 사용해야 한다.
65 |
66 | ```python
67 | print(repr(5))
68 | print(repr('5'))
69 |
70 | 5
71 | '5'
72 | ```
73 |
74 |
75 | 위의 결과는 `%r` 포맷 문자열과 `%` 연산자로 출력한 결과와 같다.
76 |
77 | ```python
78 | # %r은 파이썬 한정으로 객체의 repr 표현을 표현해준다.
79 | print('%r' % 5)
80 | print('%r' % '5')
81 |
82 | 5
83 | '5'
84 | ```
85 |
86 |
87 |
88 | ## 2. 동적 파이썬 객체의 경우
89 |
90 | **동적 파이썬 객체의 경우 기본으로 사람이 이해하기 쉬운 문자열 값이 repr값과 같다.** 이는 print에 동적 객체를 넘기면 제대로 동작하므로 명시적으로 repr을 호출하지 않아도 됨을 의미한다. 불행하게도 object 인스턴스에 대한 repr 기본값은 특별히 도움이 되지 않는다.(object는 모든 파이썬 클래스의 부모 크래스)
91 |
92 | 예를 들어 간단한 클래스를 정의하고 그 값을 출력해보자.
93 |
94 | ```python
95 | class OpaqueClass:
96 | def __init__(self, x, y):
97 | self.x = x
98 | self.y = y
99 |
100 | obj = OpaqueClass(1, 2)
101 | print(obj)
102 |
103 | <__main__.OpaqueClass object at 0x102b1c28>
104 | ```
105 |
106 | 이 결과는 eval 함수에 넘길 수 없으며 객체의 인스턴스 필드에 대한 정보를 전혀 알려주지 않는다. 문제를 해결하는 방법은 두 가지가 있다.
107 |
108 | 1. 클래스를 제어할 수 있다면, 직접 \_\_repr\_\_ 메서드를 정의해서 객체를 재생성하는 파이썬 표현식을 담은 문자열을 반환하면 된다.
109 |
110 | ```python
111 | class BetterClass:
112 | def __init__(self, x, y):
113 | self.x = x
114 | self.y = y
115 |
116 | def __repr__(self):
117 | return 'BetterClass(%d, %d)' % (self.x, self.y)
118 |
119 | obj = BetterClass(1, 2)
120 | print(obj)
121 |
122 | BetterClass(1, 2)
123 | ```
124 |
125 | 2. 클래스 정의를 제어할 수 없을 때는 \_\_dict\_\_ 속성에 저장된 객체의 인스턴스 딕셔너리를 사용한다. 다음은 OpaqueClass 인스턴스의 내용을 출력하는 예다.
126 |
127 | ```python
128 | obj = OpaqueClass(4, 5)
129 | print(obj.__dict__)
130 |
131 | {'y': 5, 'x': 4}
132 | ```
133 |
134 |
135 |
136 | ## 3. 핵심 정리
137 |
138 | * 내장 타입에 print를 호출하면 사람이 이해하기는 쉽지만 타입 정보는 숨은 문자열 버전의 값이 나온다.
139 | * 내장 타입에 repr을 호출하면 출력할 수 있는 문자열 버전의 값이 나온다. 이 repr 문자열을 eval 내장 함수에 전달하면 원래 값으로 되돌릴 수 있다.
140 | * 포맷 문자열에서 %s는 str처럼 사람이 이해하기 쉬운 문자열을 만들며, %r은 repr처럼 출력하기 쉬운 문자열을 만들어낸다.
141 | * \_\_repr\_\_ 메서드를 정의하면 클래스의 출력 가능한 표현을 사용자화하고 더 자세한 디버깅 정보를 제공할 수 있다.
142 | * 객체의 \_\_dict\_\_ 속성에 접근하면 객체의 내부를 볼 수 있다.
143 |
--------------------------------------------------------------------------------
/files/BetterWay56_UseUnittest.md:
--------------------------------------------------------------------------------
1 | ## Better Way 56. unittest로 모든 것을 테스트하자
2 |
3 | #### 289쪽
4 |
5 | * Created : 2017/08/18
6 | * Modified: 2019/07/04
7 |
8 |
9 |
10 | ## 1. unittest를 통한 코드 테스트
11 |
12 | 파이썬에는 정적 타입 검사(Statically type checking) 기능이 없다. 컴파일 시에 정적으로 타입 검사를 하지 않기 때문에 컴파일러는 프로그램을 실행하면 제대로 동작함을 보장하지 않는다. 파이썬으로는 프로그램에서 호출하는 함수가 런타임에 정의되어 있을지 알 수 없기도 하다.(정적, 동적 타입검사에 대해서는 이 [포스트](https://thesocietea.org/2015/11/programming-concepts-static-vs-dynamic-type-checking/)를 참고.)
13 |
14 | **파이썬은 동적 타입 검사(dynamically type checking)하는 언어이기 때문에 더더욱 code test가 필요하다.** 좀더 극단적으로 말해 파이썬 프로그램을 신뢰할 수 있는 유일한 방법은 직접 테스트를 작성하는 것이다. 많은 경우 우리는 테스트를 하지 않으며 시간 낭비라고 생각하기도 한다. 하지만 내가 나 혼자 쓰는 프로그램 이상의 가치를 만들고 싶다면 테스트는 꼭 필요하다. 많은 기라성 같은 프로그래머가 강조하는 바이며 나는 'TDD를 하지 않는 회사는 들어가지 마라'고 이야기하는 개발자를 보기도 했다.
15 |
16 | 이번 장에서 다루는 [unittest](https://docs.python.org/3/library/unittest.html) 내장 모듈은 코드 테스트를 위한 모듈로 파이썬에서 코드를 테스트하는 가장 간단한 방법이기도 하다. 그리고 이 테스트 코드 작성 방식은 Django에서 지원하는 테스트 모듈과 비슷해서 공부하면 도움이 될 것이다.
17 |
18 | 간단한 예제를 보도록 하자.
19 |
20 | ```python
21 | # Utility function to test
22 |
23 | def to_str(data):
24 | if isinstance(data, str):
25 | return data
26 | elif isinstance(data, bytes):
27 | return data.decode('utf-8')
28 | else:
29 | raise TypeError('Must supply str or bytes, but found: %r' % data)
30 | ```
31 |
32 | str이나 bytes 형의 자료를 받아 이를 str로 변환해 반환하는 유틸리티 함수를 만들었다.
33 |
34 |
35 |
36 | 이 함수를 테스트할 코드를 작성해보자.
37 |
38 | ```python
39 | from unittest import TestCase, main
40 | from utils import to_str # 방금 만든 모듈
41 |
42 | class UtilsTest(TestCase):
43 | def test_to_str_bytes(self):
44 | self.assertEqual('hello', to_str(b'hello'))
45 |
46 | def test_to_str_str(self):
47 | self.assertEqual('hello', to_str('hello'))
48 |
49 | def test_to_str_bad(self):
50 | self.assertRaises(TypeError, to_str, object())
51 |
52 |
53 | if __name__ == '__main__':
54 | main()
55 | ```
56 |
57 | 테스트 코드를 작성하는 가장 대표적인 방식이다. 테스트 코드들을 담을 테스트 클래스는 unittest의 **TestCase를 상속 받아야 한다.** 그 안에 내가 테스트하고 싶은 함수들을 작성하는데, 그 함수들은 모두 이름이 **'test'로 시작해야 한다.** 'test'로 시작하지 않은 메소드는 테스트할 코드로 인식하지 않으며 이런 경우는 테스트 함수들에서 사용할 헬퍼 메소드를 작성한다면 좋을 것이다.
58 |
59 | 테스트 메소드가 어떤 종류의 Exception을 일으키지 않고 실행된다면 테스트가 성공적으로 통과한 것이며 TestCase 클래스는 테스트에서 단정하는 데(assert) 필요한 기본 헬퍼 메소드를 지원한다. 위 코드에서 동등성을 검사하는 _assertEqual_, 의도한 예외가 발생했는지 검사하는 _asssertRaises_ 등이다. TestCase 서브클래스에 자신만의 헬퍼 메소드를 정의하여 맞춤형 테스트를 만들 수도 있을 것이다. 물론 그 헬퍼 메소드는 'test'로 시작해서는 안 된다.
60 |
61 |
62 |
63 |
64 | 때때로 TestCase 클래스에서 테스트 메소드를 실행하기 전에 테스트 환경을 설정해야 한다. 그러려면 setUp과 tearDown 메소드를 오버라이드해야 한다. 이 메소드들은 각 테스트 메소드가 실행되기 전후에 각각 호출되며, 각 테스트가 분리되어 실행됨을 보장한다. 예를 들어 다음 코드는 각 테스트 전에 임시 디렉토리를 생성하고, 각 테스트가 종료한 후에 그 내용을 삭제하는 TestCase를 정의한 것이다.
65 |
66 | ```python
67 | from tempfile import TempDirectory
68 |
69 | class MyTest(TestCase):
70 | def setUp(self):
71 | self.test_dir = TemporaryDirectory()
72 |
73 | def tearDown(self):
74 | self.test_dir.cleanup()
75 |
76 | # ... 테스트 메소드
77 | ```
78 |
79 | 보통 관련 테스트의 각 집합별로 TestCase 하나를 정의한다. 때로는 예외 상황이 많은 각 함수별로 만들 수도 있고, 한 케이스로 모듈 내 모든 함수를 처리하기도 한다. 이는 프로그래머의 판단이며 가독성 있고 이해하기 쉽게 구분하여 테스트하면 좋을 것이다.
80 |
81 | **프로그램이 복잡해지면 코드를 별도로 테스트하는 대신에 모듈 간의 상호작용을 검증할 테스트를 추가할 수도 있다. 이게 바로 단위 테스트(unit test)와 통합 테스트(integration test)의 차이다.**
82 |
83 | 파이썬에서는 똑같은 이유로 두 종류의 테스트를 작성해야 한다. 모듈을 검증하지 않으면 실제로 함께 동작하는지 보장하지 못하기 때문이다.
84 |
85 | 테스트를 위한 서드파티 패키지도 물론 존재한다. [pytest](https://docs.pytest.org/en/latest/)는 unittest 코드도 동작하는 호환성을 보장하며 프로젝트도 성숙해서 믿고 사용할 수 있다고 한다. 기능이 더 강력한 것은 당연한다. Pycon 2017 kr의 한 강연에서도 테스트를 설명하면서 pytest를 홍보하기도 했다. pytest를 쓰면 unittest를 아예 쓸 일이 없다고 한다. 강력한 모듈인 것은 확실한 것 같다.
86 |
87 |
88 |
89 | ## 2. 핵심 정리
90 |
91 | * 파이썬 프로그램을 신뢰할 수 있는 유일한 방법은 테스트를 작성하는 것이다.
92 | * 내장 모듈 unittest는 좋은 테스트를 작성하는 데 필요한 대부분의 기능을 제공한다.
93 | * TestCase를 상속하고 테스트하려는 동작별로 메소드 하나를 정의해서 테스트를 정의할 수 있다.
94 | TestCase 클래스에 있는 테스트 메소드는 'test'라는 단어로 시작해야 한다.
95 | * 단위 테스트와 통합 테스트는 상호 보완적이므로 모두 작성해야 한다.
96 |
--------------------------------------------------------------------------------
/files/BetterWay57_Use_pdb.md:
--------------------------------------------------------------------------------
1 | ## Better Way 57. pdb를 이용한 대화식 디버깅을 고려하자
2 |
3 | #### 293쪽
4 |
5 | * Created : 2017/08/18
6 | * Modified: 2019/07/04
7 |
8 |
9 |
10 | ## 1. pdb를 통한 대화식 디버깅
11 |
12 | 누구나 프로그램을 개발할 때 코드에서 버그를 접하기 마련이다. 보통 디버그 할 때 우리는 print를 남발해서 버그 실행 위치를 찾고 올바르게 동작하는지 확인한다. 사정이 더 나으면 테스트 코드를 작성할 수도 있다.
13 |
14 | 하지만 이런 도구만으로는 근본적인 원인을 모두 찾아내지 못할 때도 있다. 더 강력한 도구가 필요하다면 파이썬에 내장된 대화식 디버거(interactive debugger)를 사용해볼만 하다. 디버거를 이용하면 프로그램의 상태를 조사하고, 지역변수를 출력하고, 파이썬 프로그램을 한 문장씩 실행해볼 수 있다.
15 |
16 | 대부분의 언어와 또 Pycharm 같은 고성능 IDE의 경우에서는 멈추게 할 소스 파일의 줄을 설정한 다음 프로그램을 실행하는 방법으로 디버거를 실행한다. 이와 달리 파이썬에서는 디버거를 사용하는 가장 쉬운 방법은 프로그램을 수정하여 조사할 만한 문제를 만나기 전에 직접 디버거를 실행하는 것이다. 디버거에서 파이썬을 실행하는 것과 평소처럼 실행하는 것 사이에는 별다른 차이가 없다. **디버거를 시작하려면 내장 모듈 [pdb](https://docs.python.org/3.7/library/pdb.html)를 import한 후 set\_trace 함수를 실행하기만 하면 된다.**
17 |
18 | 가령 숫자의 짝홀을 판별하는 함수를 만든다고 해보자.
19 |
20 | ```python
21 | def is_even():
22 | n = int(input("정수를 입력하세요. : "))
23 | if n % 2 == 0:
24 | print("짝수")
25 | else:
26 | print("홀수")
27 | ```
28 |
29 |
30 |
31 | 이 함수를 pdb로 추적하고 싶다면 함수 안에 다음 문장을 삽입한다.
32 |
33 | ```python
34 | def is_even()
35 | import pdb; pdb.set_trace()
36 | # ... 나머지 코드
37 | ```
38 |
39 | 'import pdb'와 'pdb.set\_trace()' 두 문장을 '#' 하나로 주석화하기 쉽도록 보통 한 문장으로 이어서 작성한다. 이 상태에서 is\_even 함수를 실행하자마자 set\_trace가 있는 지점에서 프로그램은 실행을 멈춘다. 그리고 프로그램을 시작한 터미널은 대화식 파이썬 쉘로 바뀐다.
40 |
41 | ```python
42 | is_even()
43 |
44 | -> n = int(input("정수를 입력하세요. : "))
45 | (Pdb)
46 | ```
47 |
48 | 이 (Pdb) 뒤에는 파이썬 쉘처럼 명령어들을 입력할 수 있다. 그리고 그 윗줄의 '->'은 파이썬 디버거의 포인터가 현재 함수의 이 줄에 있다는 것을 알 수 있게 한다.
49 |
50 | 프롬프트에서 변수의 값을 출력하려면 지역 변수의 이름을 입력하면 된다. 모든 지역 변수의 리스트를 확인하려면 내장 locals 함수를 사용할 수도 있다.
51 |
52 | 이 외에도 모듈 import, 전역 상태 조사, 새 객체 생성, help 함수 실행, 심지어 프로그램의 일부를 수정하는 등 디버깅을 보조하는 작업은 무엇이든 다 할 수 있다. 게다가 디버거는 실행 중인 프로그램을 더 쉽게 조사할 수 있는 명령 세 개를 제공한다.
53 |
54 | * bt : 현재 실행 호출 스택의 추적 정보를 출력한다. 이 정보는 프로그램의 어느 부분에 있고 pdb.set\_trace 트리거 지점에 어떻게 도달했는지 보여준다.
55 | * up : 현재 함수의 호출자 쪽으로 함수 호출 스택의 스코프를 이동한다. 이 동작으로 호출 스택의 상위 레벨에서 지역 변수를 조사할 수 있다.
56 | * down : 함수 호출 스택을 한 단계 낮춘다.
57 |
58 |
59 |
60 | 현재 상태를 조사하고 나면 다음과 같은 디버거 명령을 사용하여 좀더 세밀한 제어로 프로그램 실행을 재개할 수 있다.
61 |
62 | * step : 프로그램의 다음 줄까지 실행하고 제어를 디버거에 돌려준다. 다음 줄의 실행이 함수 호출을 포함하고 있다면 디버거는 호출된 함수 안에서 바로 멈춘다.
63 | * next : 현재 함수에서 다음 줄까지 프로그램을 실행한 다음 제어를 디버거에 돌려준다. 다음 줄의 실행이 함수 호출을 포함하고 있다면 호출된 함수가 반환할 때까지 디버거가 멈추지 않는다.
64 | * return : 현재 함수가 값을 반환할 때까지 프로그램을 실행하고 다음 제어를 디버거에 돌려준다.
65 | * continue: 다음 중단점(혹은 set\_trace가 다시 호출될 때)까지 프로그램 실행을 계속한다.
66 |
67 | 이 함수는 꼭 써볼 것을 권장한다. 여러 함수들을 실행하는 실제 코드에서 동작을 하면 동작 내용을 잘 알 수 있다.
68 | 나도 연습이 더 필요한 것으로 보인다.
69 |
70 |
71 |
72 | ## 2. 핵심 정리
73 |
74 | * *import pdb; pdb.set\_trace()* 문으로 프로그램의 관심 지점에서 직접 파이썬 대화식 디버거를 실행할 수 있다.
75 | * 파이썬 디버거 프롬프트는 실행 중인 프로그램의 상태를 조사하거나 수정할 수 있도록 해주는 파이썬 쉘이다.
76 | * pdb 쉘 명령을 이용하면 프로그램 실행을 세밀하게 제어할 수 있다. 또한 프로그램의 상태를 조사하거나 프로그램 실행을 이어가는 과정을 교대로 반복할 수 있다.
77 |
--------------------------------------------------------------------------------
/files/BetterWay58_ProfileBeforeOptimization.md:
--------------------------------------------------------------------------------
1 | ## Better Way 58. 최적화하기 전에 프로파일하자
2 |
3 | #### 295쪽
4 |
5 | * Created : 2019/07/06
6 | * Modified: 2019/07/06
7 |
8 |
9 |
10 | ## 1. 프로파일링을 통한 성능 개선
11 |
12 | 파이썬의 동적 특징은 런타임 성능에서 놀랄만한 동작을 보여준다. 느릴 것이라고 예상한 연산이 실제로는 엄청나게 빠르다거나(문자열 처리, 제너레이터), 빠를 것이라고 예상한 언어의 특성은 실제로 매우 느리다(속성 접근, 함수 호출). 파이썬 프로그램을 느리게 만드는 요인이 불분명할 수도 있다.
13 |
14 | **가장 좋은 방법은 최적화하기 전에 직관을 무시하고 직접 프로그램의 성능을 측정해보는 것이다.** 파이썬은 프로그램의 어느 부분이 실행 시간을 소비하는지 파악할 수 있도록 `내장 프로파일러`를 제공한다. 프로파일러를 이용하면 문제의 가장 큰 원인에 최적화 노력을 최대한 집중할 수 있고, 속도에 영향을 주지 않는 부분은 무시할 수 있다.
15 |
16 | 예를 들어 프로그램의 알고리즘이 느린 이유를 알고 싶다고 하자. 다음은 삽입 정렬로, 데이터 리스트를 정렬하는 함수다.
17 |
18 | ```python
19 | def insertion_sort(data):
20 | result = []
21 | for value in data:
22 | insert_value(result, value)
23 | return result
24 | ```
25 |
26 | 알다시피 **삽입 정렬의 핵심 메커니즘은 각 데이터의 삽입 지점을 찾는 함수다.** 다음은 극히 비효율적인 *insert\_value* 함수로, 입력 배열을 순차적으로 스캔한다.
27 |
28 | ```python
29 | def insert_value(arr, value):
30 | for i, existing in enumerate(arr):
31 | if existing > value:
32 | arr.insert(i, value)
33 | return
34 | arr.append(value)
35 | ```
36 |
37 | *insertion\_sort* 와 *insert\_value* 를 프로파일하려고 난수로 구성된 데이터 집합을 생성하고, 프로파일러에 넘길 test 함수를 정의한다.
38 |
39 | ```python
40 | from random import randint
41 |
42 | MAX_SIZE = 10 ** 4
43 | data = [randint(MAX_SIZE) for _ in range(MAX_SIZE)]
44 | test = lambda: insertion_sort(data)
45 | ```
46 |
47 |
48 |
49 | 이제 프로파일러를 사용해보자. 파이썬은 두 가지 내장 프로파일러를 제공하는데, 하나는 순수 파이썬 모듈(profile)이며 다른 하나는 C 확장 모듈(cProfile)이다. **후자는 프로파일링 동안에 프로그램의 성능에 미치는 영향을 최소화할 수 있어서 더 좋다. 순수 파이썬 프로파일러는 결과를 왜곡할 정도로 부하가 크다.** [관련 문서](https://docs.python.org/3/library/profile.html)는 여기.
50 |
51 | cProfile 모듈의 Profile 객체를 생성하고 runcall 메소드로 테스트 함수를 실행해보자.
52 |
53 | ```python
54 | from cProfile import Profile
55 |
56 | profiler = Profile()
57 | profiler.runcall(test)
58 | ```
59 |
60 | **테스트 함수의 실행이 끝나면 내장 모듈 pstats의 Stats 클래스로 함수의 성능 통계를 뽑을 수 있다.** pstats는 (아마) 'Print Statistics'의 약자로 프로파일링된 파이썬 코드에 대한 보고서를 출력하는 모듈이라고 한다. 이 모듈의 Stats 객체의 다양한 메소드를 이용하면 프로파일 정보를 선택하고 정렬하는 방법을 조절해서 관심 있는 정보만 볼 수 있다.
61 |
62 | ```python
63 | from pstats import Stats
64 |
65 | stats = Stats(profiler)
66 | stats.strip_dirs()
67 | stats.sort_stats('cumulative')
68 | stats.print_stats()
69 | ```
70 |
71 | 결과는 함수로 구성된 정보의 테이블이다. 데이터 샘플은 runcall 메소드가 실행되는 동안 프로파일러가 활성화되어 있을 때만 얻어온다.
72 |
73 | ```
74 | 20003 function calls in 3.985 seconds
75 |
76 | Ordered by: cumulative time
77 |
78 | ncalls tottime percall cumtime percall filename:lineno(function)
79 | 1 0.000 0.000 3.985 3.985 :21()
80 | 1 0.012 0.012 3.985 3.985 :1(insertion_sort)
81 | 10000 3.925 0.000 3.973 0.000 :8(insert_value)
82 | 9995 0.048 0.000 0.048 0.000 {method 'insert' of 'list' objects}
83 | 5 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}
84 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
85 | ```
86 |
87 | 프로파일러 통계 칼럼의 의미를 간략히 알아보자.
88 |
89 | * ncalls: 프로파일링 주기 동안 함수 호출 횟수
90 | * tottime: 함수가 실행되는 동안 소비한 초 단위의 시간으로, 다른 함수 호출을 실행하는 데 걸린 시간은 배제한다.
91 | * tottime percall: 함수를 호출하는 데 걸린 평균 시간이며 초 단위다. 다른 함수의 호출 시간은 배제한다. tottime을 ncalls로 나눈 값이다.
92 | * cumtime: 함수를 실행하는 데 걸린 초 단위 누적 시간이며, 다른 함수를 호출하는 데 걸린 시간도 포함한다.
93 | * cumtime percall: 함수를 호출할 때마다 걸린 시간에 대한 초 단위 평균 시간이며, 다른 함수를 호출하는 데 걸린 시간도 포함한다. cumtime을 ncalls로 나눈 값이다.
94 |
95 | **프로파일러 통계를 보면 테스트에서 CPU를 가장 많이 사용한 부분은 *insert\_value* 함수에서 소비한 누적 시간이라는 것을 알 수 있다.**
96 |
97 |
98 |
99 | 이번에는 내장 모듈 bisect를 사용하도록 *insert\_value* 를 재정의한다.
100 |
101 | ```python
102 | from bisect import bisect_left
103 |
104 |
105 | def insert_value(arr, value):
106 | i = bisect_left(arr, value)
107 | arr.insert(i, value)
108 | ```
109 |
110 | 다시 프로파일러를 실행하여 새 프로파일러 통계를 생성한다. 새로운 함수는 더 빨라졌고, 누적 시간은 이전의 함수에 비해 거의 100배 이상 줄었다.
111 |
112 |
113 | ```
114 | 30003 function calls in 0.084 seconds
115 |
116 | Ordered by: cumulative time
117 |
118 | ncalls tottime percall cumtime percall filename:lineno(function)
119 | 1 0.000 0.000 0.084 0.084 :27()
120 | 1 0.010 0.010 0.084 0.084 :1(insertion_sort)
121 | 10000 0.013 0.000 0.074 0.000 :10(insert_value)
122 | 10000 0.042 0.000 0.042 0.000 {method 'insert' of 'list' objects}
123 | 10000 0.019 0.000 0.019 0.000 {built-in method _bisect.bisect_left}
124 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
125 | ```
126 |
127 |
128 |
129 |
130 | ## 2. 다른 사례
131 |
132 | 때로는 전체 프로그램을 프로파일할 때 `공통` 유틸리티 함수에서 대부분의 실행 시간을 소비할 수도 있다. 프로파일러의 기본 출력은 유틸리티 함수가 프로그램의 다른 부분에서 얼마나 많이 호출되는지는 보여주지 않기 때문에 이해하기가 어렵다.
133 |
134 | 예를 들어 다음 *my\_utility* 함수는 프로그램에 있는 다른 두 함수에서 반복적으로 호출된다.
135 |
136 |
137 | ```python
138 | def common_utility(a, b):
139 | for _ in range(10000):
140 | a * b
141 |
142 | def first_func():
143 | for _ in range(1000):
144 | common_utility(4, 5)
145 |
146 |
147 | def second_func():
148 | for _ in range(10):
149 | common_utility(1, 3)
150 |
151 |
152 | def my_program():
153 | for _ in range(20):
154 | first_func()
155 | second_func()
156 | ```
157 |
158 | 이 코드를 프로파일하고 앞서 사용한 *print\_stats* 출력을 사용하면 이해하기 어려운 통계 결과가 나온다.
159 |
160 |
161 | ```
162 | 20242 function calls in 0.014 seconds
163 |
164 | Ordered by: cumulative time
165 |
166 | ncalls tottime percall cumtime percall filename:lineno(function)
167 | 1 0.000 0.000 0.014 0.014 :15(my_program)
168 | 20 0.009 0.000 0.013 0.001 :5(first_func)
169 | 20200 0.044 0.000 0.004 0.000 :1(common_utility)
170 | 20 0.000 0.000 0.000 0.000 :10(second_func)
171 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
172 | ```
173 |
174 | *common\_utility* 함수가 대부분의 실행 시간을 소비하는 원인이라는 게 명확하지만, **이 함수가 왜 이렇게 많이 호출되는지는 명확하게 알기 어렵다.** 이 함수는 여러 함수에서 공통적으로 쓰이기 때문이다. 전체 호출 횟수에서(ncalls) 다른 여러 함수들의 호출 지분을 알기 어렵다는 말과도 같다. 프로그램 코드를 직접 읽어 파악할 수도 있겠지만 결코 좋은 방법은 아니다.
175 |
176 | 다행히 Stats 클래스는 이런 상황에도 대처할 수 있는, 호출자를 찾는 메소드도 지원한다.
177 |
178 |
179 | ```python
180 | stats.print_callers()
181 | ```
182 |
183 | 메소드를 통해 반환된 프로파일러 통계 테이블은 호출된 함수를 왼쪽에 보여주며, 누가 이런 호출을 하는지를 오른쪽에 보여준다. 표를 통해 *common\_utility* 가 *first\_func* 에서 가장 많이 사용되었음을 명확하게 알 수 있다.
184 |
185 |
186 | ```
187 | Ordered by: cumulative time
188 |
189 | Function was called by...
190 | ncalls tottime cumtime
191 | :16(my_program)
193 | :6(first_func)
194 | 200 0.000 0.000 :11(second_func)
195 | :16(my_program)
196 | {method rofiler' objects} <-
197 | ```
198 |
199 |
200 |
201 | ## 3. 핵심 정리
202 |
203 | * 성능 저하를 일으키는 원인이 때로는 불분명하므로 파이썬 프로그램을 최적화하기 전에 프로파일해야 한다.
204 | * cProfile이 더 정확한 프로파일링 정보를 제공하는 내장 모듈이므로 이것을 사용하자.
205 | * Profile 객체의 runcall 메소드는 함수 호출 트리를 프로파일하는 데 필요한 모든 기능을 제공한다.
206 | * Stats 객체는 프로그램 성능을 이해하는 데 필요한 프로파일링 정보를 선택하고 출력하는 기능을 제공한다.
207 |
--------------------------------------------------------------------------------
/files/BetterWay59_UseTracemalloc.md:
--------------------------------------------------------------------------------
1 | ## Better Way 59. tracemalloc으로 메모리 사용 현황과 누수를 파악하자
2 |
3 | #### 301쪽
4 |
5 | * Created : 2019/07/06
6 | * Modified: 2019/07/06
7 |
8 |
9 |
10 | ## 1. 파이썬의 내부 메모리 관리
11 |
12 | 일반적인 CPython의 기본구현은 `참조 카운팅`(reference counting)으로 메모리를 관리한다. 참조 카운팅은 객체의 참조가 모두 해제되면 참조된 객체 역시 정리됨을 보장한다. 또한 CPython은 자기 참조 객체가 결국 가비지 컬렉션되는 것을 보장하는 사이클 디텍터(cycle detector)도 갖추고 있다.
13 |
14 | 이론적으로는 대부분의 파이썬 프로그래머가 프로그램에서 일어나는 메모리 할당과 해제를 걱정할 필요가 없다는 의미다. 즉, 언어와 CPython 런타임이 자동으로 처리한다. 그러나 실제로 프로그램은 결국 참조 때문에 메모리 부족에 처한다. 파이썬 프로그램이 어디서 메모리를 사용하거나 누수를 일으키는지 알아내는 건 힘든 도전 과제다.
15 |
16 | **메모리 사용을 디버깅하는 첫 번째 방법은 내장 모듈 gc(garbage collection)에 요청하여 가비지 컬렉터에 알려진 모든 객체를 나열하는 것이다.** gc가 그렇게 정확한 도구는 아니지만 이 방법을 이용하면 프로그램의 메모리가 어디서 사용되는지 금방 알 수 있다.
17 |
18 | 다음과 같이 gc를 사용해 현재 런타임에 존재하는 모든 객체의 수를 출력하는 프로그램을 만들자. 또 할당된 객체들의 샘플을 소량 출력한다.
19 |
20 | ```python
21 | import gc
22 |
23 | found_objects = gc.get_objects()
24 | print(f'{len(found_objects)} objects now')
25 |
26 | 58185 objects now
27 |
28 |
29 |
30 | for obj in found_objects[:3]:
31 | print(repr(obj)[:100])
32 |
33 |
34 | KeyPress(key=Key(''), data='\n')
35 | {'key': Key(''), 'data': '\n'}
36 | [_Binding(keys=(Key(''),), handler=), _Binding(keys=(Ke
37 | ```
38 |
39 |
40 | *gc.get\_objects* 를 사용할 때의 문제는 객체가 어떻게 할당되는지 아무런 정보도 제공하지 않는다는 점이다. 복잡한 프로그램에서는 객체의 특정 클래스가 여러 방법으로 할당될 수 있다. **객체의 전체 개수는 메모리 누수가 있는 객체 할당 코드를 찾는 것만큼은 중요하지 않다.**
41 |
42 |
43 |
44 |
45 | ## 2. tracemalloc으로 메모리 누수 찾기
46 |
47 | 파이썬 3.4부터는 새 내장 모듈 [tracemalloc](https://docs.python.org/3/library/tracemalloc.html)으로 이 문제를 해결한다. tracemalloc은 'Trace Memory Allocation'의 약자로, 객체가 할당된 위치에 연결될 수 있도록 해준다. 다음은 tracemalloc을 사용하여 프로그램에서 메모리를 가장 많이 사용하는 세 부분을 출력하는 예다.
48 |
49 |
50 |
51 | ```python
52 | import tracemalloc
53 |
54 | tracemalloc.start(10)
55 | # 스택 프레임을 최대 10개까지 저장
56 |
57 |
58 | time1 = tracemalloc.take_snapshot()
59 |
60 | for i in range(10000):
61 | exec('v' + str(i) + ' = ' + str(i))
62 |
63 | time2 = tracemalloc.take_snapshot()
64 |
65 | stats = time2.compare_to(time1, 'lineno')
66 |
67 | for stat in stats[:3]:
68 | print(stat)
69 |
70 |
71 | :10: size=266 KiB (+266 KiB), count=9743 (+9743), average=28 B
72 | :11: size=0 B (-266 KiB), count=0 (-9743)
73 | /home/sunghwanpark/.../mouse_handlers.py:29: size=126 KiB (-6848 B), count=2020 (-107), average=64 B
74 | ```
75 |
76 | 어떤 객체들이 프로그램 메모리 사용량을 주로 차지하고, 소스 코드의 어느 부분에서 할당되는지를 쉽게 알 수 있다.
77 |
78 |
79 |
80 | tracemalloc 모듈은 각 할당의 전체 스택 트레이스(stack trace)도 출력할 수 있다.(start 메소드에 넘긴 프레임 개수까지). 다음 코드는 프로그램에서 메모리 사용량의 가장 큰 근원이 되는 부분의 스택 트레이스를 출력한다.
81 |
82 | ```python
83 | stats = time2.compare_to(time1, 'traceback')
84 | top = stats[0]
85 | print('\n'.join(top.traceback.format()))
86 |
87 |
88 | File "", line 10
89 | exec('v' + str(i) + ' = ' + str(i))
90 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2910
91 | exec(code_obj, self.user_global_ns, self.user_ns)
92 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2850
93 | if self.run_code(code, result):
94 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2728
95 | interactivity=interactivity, compiler=compiler, result=result)
96 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/terminal/interactiveshell.py", line 471
97 | self.run_cell(code, store_history=True)
98 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/terminal/interactiveshell.py", line 480
99 | self.interact()
100 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/terminal/ipapp.py", line 356
101 | self.shell.mainloop()
102 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/traitlets/config/application.py", line 658
103 | app.start()
104 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/lib/python3.6/site-packages/IPython/__init__.py", line 125
105 | return launch_new_instance(argv=argv, **kwargs)
106 | File "/home/sunghwanpark/.pyenv/versions/3.6.3/bin/ipython", line 11
107 | sys.exit(start_ipython())
108 | ```
109 |
110 | 이와 같은 스택 트레이스는 공통 함수의 어느 부분이 프로그램의 메모리를 많이 소비하는지 알아내는 데 가장 중요한 정보다. 트레이스의 개수가 정확히 10개인 것을 알 수 있는 것도 눈여겨 보자.
111 |
112 |
113 |
114 |
115 | ## 3. 핵심 정리
116 |
117 | * 파이썬 프로그램이 메모리를 어떻게 사용하고, 메모리 누수를 일으키는지를 이해하기는 어렵다.
118 | * gc 모듈은 어떤 객체가 존재하는지를 이해하는 데 도움을 주지만, 해당 객체가 어떻게 할당되었는지에 대한 정보는 제공하지 않는다.
119 | * 내장 모듈 tracemalloc은 메모리 사용량의 근원을 이해하는 데 필요한 강력한 도구를 제공한다.
120 | * tracemalloc은 파이썬 3.4와 그 이후 버전에서만 사용할 수 있다.
121 |
--------------------------------------------------------------------------------
/images/diamond-inheritance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shoark7/Effective-Python/a5d720592cf6d72007f78f79e6e50d7ffdc2adf0/images/diamond-inheritance.png
--------------------------------------------------------------------------------