6.파이썬 고급주제

멀티프로세스와 멀티 스레드

좋은 습관

단위테스트

6.1.1 subprocess 모듈

import subprocess


subprocess.run(["echo","이것은 subprocess입니다."])
subprocess.run(["sleep","10"])
root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject# python3.6 0_sub_pro.py
이것은 subprocess입니다.

6.1.2 threading 모듈

스레드가 여러 개로 분리되면, 스레드 간 데이터 공유의 복잡성이 증가한다. 또한 락(lock)과 데드락(deadlock)을 회피하는 데 주의를 기울여야 한다. 파이썬 프로그램에는 단 하나의 메인 스레드만 존재한다. 멀티 스레드를 사용하려면 threading 모듈을 사용한다.

내부적으로 락을 관리하려면 queue 모듈을 사용한다. 큐에 의존하면 자원의 접근을 직렬화할 수 있고, 이는 곧 한 번에 하나의 스레드만 데이터에 접근할 수 있게 한다는 뜻이다(FIFO first in, first out 방식으로). 실행 중인 스레드가 있는 동안에는 프로그램은 종료되지 않는다.

워커 스레드(worker thread)가 작업을 완료했는데도, 프로그램이 종료되지 않고 계속 실행되는 경우 문제가 될 수 있다. 스레드를 데몬(daemon)으로 변환하면 데몬 스레드가 실행되지 않는 즉시 프로그램이 종료된다. queue.join() 메서드는 큐가 빌 때까지 (큐의 모든 항목이 처리될 때까지) 기다린다. queue 모듈의 공식 문서 예쩨를 조금 수정한 다음 코드를 살펴보자.

import queue
import threading

q = queue.Queue()

def worker(num):
    while True:
        item = q.get()
        if item is None:
            break
            # 작업을 처리한다. 
        print("스레드 {0} : 처리 완료 {1}".format(num+1, item))
        q.task_done()
if __name__ == "__main__":
    num_worker_threads = 5
    threads  = []
    for i in range(num_worker_threads):
        t = threading.Thread(target=worker, args=(i,))
        t.start()
        threads.append(t)
    for item in range(20):
        q.put(item)
    
    #모든 작업이 끝날 떄까지 기다린다.(block).
    q.join()

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\1_threading_with_queue.py
스레드 1 : 처리 완료 0
스레드 2 : 처리 완료 1
스레드 5 : 처리 완료 3
스레드 1 : 처리 완료 5
스레드 1 : 처리 완료 8
스레드 5 : 처리 완료 7
스레드 2 : 처리 완료 6
스레드 1 : 처리 완료 9
스레드 5 : 처리 완료 10
스레드 5 : 처리 완료 13
스레드 1 : 처리 완료 12
스레드 2 : 처리 완료 11
스레드 5 : 처리 완료 14
스레드 1 : 처리 완료 15
스레드 1 : 처리 완료 18
스레드 1 : 처리 완료 19
스레드 3 : 처리 완료 4
스레드 5 : 처리 완료 17
스레드 2 : 처리 완료 16
스레드 4 : 처리 완료 2

6.1.3 뮤텍스와 세마포어

**뮤텍스**는 락과 같다. 뮤텍스는 공유 리소스에 한 번에 하나의 스레드만 접근 할 수 있도록 하는 상호 배제(mutual exclusion) 동시성 제어정책을 강제하기 위해 설계되었다. 예를 들어 한 스레드가 배열을 수정하고 있다고 가정해보자. 배열 작업을 절반 이상 수행했을 떄, 프로세서가 다른 스레드로 전환했다고 하자, 여기에서 뮤텍스를 사용하지 않는다면, 두 스레드가 동시에 배열을 수정하는 일이 벌어질 것이다. 개념적으로. 뮤텍스는 1부터 시작하는 정수다. 스레드는 배열을 변경해야 할 때마다 뮤텍스를 '잠근다.' 즉, 스레드는 뮤텍스가 양수가 될 때까지 대기한 다음 숫자를 1 감소시킨다.(이것이 곧 락이다.) 배열 수정을 마치면 뮤텍스가 잠금 해제되어 숫자가 1 증가한다.(언락). 배열을 수정하기 전에 뮤텍스를 잠근 후 수정작업이 끝나고 잠금을 해제하면, 두 스레드가 배열을 동시에 수정하는 일은 일어나지 않는다. 다음 뮤텍스 예제를 살펴보자. 예제파일을 다운로드 했다면 thread_safe 변수가 True로 작성되어 있을 텐데, 비교를 위해 다음과 같이 False로 지정하여 실행해보자.

