넥슨코리아 사내 발표자료로 왓 스튜디오에서 파이썬으로 《야생의 땅: 듀랑고》 서버를 비롯한 여러가지 도구를 만든 경험을 공유합니다.
- 게임서버와 각종 툴, 테스트/빌드/배포 시스템을 만들 때 사용한 재료
- 파이썬 코드 품질 개선, 디버깅, 프로파일링, 최적화
- 파이썬 오픈소스 생태계와 왓 스튜디오가 하는 오픈소스 활동
82. 1. 파이썬, 인터프리터
2. 동시성
3. 웹 서버
4. 터미널 앱
5. 빌드와 배포
6. 컨벤션 통일
7. REPL
8. 디버깅
9. 자료구조
10. 제너레이터
11. 성능 측정
12. 로드타임
13. 메모리 최적화
14. 흔한 실수
15. 오픈소스
저희가 이런 걸 만들 때
어떤 재료를 써왔고
83. 1. 파이썬, 인터프리터
2. 동시성
3. 웹 서버
4. 터미널 앱
5. 빌드와 배포
6. 컨벤션 통일
7. REPL
8. 디버깅
9. 자료구조
10. 제너레이터
11. 성능 측정
12. 로드타임
13. 메모리 최적화
14. 흔한 실수
15. 오픈소스
또 어떤 재료를 안 썼거나
혹은 못 썼는지
84. 1. 파이썬, 인터프리터
2. 동시성
3. 웹 서버
4. 터미널 앱
5. 빌드와 배포
6. 컨벤션 통일
7. REPL
8. 디버깅
9. 자료구조
10. 제너레이터
11. 성능 측정
12. 로드타임
13. 메모리 최적화
14. 흔한 실수
15. 오픈소스
그리고 코드 품질은
어떻게 관리하고 개선하고 있는지
85. 1. 파이썬, 인터프리터
2. 동시성
3. 웹 서버
4. 터미널 앱
5. 빌드와 배포
6. 컨벤션 통일
7. REPL
8. 디버깅
9. 자료구조
10. 제너레이터
11. 성능 측정
12. 로드타임
13. 메모리 최적화
14. 흔한 실수
15. 오픈소스
주제가 꽤 다양하긴 한데
하나씩 살펴보겠습니다.
136. g1 = gevent.spawn(requests.get, url1)
g2 = gevent.spawn(requests.get, url2)
g3 = gevent.spawn(requests.get, url3)
gevent.joinall([g1, g2, g3])
print([g.value for g in [g1, g2, g3]])
코루틴을 직접 다루는 건
굉장히 까다로워요.
137. g1 = gevent.spawn(requests.get, url1)
g2 = gevent.spawn(requests.get, url2)
g3 = gevent.spawn(requests.get, url3)
gevent.joinall([g1, g2, g3])
print([g.value for g in [g1, g2, g3]])
gevent는 다루기 까다로운
greenlet 코루틴을
138. g1 = gevent.spawn(requests.get, url1)
g2 = gevent.spawn(requests.get, url2)
g3 = gevent.spawn(requests.get, url3)
gevent.joinall([g1, g2, g3])
print([g.value for g in [g1, g2, g3]])
평범한 멀티스레딩 코드처럼
쓸 수 있게 해주죠.
139. g1 = gevent.spawn(requests.get, url1)
g2 = gevent.spawn(requests.get, url2)
g3 = gevent.spawn(requests.get, url3)
gevent.joinall([g1, g2, g3])
print([g.value for g in [g1, g2, g3]])
특정한 일을 하는
스레드를 띄우고
140. g1 = gevent.spawn(requests.get, url1)
g2 = gevent.spawn(requests.get, url2)
g3 = gevent.spawn(requests.get, url3)
gevent.joinall([g1, g2, g3])
print([g.value for g in [g1, g2, g3]])
다른 스레드가 일을 다 할 때까지 기다려서
동기화 하는 식이에요.
141. g1 = gevent.spawn(requests.get, url1)
g2 = gevent.spawn(requests.get, url2)
g3 = gevent.spawn(requests.get, url3)
gevent.joinall([g1, g2, g3])
print([g.value for g in [g1, g2, g3]])
평범하죠?
178. @asyncio.coroutine
def main():
f1 = aiohttp.get(url1)
f2 = aiohttp.get(url2)
f3 = aiohttp.get(url3)
for fut in [f1, f2, f3]:
yield from fut
print([fut.result() for fut in [f1, f2, f3]])
파이썬의 언어 기능인 제너레이터를 이용해서
명시적인 코루틴을 구현하고 있어요.
179. @asyncio.coroutine
def main():
f1 = aiohttp.get(url1)
f2 = aiohttp.get(url2)
f3 = aiohttp.get(url3)
for fut in [f1, f2, f3]:
yield from fut
print([fut.result() for fut in [f1, f2, f3]])
gevent와 다르게 코드만 봐도
어디서 봉쇄가 발생할지 명확하게 보이죠.
180. async def main():
f1 = aiohttp.get(url1)
f2 = aiohttp.get(url2)
f3 = aiohttp.get(url3)
for fut in [f1, f2, f3]:
await fut
print([fut.result() for fut in [f1, f2, f3]])
파이썬 3.5에서는 아예 C#처럼
async와 await 키워드까지 추가돼서
181. async def main():
f1 = aiohttp.get(url1)
f2 = aiohttp.get(url2)
f3 = aiohttp.get(url3)
for fut in [f1, f2, f3]:
await fut
print([fut.result() for fut in [f1, f2, f3]])
더 편하게 쓸 수 있게 됐어요.
182. async def main():
f1 = aiohttp.get(url1)
f2 = aiohttp.get(url2)
f3 = aiohttp.get(url3)
for fut in [f1, f2, f3]:
await fut
print([fut.result() for fut in [f1, f2, f3]])
다만 asyncio를 쓰면
183. async def main():
f1 = aiohttp.get(url1)
f2 = aiohttp.get(url2)
f3 = aiohttp.get(url3)
for fut in [f1, f2, f3]:
await fut
print([fut.result() for fut in [f1, f2, f3]])
asyncio에 맞춰진 라이브러리만
써야해서 선택의 폭이 좁아지긴 해요.
215. def wsgi_app(environ, start_response):
headers = [('Content-Type', 'text/plain')]
start_response('200 OK', headers)
return ['Hello, worldn']
이 인터페이스에만 맞추면
파이썬의 거의 모든 웹 도구를 같이 쓸 수 있어요.
240. ❯ python cli.py -n "Heungsub Lee"
Hi Heungsub Lee!
❯ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
-n, --name TEXT Your name.
--help Show this message and exit.
돌려보면 이렇게 동작합니다.
241. ❯ python cli.py -n "Heungsub Lee"
Hi Heungsub Lee!
❯ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
-n, --name TEXT Your name.
--help Show this message and exit.
보시다시피 리눅스 CLI를
상식적으로 잘 구현하고 있다는 점에서
242. ❯ python cli.py -n "Heungsub Lee"
Hi Heungsub Lee!
❯ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
-n, --name TEXT Your name.
--help Show this message and exit.
애용하는 프레임워크예요.
243. ❯ python cli.py -n "Heungsub Lee"
Hi Heungsub Lee!
❯ python cli.py --help
Usage: cli.py [OPTIONS]
Options:
-n, --name TEXT Your name.
--help Show this message and exit.
상식과 미묘하게 다르게 동작하는
프레임워크도 여럿 봐왔거든요.
275. ❯ pytest test.py
========= FAILURES =========
E assert 24 == 42
E + where 24 = answer()
def test_answer():
assert answer() == 42
테스트가 실패했을 때 왜 실패했는지
단서까지 함께 보고받을 수 있어요.
276. ❯ pytest test.py
========= FAILURES =========
E assert 24 == 42
E + where 24 = answer()
def test_answer():
assert answer() == 42
여기 보시면 answer의 결과값이
42가 아니라 24였다는 걸 알 수 있죠.
279. assertEqual(a, b)
assertNotEqual(a, b)
assertIs(a, b)
assertIn(a, b)
assertGreater(a, b)
unittest
assert a == b
assert a != b
assert a is b
assert a in b
assert a > b
pytest
이런 식으로 비교연산 별로
메소드가 따로 마련돼있죠.
281. assertEqual(a, b)
assertNotEqual(a, b)
assertIs(a, b)
assertIn(a, b)
assertGreater(a, b)
unittest
assert a == b
assert a != b
assert a is b
assert a in b
assert a > b
pytest
사실 처음에 제가 pytest를
쓰기 시작했던 이유는 이 점이 전부였는데
282. assertEqual(a, b)
assertNotEqual(a, b)
assertIs(a, b)
assertIn(a, b)
assertGreater(a, b)
unittest
assert a == b
assert a != b
assert a is b
assert a in b
assert a > b
pytest
쓰다 보니까 테스트 만드는 수고를 덜어주는
편의 기능도 많다는 걸 알게 됐어요.
285. @pytest.fixture
def db(request):
db = Database()
request.addfinalizer(db.close)
return db
def test_db(db):
db.insert('answer', 42)
assert db.get('answer') == 42
“픽스처”란 테스트에서 쓰이는
상태가 일관적인 재료를 말하는데
286. @pytest.fixture
def db(request):
db = Database()
request.addfinalizer(db.close)
return db
def test_db(db):
db.insert('answer', 42)
assert db.get('answer') == 42
이렇게 데코레이터로 픽스처를 정의해두면
매개변수 이름을 이용해서
287. @pytest.fixture
def db(request):
db = Database()
request.addfinalizer(db.close)
return db
def test_db(db):
db.insert('answer', 42)
assert db.get('answer') == 42
어느 테스트 케이스에서든
쉽게 끌어다 쓸 수 있게 해줘요.
343. ❯ flake8 bad.py
bad.py:1:1: F401 'sys' imported but unused
bad.py:2:1: F401 'os' imported but unused
bad.py:7:6: E111 indentation is not a multiple of four
이런 식으로 어떤 코드가 컨벤션에 안 맞는지
아니면 잘못 쓰였는지
344. ❯ flake8 bad.py
bad.py:1:1: F401 'sys' imported but unused
bad.py:2:1: F401 'os' imported but unused
bad.py:7:6: E111 indentation is not a multiple of four
코드를 읽어서 정적으로 분석해주는
단순한 Lint인데
345. ❯ flake8 bad.py
bad.py:1:1: F401 'sys' imported but unused
bad.py:2:1: F401 'os' imported but unused
bad.py:7:6: E111 indentation is not a multiple of four
이걸 작업자 각자가
에디터에 연동해서 쓰기도 하고
346. ❯ flake8 bad.py
bad.py:1:1: F401 'sys' imported but unused
bad.py:2:1: F401 'os' imported but unused
bad.py:7:6: E111 indentation is not a multiple of four
CI에서 빌드 돌릴 때
같이 돌리기도 합니다.
374. ❯ python
Python 2.7.11+ (default, Apr 17 2016, 14:00:29)
[GCC 5.3.1 20160413] on linux2
Type "help", "copyright", "credits" or "license"
for more information.
>>> import os
>>> os.getpid()
42
파이썬 기본 REPL은 다들 아실 테고
375. ❯ ipython
Python 2.7.11+ (default, Apr 17 2016, 14:00:29)
Type "copyright", "credits" or "license" for
more information.
In [1]: import os
In [2]: os.getpid()
Out[2]: 42
IPython이라는 조금 더 스마트한
REPL도 있는데
376. ❯ ipython
Python 2.7.11+ (default, Apr 17 2016, 14:00:29)
Type "copyright", "credits" or "license" for
more information.
In [1]: import os
In [2]: os.getpid()
Out[2]: 42
문법강조도 되고 자동완성도 되고
그 밖에도 다양한 편의기능이 들어있어요.
377. ❯ ipython
Python 2.7.11+ (default, Apr 17 2016, 14:00:29)
Type "copyright", "credits" or "license" for
more information.
In [1]: import os
In [2]: os.getpid()
Out[2]: 42
이 IPython을 터미널 대신
웹에서도 쓸 수 있게 나온 게
383. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
저희가 만드는 게 MMORPG라서 그런지
384. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
간단한 게임플레이 테스트 시나리오
하나 짜려고 해도
385. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
그 시나리오에 필요한
게임 세계를 구축하는데
386. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
굉장히 깊은 의존성을
풀어야 하는 경우가 있어요.
387. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
이 코드는 읽으라고 쓴 게 아니에요.
388. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
여기서 테스트하고 싶은 건
389. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
그저 플레이어를 호수 근처로
옮기는 것 뿐인데
390. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
플레이어와 호수를 준비하려면
391. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
앞쪽에 훨씬 복잡하고 많은 코드가
필요할 수 있다는 걸 보여드리고 싶었어요.
392. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
게임서버에서는 게임 세계가
알아서 구축되잖아요?
393. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
만약 서버를 직접 돌리는 방식으로만
테스트하게 되면
394. >>> world_id = db.find_all(World.id)[0]
>>> world = db.load(World, world_id)
>>> islands = world.all_islands()
>>> for island in islands:
... if 'Python' in island.name:
... break
... else:
... assert False, 'island not found'
...
>>> lakes = island.find_lakes()
>>> lake = lakes[42]
>>> p = Player(id=1)
>>> p.level = 0
>>> for item in INITIAL_ITEMS:
... p.inventory.add_item(item)
...
>>> homes = island.find_homes()
>>> p.home = homes[999]
>>> p.move_to(lake)
Player-1 moved to lake-42!
이런 부분이 고달플 수 있다는 걸
잘 모르고 넘어갈 수 있어요.
395. >>> lake = find_lake('*/42')
>>> p = make_player(1, mock=True)
>>> p.move_to(lake)
Player-1 moved to lake-42!
>>> p.is_nearby(lake)
True
하지만 REPL을 주로 쓰면
자연스레 귀찮고 반복적인 의존성 풀기를
396. >>> lake = find_lake('*/42')
>>> p = make_player(1, mock=True)
>>> p.move_to(lake)
Player-1 moved to lake-42!
>>> p.is_nearby(lake)
True
최대한 쉽게 쓸 수 있게끔
추상화하게 되곤 합니다.
397. >>> lake = find_lake('*/42')
>>> p = make_player(1, mock=True)
>>> p.move_to(lake)
Player-1 moved to lake-42!
>>> p.is_nearby(lake)
True
그래야 REPL에서 다시 쓰기 편하니까요.
398. >>> lake = find_lake('*/42')
>>> p = make_player(1, mock=True)
>>> p.move_to(lake)
Player-1 moved to lake-42!
>>> p.is_nearby(lake)
True
이런 식으로 REPL에서
한 번 돌려본 명령어들을
399. def test_move_to():
lake = find_lake('*/42')
p = make_player(1, mock=True)
p.move_to(lake)
assert p.is_nearby(lake)
복붙해서 테스트케이스로 구워 두면
두고두고 도움 될 거예요.
400. def test_move_to():
lake = find_lake('*/42')
p = make_player(1, mock=True)
p.move_to(lake)
assert p.is_nearby(lake)
이렇듯 우리가 만드는 프로그램을
유저로서 테스트하는 데에 그치지 말고
401. def test_move_to():
lake = find_lake('*/42')
p = make_player(1, mock=True)
p.move_to(lake)
assert p.is_nearby(lake)
REPL을 주요 개발 도구 중 하나로 삼아서
402. def test_move_to():
lake = find_lake('*/42')
p = make_player(1, mock=True)
p.move_to(lake)
assert p.is_nearby(lake)
API를 직접,
그리고 또 자주 써보는 게
403. def test_move_to():
lake = find_lake('*/42')
p = make_player(1, mock=True)
p.move_to(lake)
assert p.is_nearby(lake)
API를 더 낫게 만드는 데
도움된다고 생각합니다.
413. > helloworld.py(7)hello_world()
-> return hello
(Pdb) hello
'world'
(Pdb) l
4 def hello_world():
5 hello = 'world'
6 pdb.set_trace()
7 -> return hello
8 hello_world()
[EOF]
아니면 콜스택 위아래를 오간다거나
한 스텝씩 넘겨본다거나 할 수 있어요.
414. > helloworld.py(7)hello_world()
-> return hello
(Pdb++) hello
'world'
(Pdb++) l
4 def hello_world():
5 hello = 'world'
6 pdb.set_trace()
7 -> return hello
8 hello_world()
[EOF]
pdb++ 확장을 깔면
문법강조도 볼 수 있죠.
415. >>> x = 0
>>> x / x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: ...
>>> pdb.pm()
> <stdin>(1)<module>()
(Pdb) x
0
Pdb는 REPL에서 놀다가
에러 났을 때도 유용하게 쓸 수 있어요.
416. >>> x = 0
>>> x / x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: ...
>>> pdb.pm()
> <stdin>(1)<module>()
(Pdb) x
0
pdb.pm(), “부검”을 뜻하는
“Post-mortem”의 약자죠.
417. >>> x = 0
>>> x / x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: ...
>>> pdb.pm()
> <stdin>(1)<module>()
(Pdb) x
0
이것을 실행하면 마지막 에러가 발생했던 곳을
바로 디버깅할 수 있어요.
418. >>> x = 0
>>> x / x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: ...
>>> pdb.pm()
> <stdin>(1)<module>()
(Pdb) x
0
저희 서버도 디버깅 용으로 돌리면
서버 실행 도중 pdb.pm()으로
419. >>> x = 0
>>> x / x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: ...
>>> pdb.pm()
> <stdin>(1)<module>()
(Pdb) x
0
마지막 에러를 디버깅해볼 수 있게
연동해놨어요.
420. >>> x = 0
>>> x / x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: ...
>>> pdb.pm()
> <stdin>(1)<module>()
(Pdb) x
0
다만 디버깅 콘솔 사용법이
처음엔 조금 생소할 수 있는데
421. (Pdb) ?
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pp run unt
a c continue exit l q s until
alias cl d h list quit step up
args clear debug help n r tbreak w
b commands disable ignore next restart u whatis
break condition down j p return unalias where
Miscellaneous help topics:
==========================
exec pdb
물음표 치면 설명서가 나오니까
422. (Pdb) ?
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pp run unt
a c continue exit l q s until
alias cl d h list quit step up
args clear debug help n r tbreak w
b commands disable ignore next restart u whatis
break condition down j p return unalias where
Miscellaneous help topics:
==========================
exec pdb
조금씩 찾아보면서 익히다 보면
아마 금방 익숙해질 수 있을 거예요.
439. stack = []
while f:
stack.append(f)
f = f.f_back
stack.reverse()
stack = deque()
while f:
stack.appendleft(f)
이런 식으로 list에 순차적으로 쌓은 다음
뒤집는 경우가 있는데
440. stack = []
while f:
stack.append(f)
f = f.f_back
stack.reverse()
stack = deque()
while f:
stack.appendleft(f)
deque을 쓰면 처음부터
역순으로 쌓을 수 있어요.
441. >>> sub = OrderedDict([('S', 1), ('U', 2), ('B', 3)])
>>> sub['S']
1
>>> sub.keys()
['S', 'U', 'B']
OrderedDict는 dict의 서브클래스인데
그냥 dict와 달리 순서를 보장해줘서
442. >>> sub = OrderedDict([('S', 1), ('U', 2), ('B', 3)])
>>> sub['S']
1
>>> sub.keys()
['S', 'U', 'B']
순서도 중요하고
키로 접근하는 것도 중요할 때 쓰면 좋아요.
456. foo = [1, 10, 100, 1000]
x = bisect_left(foo, 42)
foo.insert(x, 42)
from sortedcontainer import SortedList
bar = SortedList([1, 10, 100, 1000])
bar.add(42)
sortedcontainers는
항상 정렬돼있는 list와 dict를 제공해서
457. foo = [1, 10, 100, 1000]
x = bisect_left(foo, 42)
foo.insert(x, 42)
from sortedcontainer import SortedList
bar = SortedList([1, 10, 100, 1000])
bar.add(42)
굉장히 빠르게 원소를 정렬된 상태로
넣거나 빼는 데 쓸 수 있어요.
458. foo = [1, 10, 100, 1000]
x = bisect_left(foo, 42)
foo.insert(x, 42)
from sortedcontainer import SortedList
bar = SortedList([1, 10, 100, 1000])
bar.add(42)
표준 list나 set으로는
할 수 없는 일이죠.
460. >>> from bidict import bidict
>>> type_table = bidict()
>>> type_table[1] = Ping
>>> type_table[2] = Pong
>>> type_table.inv[Pong]
2
키로 값을 찾는 것 뿐만 아니라
값으로 키를 찾는 것까지 가능해서
461. >>> from bidict import bidict
>>> type_table = bidict()
>>> type_table[1] = Ping
>>> type_table[2] = Pong
>>> type_table.inv[Pong]
2
타입코드 테이블 같은 거
만들 때 유용해요.
486. ❯ python -m timeit
-s 'from exm import only_odds' '
seq = xrange(1000000)
seq = only_odds(seq)
seq = (x ** 2 for x in seq)
list(seq)
'
10 loops, best of 3: 137 msec per loop
간단히 벤치마킹해보니까
제너레이터를 쓸 땐 137ms가 걸렸어요.
487. ❯ python -m timeit
-s 'from exm import only_odds' '
seq = list(xrange(1000000))
seq = list(only_odds(seq))
seq = [x ** 2 for x in seq]
'
10 loops, best of 3: 456 msec per loop
반면 매번 이터레이션하는 방식으로는
456ms나 걸리는 걸 볼 수 있었습니다.
488. >>> list(gen)
[1, 2, 3]
>>> list(gen)
[]
def gen():
yield 1
yield 2
yield 3
제너레이터는 딱 한 바퀴만
돌릴 수 있어서
489. >>> list(gen)
[1, 2, 3]
>>> list(gen)
[]
def gen():
yield 1
yield 2
yield 3
여러 번 돌려야 하는 경우에
당황스러울 수 있어요.
491. gen1, gen2 = itertools.tee(gen)
do_something(gen1)
do_another(gen2)
아니면 표준 라이브러리
itertools에 있는 tee를 이용해서
492. gen1, gen2 = itertools.tee(gen)
do_something(gen1)
do_another(gen2)
제너레이터를 여러 개의 뷰로
나눠서 쓸 수 있어요.
493. gen1, gen2 = itertools.tee(gen)
do_something(gen1)
do_another(gen2)
tee는 리눅스 커맨드에서 따온 이름인데
T자형 파이프를 뜻합니다.
494. enemy = player.nearest_player()
while player.alive() or enemy.alive():
player.move_to(enemy)
player.attack(enemy)
yield
제너레이터는 AI 만들 때
쓰기에도 굉장히 편리한데
495. enemy = player.nearest_player()
while player.alive() or enemy.alive():
player.move_to(enemy)
player.attack(enemy)
yield
지금이 어떤 상태고
각종 정보를 다음 틱에 어떻게 넘길지
496. enemy = player.nearest_player()
while player.alive() or enemy.alive():
player.move_to(enemy)
player.attack(enemy)
yield
고민할 필요 없이
그냥 지역변수를 쓰면 되거든요.
497. if self.state is not Fighting:
self.enemy = player.nearest_player()
self.state = Fighting
elif player.alive() or self.enemy.alive():
player.move_to(self.enemy)
player.attack(self.enemy)
제너레이터가 없다면
어떤 형태로든 스테이트머신이 필요하겠죠?
505. ❯ python -m timeit
-s 한 번 실행할 코드 벤치마킹할 코드
timeit은 표준 라이브러리예요.
506. ❯ python -m timeit
-s 한 번 실행할 코드 벤치마킹할 코드
앞에서 제너레이터 벤치마킹할 때
이것을 썼었죠.
507. ❯ python -m timeit
-s 한 번 실행할 코드 벤치마킹할 코드
그냥 쉘에다가 이렇게 치면
508. ❯ python -m timeit
-s 한 번 실행할 코드 벤치마킹할 코드
적당히 너무 오래 걸리지 않는 선에서
반복실행한 다음
509. ❯ python -m timeit
-s 한 번 실행할 코드 벤치마킹할 코드
한 번 돌릴 때 실행시간이
얼마나 걸리는지 알려줍니다.
510. ❯ python -m timeit
-s 'import math' 'math.log10(99999999)'
10000000 loops, best of 3: 0.108 usec per loop
이런 식으로요.
511. ❯ python -m timeit
-s 'import math' 'math.log10(99999999)'
10000000 loops, best of 3: 0.108 usec per loop
이건 math.log10()을
벤치마킹하는 모습이에요.
512. ❯ python -m timeit
-s 'import math' 'math.log10(99999999)'
10000000 loops, best of 3: 0.108 usec per loop
여기서 math 모듈을 import하는 부분은
벤치마킹 대상이 아니잖아요?
513. ❯ python -m timeit
-s 'import math' 'math.log10(99999999)'
10000000 loops, best of 3: 0.108 usec per loop
그래서 -s 옵션에 넣어서
한 번만 부르게 했어요.
514. ❯ python -m timeit
-s 'import math' 'math.log10(99999999)'
10000000 loops, best of 3: 0.108 usec per loop
1,000만 번 씩 3번 돌려봤고
515. ❯ python -m timeit
-s 'import math' 'math.log10(99999999)'
10000000 loops, best of 3: 0.108 usec per loop
그 중 가장 빨랐을 때 log10 한 번에
0.108μs가 걸렸다는 내용입니다.
516. •호출 횟수
•고유 실행시간
•하위 콜스택 포함 실행시간
❯ python -m profile 파이썬 파일
profile 역시 표준 라이브러리인데
517. •호출 횟수
•고유 실행시간
•하위 콜스택 포함 실행시간
❯ python -m profile 파이썬 파일
함수에 진입하고 나가는 이벤트를
모두 기록해서
518. •호출 횟수
•고유 실행시간
•하위 콜스택 포함 실행시간
❯ python -m profile 파이썬 파일
함수 별로 호출 횟수와
실행시간을 측정해주죠.
519. •호출 횟수
•고유 실행시간
•하위 콜스택 포함 실행시간
❯ python -m cProfile 파이썬 파일
표준 라이브러리에 cProfile이라고
C로 만들어서 오버헤드가 적은 버전도 있으니까
520. •호출 횟수
•고유 실행시간
•하위 콜스택 포함 실행시간
❯ python -m cProfile 파이썬 파일
저희처럼 PyPy가 아닌
CPython을 쓰신다면 이걸 쓰세요.
521. ❯ python -m cProfile log10.py
10000002 function calls in 2.269 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.327 0.327 2.269 2.269 log10.py:1(<module>)
10000000 1.942 0.000 1.942 0.000 {math.log10}
측정 결과는
이런 식으로 출력되는데