본문 바로가기

Python/자료구조 (Data Structure)

6-2. 좋은 습관, 단위 테스트: 파이썬 자료구조와 알고리즘

좋은 습관

가상환경

프로젝트가 많아질수록 다양한 버전의 파이썬이나 라이브러리로 작업하는 일이 생긴다. 별도의 파이썬 가상 환경을 만들기 위한 라이브러리는 많다. virtualenv 와 virtualenvwrapper로 파이썬 가상 환경을 간단하게 만들어보자.

virtualenv & virtualenvwrapper

virtualenv 는 파이썬 프로젝트에 필요한 패키지를 사용하기 위해 필요한 모든 실행파일을 포함하는 폴더를 생성한다.

virtualenvwrapper 는 virtualenv를 사용하여 모든 가상 환경을 한곳에 배치한다.

디버깅

파이썬 디버거, pdb 를 사용하면 디버깅을 할 수 있다. 파이썬 스크립트 파일을 대화식 인터프리터에 사용해 살펴보고 싶다면, -i 뒤 또는 -m pdb 뒤에 파일명을 적어서 실행하면 된다.

PS C:\Users\bumsu\Desktop\Python\DSA> python -i threading_with_queue.py
스레드 4: 처리 완료 0
스레드 1: 처리 완료 1
스레드 1: 처리 완료 6
스레드 4: 처리 완료 5
스레드 2: 처리 완료 3
스레드 1: 처리 완료 7
스레드 4: 처리 완료 8
스레드 2: 처리 완료 9
스레드 2: 처리 완료 12
스레드 4: 처리 완료 11
스레드 3: 처리 완료 4
스레드 2: 처리 완료 13
스레드 4: 처리 완료 14
스레드 3: 처리 완료 15
스레드 2: 처리 완료 16
스레드 4: 처리 완료 17
스레드 3: 처리 완료 18
스레드 2: 처리 완료 19
스레드 1: 처리 완료 10
스레드 5: 처리 완료 2
>>> q
<queue.Queue object at 0x0000029FF22D38E0>
>>> num_worker_threads
5
PS C:\Users\bumsu\Desktop\Python\DSA> python -m pdb threading_with_queue.py
> c:\users\bumsu\desktop\python\dsa\threading_with_queue.py(1)<module>()
-> import queue
(Pdb) help

Documented commands (type help <topic>):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt      
alias  clear      disable  ignore    longlist  r        source   until
args   commands   display  interact  n         restart  step     up
b      condition  down     j         next      return   tbreak   w
break  cont       enable   jump      p         retval   u        whatis
bt     continue   exit     l         pp        run      unalias  where

Miscellaneous help topics:
==========================
exec  pdb

(Pdb) help n 
n(ext)
        Continue execution until the next line in the current function
        is reached or it returns.
(Pdb) n
> c:\users\bumsu\desktop\python\dsa\threading_with_queue.py(2)<module>()
-> import threading
(Pdb) n
> c:\users\bumsu\desktop\python\dsa\threading_with_queue.py(4)<module>()
-> q = queue.Queue()
(Pdb) n
> c:\users\bumsu\desktop\python\dsa\threading_with_queue.py(6)<module>()
-> def worker(num):
...

pdb의 명령어는 다음과 같다.

  • c (continue): 프로그램을 끝까지 실행
  • s (step): 코드 다음줄로 (한단계씩 코드 실행)
  • n (next): 코드 다음줄로 넘어가되 프로시저 단위 실행 (step over), s와 다른 점은 어떤 함수를 만날 경우 함수 전체를 실행한 뒤 다음 줄로 넘어감.
  • p (point): 표현식의 값을 출력
  • l (line): 다음 실행할 코드를 몇 줄 보기
  • h (help): 도움말

프로파일링

프로그램이 매우 느리게 실행되거나 예상보다 많은 메모리가 소비되는 것은 자료구조나 알고리즘을 잘못 선택했거나 비효율적으로 구현했기 때문인 경우가 많다. 다음과 같은 성능 항목을 검토할 수 있다.

  • 읽기 전용 데이터는 리스트 대신 튜플을 사용
  • 반복문에서 항목이 많은 리스트나 튜플 대신 제너레이터를 사용
  • 문자열을 연결할 때 + 연산자로 문자열을 연결 (concatenate) 대신, 리스트에 문자열을 추가 (append) 한 후, 마지막에 리스트의 항목을 모두 하나로 연결 (join) 한다.

cProfile 모듈

cProfile 모듈은 호출 시간에 대한 세부 분석을 제공하며, 병목 현상 (bottleneck) 을 찾는 데 사용된다. 흔히 다음과 같은 형태로 사용한다.

>>> import cProfile
>>> import time
>>> def sleep():
    time.sleep(5)


>>> def hello_world():
    print("Hello World!")


>>> def main():
    sleep()
    hello_world()