from threading import Thread, Lock
import threading

def worker(mutex, data, thread_safe):
    if thread_safe:
        mutex.acquire()
    try:
        print("스레드 {0}: {1}\n".format(threading.get_ident(),data))
    finally:
        if thread_safe:
            mutex.release()

if __name__ == "__main__":
    threads = []
    thread_safe = False
    mutex =Lock()
    for i in range(20):
        t = Thread(target=worker, args=(mutex, i,thread_safe))
        t.start()
        threads.append(t)
    for i in threads:
        t.join()
        

실행할 때마다 결과가 다르게 나올 것이다. 이제 뮤텍스를 사용하기 위해 thread_safe 변수를 True로 설정한 후 다시 코드를 실행해보자.

한편, **세마포어**는 뮤텍스보다 더 일반적으로 사용되는 개념이다. 세마포어는 1보다 큰 수로 시작할 수 있다. 세마포어 값은 곧 한 번에 자원에 접근할 수 있는 스레드의 수다. 세마포어는 뮤텍스의 락 및 언락 작업과 유사한 대기(wait) 및 신호(signal) 작업을 지원한다. 파이썬의 뮤텍스(락)와 세마포어에 관한 내용은 threading 모듈의 공식 문서를 참조한다. 다음 세마포어 예제를 살펴보자.

import threading
import time

class ThreadPool(object):
    def __init__(self):
        self.active = []
        self.lock = threading.Lock()

    def acquire(self,name):
        with self.lock:
            self.active.append(name)
            print("획득: {0} | 스레드 풀: {1}".format(name,self.active))
    
    def release(self,name):
        with self.lock:
            self.active.remove(name)
            print("반환: {0} | 스레드 풀: {1}".format(name,self.active))
    
def worker(semaphore,pool):
    with semaphore:
        name = threading.currentThread().getName()
        pool.acquire(name)
        time.sleep(1)
        pool.release(name)

if __name__ == "__main__":
    threads=[]
    pool = ThreadPool()
    semaphore = threading.Semaphore(3)
    for i in range(10):
        t = threading.Thread(target=worker, name="스레드 "+ str(i), args=(semaphore,pool))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\3_threading_semaphore.py
획득: 스레드 0 | 스레드 풀: ['스레드 0']
획득: 스레드 1 | 스레드 풀: ['스레드 0', '스레드 1']
획득: 스레드 2 | 스레드 풀: ['스레드 0', '스레드 1', '스레드 2']
반환: 스레드 2 | 스레드 풀: ['스레드 0', '스레드 1']
반환: 스레드 1 | 스레드 풀: ['스레드 0']
반환: 스레드 0 | 스레드 풀: []
획득: 스레드 3 | 스레드 풀: ['스레드 3']
획득: 스레드 4 | 스레드 풀: ['스레드 3', '스레드 4']
획득: 스레드 5 | 스레드 풀: ['스레드 3', '스레드 4', '스레드 5']
반환: 스레드 5 | 스레드 풀: ['스레드 3', '스레드 4']
반환: 스레드 4 | 스레드 풀: ['스레드 3']
반환: 스레드 3 | 스레드 풀: []
획득: 스레드 6 | 스레드 풀: ['스레드 6']
획득: 스레드 7 | 스레드 풀: ['스레드 6', '스레드 7']
획득: 스레드 8 | 스레드 풀: ['스레드 6', '스레드 7', '스레드 8']
반환: 스레드 8 | 스레드 풀: ['스레드 6', '스레드 7']
반환: 스레드 6 | 스레드 풀: ['스레드 7']
반환: 스레드 7 | 스레드 풀: []
획득: 스레드 9 | 스레드 풀: ['스레드 9']
반환: 스레드 9 | 스레드 풀: []

6.1.4 데드락과 스핀락

**데드락**(교착상태)은 두 개 이상의 프로세스나 스레드가 서로 상대방의 작업이 끝나기만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태다. 프로그램에서 락을 할당하고, 락을 순서대로 획득한다면, 교착 상태를 막을 수 있다.(이는 일반적인 접근법일뿐 정교한 것은 아니다.)

다음 네 가지 조건을 모두 충족하면 데드락이 발생한다. 네 가지 조건 중 하나라도 막을 수 있다면, 데드락을 해결할 수 있다.

  • **상호배제**(mutual exclusion): 자원은 한 번에 한 프로세스(혹은 스레드)만 사용할 수 있다.
  • **점유와 대기**(hold and wait) : 한 프로세스가 자원을 가지고 있는 상태에서, 다른 프로세스가 쓰는 자원의 반납을 기다린다.
  • **비선점**(no preemtion) : 다른 프로세스가 이미 점유한 자원을 강제로 뻇어오지 못한다.
  • **순환대기**(circular wait) : 프로세스 A,B,C가 있다고 가정할 떄 A는 B가 점유한 자원을, B는 C가 점유한 자원을, C는 A가 점유한 자원을 대기하는 상태다.

**스핀락**은 (전체 시스템이 단일 애플리케이션 전용이고, 코어당 하나의 스레드만 사용하는 ) 고성능 컴퓨팅 상황에 유용한 바쁜 대기(busy wating )의 한 형태다. 스핀락은 임계 구역에 진입이 불가능할 때, 진입이 가능할 때까지 반복문을 돌면서 재시도하는 방식으로 구현된 락이다.

6.1.5 스레딩에 대한 구글 파이썬 스타일 가이드

내장 타입의 원자성(atomicity)에 의존하지 않는다. 딕셔너리 같은 파이썬 기본 데이터 타입은 원자적 연산을 수행하는 반면, 내장 타입이 원자적이지 않은 경우가 있어서 __hash__() 또는 __eq__() 메서드가 구현된 경우), 내장 타입의 원자성에 의존해선 안 된다. 또한 원자적 변수 할당에 의존하지 않아야한다. (이것은 결국 딕셔너리에 의존하기 때문이다.)

queue 모듈의 Queue 데이터 타입을 스레드 간 데이터를 전달하는 기본 방식으로 사용한다. 그렇지 않으면, threading 모듈의 락을 사용한다. 저수준의 락 대신, threading.Condition을 사용할 수 있도록 조건 변수를 적절하게 사용하는 방법을 숙지한다. 생산자-소비자 모델의 간단한 예제를 살펴보자.

import threading

def consumer(cond):
    name = threading.currentThread().getName()
    print("{0} 시작".format(name))
    with cond:
        print("{0} 대기".format(name))
        cond.wait()
        print("{0} 자원 소비".format(name))

def producer(cond):
    name = threading.currentThread().getName()
    print("{0} 시작".format(name))
    with cond:
        print("{0} 자원 생산 후 모든 소비자에게 알림".format(name))
        cond.notifyAll

if __name__ == "__main__":
    condition = threading.Condition()
    consumer1 = threading.Thread(
        name="소비자1", target=consumer,args=(condition,))
    consumer2 = threading.Thread(
        name="소비자2", target=consumer, args=(condition,))
    producer = threading.Thread(name="생산자", target=producer, args=(condition,))

    consumer1.start()
    consumer2.start()
    producer.start()
    

결과

소비자1 시작
소비자1 대기
소비자2 시작
생산자 시작
소비자2 대기
생산자 자원 생산 후 모든 소비자에게 알림
소비자2 자원 소비
소비자1 자원 소비

-기타

_ = "_"

6.2. 좋은 습관


6.2.1 가상 환경

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