>>> cProfile.run('main()')
Hello World!
         160 function calls in 5.006 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    5.006    5.006 <pyshell#11>:1(main)
        1    0.000    0.000    5.000    5.000 <pyshell#4>:1(sleep)
        1    0.000    0.000    0.006    0.006 <pyshell#7>:1(hello_world)
        1    0.000    0.000    5.006    5.006 <string>:1(<module>)
       21    0.000    0.000    0.000    0.000 rpc.py:153(debug)
        3    0.000    0.000    0.006    0.002 rpc.py:216(remotecall)
        3    0.000    0.000    0.001    0.000 rpc.py:226(asynccall)
        3    0.000    0.000    0.005    0.002 rpc.py:246(asyncreturn)
        3    0.000    0.000    0.000    0.000 rpc.py:252(decoderesponse)
        3    0.000    0.000    0.005    0.002 rpc.py:290(getresponse)
        3    0.000    0.000    0.000    0.000 rpc.py:298(_proxify)
        3    0.000    0.000    0.005    0.002 rpc.py:306(_getresponse)
        3    0.000    0.000    0.000    0.000 rpc.py:328(newseq)
        3    0.000    0.000    0.001    0.000 rpc.py:332(putmessage)
        2    0.000    0.000    0.001    0.001 rpc.py:559(__getattr__)
        3    0.000    0.000    0.000    0.000 rpc.py:57(dumps)
        1    0.000    0.000    0.001    0.001 rpc.py:577(__getmethods)
        2    0.000    0.000    0.000    0.000 rpc.py:601(__init__)
        2    0.000    0.000    0.005    0.002 rpc.py:606(__call__)
        4    0.000    0.000    0.000    0.000 run.py:412(encoding)
        4    0.000    0.000    0.000    0.000 run.py:416(errors)
        2    0.000    0.000    0.006    0.003 run.py:433(write)
        6    0.000    0.000    0.000    0.000 threading.py:1306(current_thread)
        3    0.000    0.000    0.000    0.000 threading.py:222(__init__)
        3    0.000    0.000    0.005    0.002 threading.py:270(wait)
        3    0.000    0.000    0.000    0.000 threading.py:81(RLock)
        3    0.000    0.000    0.000    0.000 {built-in method _struct.pack}
        3    0.000    0.000    0.000    0.000 {built-in method _thread.allocate_lock}
        6    0.000    0.000    0.000    0.000 {built-in method _thread.get_ident}
        1    0.000    0.000    5.006    5.006 {built-in method builtins.exec}
        6    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        9    0.000    0.000    0.000    0.000 {built-in method builtins.len}
        1    0.000    0.000    0.006    0.006 {built-in method builtins.print}
        3    0.000    0.000    0.000    0.000 {built-in method select.select}
        1    5.000    5.000    5.000    5.000 {built-in method time.sleep}
        3    0.000    0.000    0.000    0.000 {method '_acquire_restore' of '_thread.RLock' objects}
        3    0.000    0.000    0.000    0.000 {method '_is_owned' of '_thread.RLock' objects}
        3    0.000    0.000    0.000    0.000 {method '_release_save' of '_thread.RLock' objects}
        3    0.000    0.000    0.000    0.000 {method 'acquire' of '_thread.RLock' objects}
        6    0.005    0.001    0.005    0.001 {method 'acquire' of '_thread.lock' objects}
        3    0.000    0.000    0.000    0.000 {method 'append' of 'collections.deque' objects}
        2    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        3    0.000    0.000    0.000    0.000 {method 'dump' of '_pickle.Pickler' objects}
        2    0.000    0.000    0.000    0.000 {method 'encode' of 'str' objects}
        2    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        3    0.000    0.000    0.000    0.000 {method 'getvalue' of '_io.BytesIO' objects}
        3    0.000    0.000    0.000    0.000 {method 'release' of '_thread.RLock' objects}
        3    0.000    0.000    0.000    0.000 {method 'send' of '_socket.socket' objects}

timeit 모듈

코드 일부분의 실행 시간을 확인하는 데 사용한다.

>>> import timeit
>>> timeit.timeit("x = 2 + 2")
0.01502549999997882
>>> timeit.timeit("x = sum(range(10))")
0.4615995000000055

단위 테스트

개별 함수 및 클래스의 메서드에 대한 테스트 코드를 작성하여 예상한 값이 맞게 나오는지 확인하는 것은 좋은 습관이다. 파이썬 표준 라이브러리는 이러한 단위 테스트 (unit test) 를 위해 doctest 와 unittest 모듈을 제공한다. 또한 외부 라이브러리인 pytest도 있다.

용어

  • 테스트 픽스쳐 (test fixture): 테스트 설정을 위한 코드
  • 테스트 케이스 (test case): 테스트의 기본 단위
  • 테스트 스위트 (test suite): unittest.TestCase 의 하위 클래스에 의해 생성된 테스트 케이스 집합. 각 테스트 케이스의 메서드 이름은 test로 시작한ㄷ.ㅏ
  • 테스트 러너 (test runner): 하나 이상의 테스트 스위트를 실행하는 객체

doctest & unittest

먼저 doctest 모듈은 모듈과 함수의 독스트링 (doctring) 안에 테스트 코드를 작성할 때 사용한다. unittest와 함께 사용할 수도 있다.

pytest

외부 라이브러리인 pytest는 사용법이 매우 쉽다. test로 시작하는 파일에서 test로 시작하는 함수를 작성하기만 하면 된다.

출처

파이썬 자료구조와 알고리즘 - 한빛미디어, 미아 스타인