**virtualenv**는 파이썬 프로젝트에 필요한 패키지를 사용하기 위해 필요한 모든 실행파일을 포함하는 폴더를 생성한다. 공식문서(https://docs.python-guide.org/dev/virtualenvs/#lower-level-virtualenv)에서 예제를 볼 수 있다.

#virtualenv를 설치한다.
$pip install virtualenv


#설치된 버전을 확인한다.
$virtualenv --version

#가상 환경 프로젝트를 생성한다.
$cd my_project_folder
$virtualenv my_project

#가상 환경 프로젝트를 활성화한다.
$source my_project/bin/activate

# 파이썬 외부 패키지 모듈을 설치한다. (다음 예제에서는 request 라이브러리를 설치한다.).
$(my_project)pip install requests

#가상 환경에서 설치된 외부 패키지 목록을 확인한다.
$(my_project) pip freeze

# 가상 환경 프로젝트를 비활성화한다.
$(my_project) deactivate

이렇게 설정한 로컬 가상환경을 삭제하려면, 생성한 폴더(my_project)를 삭제하면 된다.

**virtualenvwrapper**는 virtualenv를 사용하여 모든 가상 환경을 한곳에 배치한다.(https://docs.python-guide.org/dev/virtualenvs/#virtualenvwrapper). 윈도우 사용자를 위한 문서도 있으니 참조한다.

#virtualenvwrapper를 설치한다.
$pip install virtualenvwrapper

#가상 환경 폴더를 생성한다.
$export WORKON_HOME=~/Envs
$mkdir -p $WORKON_HOME
$source /usr/local/bin/virtualenvwrapper.sh

#가상 환경 프로젝트를 생성한다.
$mkvirtualenv env1

#requests 라이브러리를 설치한다.
(env1)$pip install requests

#설치된 패키지를 확인한다.
(env1)$ pip freeze

#가상 환경 프로젝트를 활성화한다.
$workon env1

$가상 환경 프로젝트를 비활성화한다.
(env1)$ deactivate

# 가상 환경 프로젝트를 삭제한다.
(env1)$ rmvirtualenv env1

6.2.2 디버깅


파이썬 디버거 **pdb**를 이용하면 디버깅을 할 수 있다.(http://pymotw.com/3/pdb) 자세한 사용법은 파이썬 공식 문서를 참조한다.

파이썬 스크립트 파일을 대화식 인터프리터를 사용해 살펴보고 싶다면 -i 뒤에 파일명을 적거나 -m pdb 뒤에 파일명을 적어서 실행하면 된다. 스크립트에 있는 변수와 함수 등을 사용할 수 있다. 먼저 -i 실행 예이다.

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject# python3.6 -i 1_threading_with_queue.py
스레드 1 : 처리 완료 0
스레드 1 : 처리 완료 5
스레드 4 : 처리 완료 3
스레드 2 : 처리 완료 1
스레드 1 : 처리 완료 6
스레드 4 : 처리 완료 7
스레드 2 : 처리 완료 8
스레드 1 : 처리 완료 9
스레드 4 : 처리 완료 10
스레드 2 : 처리 완료 11
스레드 1 : 처리 완료 12
스레드 4 : 처리 완료 13
스레드 2 : 처리 완료 14
스레드 1 : 처리 완료 15
스레드 4 : 처리 완료 16
스레드 2 : 처리 완료 17
스레드 1 : 처리 완료 18
스레드 4 : 처리 완료 19
스레드 5 : 처리 완료 4
스레드 3 : 처리 완료 2
>>> q
<queue.Queue object at 0x7f7c52c5cda0>
>>> 5

다음은 -m pdb 실행 예이다.

root@DESKTOP-JPGB0S5:/mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject# python3.6 -m pdb 1_threading_with_queue.py
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_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
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(2)<module>()
-> import threading
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(4)<module>()
-> q = queue.Queue()
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(6)<module>()
-> def worker(num):
(Pdb) n
> /mnt/d/Mastering-Python-Design-Patterns-Second-Edition/algo/advanced_subject/1_threading_with_queue.py(14)<module>()
-> if __name__ == "__main__":

pdb의 명령어를 몇 가지 살펴보겠다. c(continue)를 입력하면 프로그램을 끝까지 실행하고, s(step)은 코드 다음 줄로 넘어간다.(한 단계 씩 코드 실행step into). n(next)도 코드 다음줄로 넘어가되 프로시저 단위 실행(step over)으로서, s와 다른 점은 어떤 함수를 만날 경우 함수 전체를 실행한 뒤 다음 줄로 넘어간다는 점이다. p(point)는 표현식의 값을 출력한다. l(line)은 다음 실행할 코드를 몇 줄 보여준다. h(help)는 도움말이다.

스크립트에서 디버깅하고 싶은 위치에 pdb.set_trace() 함수를 삽입하는 방법도 있다.

import pdbpdb.set_trace()

6.2.3 프로파일링

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

  • 읽기 전용 데이터는 리스트 대신 튜플을 사용한다.
  • 반복문에서 항목이 많은 리스트나 튜플 대신 **제너레이터**를 사용하여 순회한다.
  • 문자열을 연결할 떄 + 연산자로 문자열을 연결(concatenate)하는 대신, 리스트에 문자열을 추가(append)한 후, 마지막에 리스트의 항목을 모두 하나로 연결(join)한다. 다음 구글 파이썬 스타일 가이드의 예제 코드를 살펴보자.
#좋은 예items=['<table>']for last_name, first_name in employee_list:    items.append('<tr><td>%s,%s</td></tr>' %(last_name,first_name))    items.append('</table>')    employee_table=''.join(items)# 나쁜 예employee_table = '<table>'for last_name, first_name in employee_list:    employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)    employee_table += '</table>'

cProfile 모듈

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

import cProfilecProfile.run('main()')

조금 더 실제적인 예는 다음과 같다.

import cProfileimport timedef sleep():    time.sleep(5)def hello_world():    print("Hello World!")def main():    sleep()    hello_world()cProfile.run('main()')

실행 결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\test.pyHello World!         8 function calls in 5.014 seconds   Ordered by: standard name   ncalls  tottime  percall  cumtime  percall filename:lineno(function)        1    0.000    0.000    5.014    5.014 <string>:1(<module>)        1    0.000    0.000    5.013    5.013 test.py:17(sleep)        1    0.000    0.000    0.001    0.001 test.py:20(hello_world)        1    0.000    0.000    5.014    5.014 test.py:23(main)        1    0.000    0.000    5.014    5.014 {built-in method builtins.exec}        1    0.001    0.001    0.001    0.001 {built-in method builtins.print}        1    5.013    5.013    5.013    5.013 {built-in method time.sleep}        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

혹은 다음과 같이 스크립트 파일에 대해 실행할 수도 있다.

python -m cProfile -o profile.dat 1_threading_with_queue.pyPS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python -m cProfile -o profile.dat .\1_threading_with_queue.py스레드 2 : 처리 완료 0스레드 1 : 처리 완료 1스레드 2 : 처리 완료 3스레드 5 : 처리 완료 5스레드 5 : 처리 완료 8스레드 2 : 처리 완료 7스레드 2 : 처리 완료 10스레드 4 : 처리 완료 2스레드 4 : 처리 완료 12스레드 4 : 처리 완료 13스레드 4 : 처리 완료 14스레드 2 : 처리 완료 11스레드 1 : 처리 완료 6스레드 5 : 처리 완료 9스레드 2 : 처리 완료 16스레드 2 : 처리 완료 19스레드 5 : 처리 완료 18스레드 4 : 처리 완료 15스레드 3 : 처리 완료 4스레드 1 : 처리 완료 17$python -m pstats profile.dat

이 밖에 프로파일링에 대한 자세한 내용은 파이썬 공식 문서를 참조한다.

timeit 모듈

코드 일부분의 실행 시간을 확인하는 데 사용한다. 다음예제를 살펴보자.

import timeitprint(timeit.timeit("x = 2+2"))print(timeit.timeit("x = sum(range(10))"))

결과

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\test.py0.0251481999999999960.9078216

다음과 같이 스크립트로 실행할수도 있다.

PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python -m timeit "d={}"5000000 loops, best of 5: 49.3 nsec per loopPS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python -m timeit "import collections" "d = collections.OrderedDict()"1000000 loops, best of 5: 371 nsec per loop

time 모듈의 time()함수를 사용한 아주 간단한 예제를 살펴보자.

import time


def sumOfN2(n):
    start = time.time()
    theSum = 0
    for i in range(1,n+1):
        theSum = theSum + i
    end = time.time()
    return theSum, end-start

if __name__ == "__main__":
    n = 5
    print("총 합계: %d\t 시간: %10.7f초" % sumOfN2(n))
    n = 200
    print("총 합계: %d\t 시간: %10.7f초" % sumOfN2(n))
PS D:\Mastering-Python-Design-Patterns-Second-Edition> cd .\algo\advanced_subject\
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject> python .\5_using_time_module.py
총 합계: 15      시간:  0.0000000초
총 합계: 20100   시간:  0.0000000초

6.3. 단위 테스트

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

6.3.1 용어

  • 테스트 픽스처(test fixture) : 테스트 설정을 위한 코드( 예: 테스트용 입력 파일을 만들었다 삭제하는 코드)
  • 테스트 케이스(test case):테스트의 기본 단위
  • 테스트 스위트(test suitte): unittest.TestCase의 하위 클래스에 의해 생성된 테스트 케이스 집합. 각 테스트 케이스의 메서드 이름은 test로 시작한다.
  • 테스트 러너(test runner): 하나 이상의 테스트 스위트를 실행하는 객체

6.3.2 doctest

먼저 **doctest** 모듈은 모듈과 함수의 독스트링(docstring) 안에 테스트 코드를 작성할 때 사용한다. 테스트를 작성한 후 , 다음 코드 세 줄만 추가하면된다.

if __name__ == "__main__"   import doctest   doctest.testmod()

doctest 모듈이 포함된 프로그램은 두 가지 방법으로 실행할 수 있다. 먼저 -v 옵션으로 파이썬을 실행하는 방법이다. 파이썬 공식문서의 예제를 살펴보자.

"""This is the "example" module.the example module supplies one function, factorial(). For example>>> factorial(5)120"""def factorial(n):    """Return the factorial of n, an exact integer >= 0        >>> [factorial(n) for n in range(6)]      [1,1,2,6,24,120]    >>> factorial(30)    >>> factorial(-1)     >>> factorial(30.0)     >>> factorial(1e100)    """    import math    if not n >= 0:        raise ValueError("n must be >=0")    if math.floor(n) != n:        raise ValueError("n must be exact integer")    if n+1 == n: #catch a value like 1e300        raise OverflowError("n too large")    result =1    factor =2    while factor <= n:        result *= factor        factor += 1    return result    if __name__ == "__main__":        import doctest        doctest.testmod()        

다음과 같이 unittest 모듈과 함께 실행할 수도 있다.

>>> import doctest>>> import unittest>>> import doctest_factorial>>>>>> suite unittest.TestSuite()>>> suite.addTest(doctest.DocTestSuite(doctest_factorial))>>> runner = unittest.TextTestRunner()>>> print(runner.run(suite))

6.3.3 pytest

외부 라이브러리인 **pytest**는 사용법이 매우 쉽다. test로 시작하는 파일에서 test로 시작하는 함수를 작성하면 된다. 간단한 예를 살펴보겠다.

먼저 pytest 라이브러리를 설치한다.

$ pip install pytest

다음 코드를 간단하게 테스트해보자.

def func(x):
    return x + 1

def test_answer():
    assert func(3) == 51

    

터미널의 현재 위치에서 다음 명령을 실행하면, 파일명이 test로 시작하는 파이썬 스크립트가 실행된다.

결과

================================================================================= test session starts ===================================================================================
platform win32 -- Python 3.7.4, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Mastering-Python-Design-Patterns-Second-Edition\algo\advanced_subject
collected 1 item / 1 error

========================================================================================= ERRORS =========================================================================================
_____________________________________________________________________________ ERROR collecting study_test.py _____________________________________________________________________________
study_test.py:4: in <module>
    for last_name, first_name in employee_list:
E   NameError: name 'employee_list' is not defined
================================================================================ short test summary info =================================================================================
ERROR study_test.py - NameError: name 'employee_list' is not defined
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

다음과 같이 파일을 지정하여 실행할 수 있다.

$ py.test test_pytest.py

파이썬 디버거 pdb와 같이 실행할 수 있다.

$ py.test --pdb

+ Recent posts