7 분 소요

파이썬 프로그램에 오류가 있는지 아니면 잘 작동하는지 확인하는 가장 간단한 방법은 print() 메서드를 적절히 사용하는 것이다. 그러나 코드 자체가 길거나 실제 상품으로 만들 프로그램의 코드라면 테스트 한 후 바로 지워야 하는데, print()가 곳곳에 있을 경우 이는 번거로운 일이 될 것이다.

그래서 이 장에서는 파이썬 코드를 테스트하는 여러 도구와 방법에 대해서 살펴볼 것이다.

파이썬 코드 검사기 : pylint, pyflakes, pep8

해당 모듈들은 터미널 창에서 pip을 통해 다운로드 받을 수 있다.

pip install pylint
pip install pyflakes
pip install pep8

해당 프로그램들은 코드 내 에러들을 검사해줄 뿐만 아니라, 좀 더 나은 스타일의 코드를 짤 수 있도록 도와준다. 여기서는 pylint에 대해서만 간단히 알아보자.

먼저 테스트해볼 파이썬 코드를 작성해보자. 다음 코드 파일은 study.py라는 이름으로 저장하였다.

a = 1
b = 2
print(a)
print(b)
print(c)

해당 파일을 저장한 후, 터미널 창에서 pylint study.py라고 치면 다음의 결과를 얻을 것이다.

************* Module study
study.py:5:0: C0304: Final newline missing (missing-final-newline)
study.py:1:0: C0114: Missing module docstring (missing-module-docstring)
study.py:1:0: C0103: Constant name "a" doesn't conform to UPPER_CASE naming style (invalid-name)
study.py:2:0: C0103: Constant name "b" doesn't conform to UPPER_CASE naming style (invalid-name)
study.py:5:6: E0602: Undefined variable 'c' (undefined-variable)

-----------------------------------
Your code has been rated at 0.00/10

위의 출력 결과에서, E로 시작하는 코드는 에러를 뜻한다. 위 코드에서는 정의되지도 않은 변수 c를 출력하려고 했으므로 에러가 발생하였다. 이를 고치고 다시 실행해보자.

a = 1
b = 2
c = 3
print(a)
print(b)
print(c)
************* Module study
study.py:6:0: C0304: Final newline missing (missing-final-newline)
study.py:1:0: C0114: Missing module docstring (missing-module-docstring)
study.py:1:0: C0103: Constant name "a" doesn't conform to UPPER_CASE naming style (invalid-name)
study.py:2:0: C0103: Constant name "b" doesn't conform to UPPER_CASE naming style (invalid-name)
study.py:3:0: C0103: Constant name "c" doesn't conform to UPPER_CASE naming style (invalid-name)

------------------------------------------------------------------
Your code has been rated at 1.67/10 (previous run: 0.00/10, +1.67)

에러는 사라진 것을 알 수 있다. 그 외에 뜨는 문구들은 에러까지는 아니지만 지켜주면 좋은 규칙들에 대해서 설명하는 것 같다. 위 출력 결과에서는 맨 마지막 코드에 개행을 해야 하고, 모듈 자체에 docstring을 쓰는 것을 추천하며, 변수 이름을 설정한 방식을 바꿀 것을 추천하는 것 같다. 이들을 고쳐보자.

"모듈에 대한 설명이 들어가는 곳입니다."

def func():
    "해당 함수에 대한 docstring은 이 곳에..."
    one = 1
    two = 2
    three = 3
    print(one)
    print(two)
    print(three)

func()
-------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 1.67/10, +8.33)

출력 결과를 보니 스타일 규칙을 모두 지킨 것 같다.

unittest로 테스트하기

unittest는 말 그대로 코드 중에서 단위 별로 나눠서 해당 코드가 잘 작동하는지 테스트하기 위한 표준 라이브러리이다.

먼저 다음처럼 테스트할 코드를 준비했다. cap.py로 저장하였다.

def capitalize_string(text):
    return text.capitalize()

그 다음, test_cap.py 파일에 테스트 코드를 작성하였다.

import unittest
import cap

class TestCap(unittest.TestCase):
    def setUp(self):
        '''
        각각의 테스트 메서드들이 호출되기 이전에 먼저 호출됨.
        테스트할 때 필요한 외부 리소스를 할당하기 위해 만듦.
        '''
        pass

    def tearDown(self):
        '''
        각각의 테스트 메서드들이 호출된 이후에 호출됨.
        테스트 후 사용했던 외부 리소스 할당을 풀기 위해 만듦.
        '''
        pass

    def test_one_word(self):
        text = 'hello'
        result = cap.capitalize_string(text)
        self.assertEqual(result, 'Hello')

    def test_multiple_words(self):
        text = 'hello and nice to meet you'
        result = cap.capitalize_string(text)
        self.assertEqual(result, 'Hello And Nice To Meet You')

if __name__ == '__main__':
    unittest.main()

setUp(), tearDown() 메서드는 여기서는 외부 데이터를 이용할 일이 없으므로 정의하지 않아도 되나 이런 것도 존재한다는 것을 보이기 위해 의도적으로 넣었다. cap.py 파일 내 코드를 실제로 테스트할 부분은 test_one_word(), test_multiple_words 메서드들이다. 각각 capitalize_string 함수에 각기 다른 문자열 인수들을 대입하여 원하는 결과가 나오는지 확인하고 있다. 참고로, unittest에서는 무언가를 검사할 때 assertEqual()처럼 assert로 시작하는 메서드들을 사용하면 된다.

이제 test_cap.py 파일을 터미널 창에서 실행시켜보자.

F.
======================================================================
FAIL: test_multiple_words (__main__.TestCap)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\python2\python_basic\test_cap.py", line 27, in test_multiple_words
    self.assertEqual(result, 'Hello And Nice To Meet You')
AssertionError: 'Hello and nice to meet you' != 'Hello And Nice To Meet You'
- Hello and nice to meet you
?       ^   ^    ^  ^    ^
+ Hello And Nice To Meet You
?       ^   ^    ^  ^    ^

----------------------------------------------------------------------
Ran 2 tests in 0.001s

첫 번째 테스트는 통과했으나, 두 번째 테스트에서 에러를 발견하였다. 어디서 에러가 났는지도 상세히 설명해주고 있다.

doctest로 테스트하기

또 다른 표준 라이브러리의 테스트 패키지로 doctest가 있다. 이는 docstring 자체에 테스트 코드를 쓰는 방식이다. 마치 interactive interpreter처럼, 테스트하고자 하는 함수 호출문 앞에 »> 를 쓰고, 그 다음 라인에 예상되는 결과물을 쓰면 된다.

앞서 만든 cap.py 파일에 적용해보자.

def capitalize_string(text):
    """
    >>> capitalize_string('hello')
    'Hello'
    """
    return text.capitalize()

if __name__ == '__main__':
    import doctest
    doctest.testmod()

터미널 창에서 해당 파일을 실행했을 때 테스트를 모두 통과했다면 아무것도 출력되지 않을 것이다.

$ python cap.py

뒤에 -v를 추가로 입력하면 테스트 과정을 볼 수 있다.

$ python cap.py -v
Trying:
    capitalize_string('hello')
Expecting:
    'Hello'
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.capitalize_string
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

이번에는 테스트를 통과 못하면 어떻게 출력되는지 알기 위해 cap.py에 한 가지 테스트를 더 추가해보았다.

def capitalize_string(text):
    """
    >>> capitalize_string('hello')
    'Hello'
    >>> capitalize_string('nice to meet you')
    'Nice To Meet You'
    """
    return text.capitalize()

if __name__ == '__main__':
    import doctest
    doctest.testmod()

터미널 창에서 실행한 결과이다.

**********************************************************************
File "C:\python2\python_basic\cap.py", line 5, in __main__.capitalize_string
Failed example:
    capitalize_string('nice to meet you')
Expected:
    'Nice To Meet You'
Got:
    'Nice to meet you'
**********************************************************************
1 items had failures:
   1 of   2 in __main__.capitalize_string
***Test Failed*** 1 failures.

코드 실행 시간 재기

어떤 코드가 실행하는데 걸리는 시간을 알고 싶을 때, 가장 간단한 방법은 현재 시간을 먼저 잰 후, 검사하고자 하는 코드를 실행시킨 다음에 지금 시간을 재면 이 시간을 앞서 잰 시간에서 빼면 코드 실행 시간이 나올 것이다.

from time import time

t1 = time()  # 코드 실행 전 시각 기록

# 꽤 큰 범위의 구구단 계산 코드
mul_table = []
for i in range(2, 5000):
    one_table = []
    for j in range(2, 5000):
        one_table.append(i * j)
    mul_table.append(one_table)

print(time() - t1)  # 실행 시간 = 현재 시각 - 처음 잰 시각
5.24768590927124

이렇게 시간을 재는 것보다 더 간단한 방법이 있다. 바로 표준 모듈인 timeit을 사용하는 것이다. 여기서는 timeit 모듈의 timeit() 과 repeat() 함수 이 두 개에 대해서만 살펴보자.

  • timeit.timeit(stmt=’pass’setup=’pass’timer<default timer>,

number=1000000globals=None)

stmt : 실행 시간을 측정할 코드 또는 함수. 코드의 경우 반드시 문자열로 대입해야 한다. 함수의 경우 그저 함수명만 명시하면 된다.

setup: stmt를 실행하기 위해 사전에 필요한 코드나 함수 선언. setup 실행 시간은 측정되지 않음

timer: Timer 인스턴스

number: stmt를 반복 실행할 횟수.

from timeit import timeit

# 꽤 큰 범위의 구구단 계산 코드
test_code = '''mul_table = []
for i in range(2, 5000):
    one_table = []
    for j in range(2, 5000):
        one_table.append(i * j)
    mul_table.append(one_table)
'''

print(timeit(test_code, number=1)) # test_code를 한 번만 실행.
4.286585800000466
  • timeit.repeat(stmt=’pass’setup=’pass’timer=<default timer>repeat=5number=1000000globals=None)

repeat : timeit() 함수 자체를 호출할 반복 횟수. number가 stmt에 들어온 코드 또는 함수를 실행할 횟수라는 점과 다르다.

같은 코드라도 실행 시간을 여러 번 측정하다보면 시간이 똑같지 않다. 이럴 때 해당 코드의 평균 실행 시간을 도출하기 위해 repeat 함수를 쓰면 유용할 것이다. repeat 함수는 timeit() 함수를 여러번 실행시킨 후의 결과값인 측정 시간들을 요소로 갖는 list로 반환한다.

from timeit import repeat

# 꽤 큰 범위의 구구단 계산 코드
test_code = '''mul_table = []
for i in range(2, 5000):
    one_table = []
    for j in range(2, 5000):
        one_table.append(i * j)
    mul_table.append(one_table)
'''

print(repeat(test_code, number=1, repeat=3))
[4.149668500002008, 4.113044900004752, 4.114543700008653]

측정 시간에 쓰이는 것이 함수일 경우 다음과 같이 함수명을 그대로 입력하면 된다. 다음 예제는 list 생성 시 for문을 이용하는 방법과 comprehension문을 이용한 방법의 실행 시간을 비교해보고자 하는 예제이다.

from timeit import timeit

def make_list_for():
    some_list = []
    for element in range(1000):
        some_list.append(element)
    return some_list

def make_list_comp():
    some_list = [element for element in range(1000)]
    return some_list

time_for = timeit(make_list_for, number=1000)
time_comp = timeit(make_list_comp, number=1000)

print('for문을 이용한 list 생성 시간: ', time_for ,'초')
print('comprehension을 이용한 list 생성 시간: ', time_comp, '초')
for문을 이용한 list 생성 시간:  0.1371680000447668 
comprehension을 이용한 list 생성 시간:  0.04923979996237904 

그런데 만약, 테스트할 함수에 매개변수가 존재한다면 어떻게 해야 할까?

from timeit import timeit

def make_list_for(number):
    some_list = []
    for element in range(number):
        some_list.append(element)
    return some_list

def make_list_comp(number):
    some_list = [element for element in range(number)]
    return some_list

num = 1000
time_for = timeit('make_list_for({})'.format(num), number=1000, setup='from __main__ import make_list_for')
time_comp = timeit('make_list_comp({})'.format(num), number=1000, setup='from __main__ import make_list_comp')

print('for문을 이용한 list 생성 시간: ', time_for ,'초')
print('comprehension을 이용한 list 생성 시간: ', time_comp, '초')
for문을 이용한 list 생성 시간:  0.1316596000106074 
comprehension을 이용한 list 생성 시간:  0.049061099998652935 

이 경우 timeit()의 setup 매개변수에 ‘from __main__ import 함수명’을 대입해야 한다. __main__ 은 현재 파이썬 인터프리터가 실행하는 스크립트 파일 자체를 말한다. 그래서 만약 다른 모듈에서 실행한다면 그 모듈의 이름을 직접 명시하는 것이 좋다. make_list_for라는 함수가 포함된 모듈명이 study.py라면 from study import make_list_for 처럼 말이다.) 그리고 stmt 부분에는 문자열 형태로 함수명과 옆에 괄호 ()를 써주고, 괄호 안에 직접 인자를 써넣거나 위 예제처럼 format() 을 이용하여 실제 인자를 넘기면 된다.


Reference

[1] Bill Lubannovic, “Introducing Python” (O’REILLY, 2015)

[2] timeit — 작은 코드 조각의 실행 시간 측정

[3] [Python] 함수 및 코드 실행시간 측정하기 (timeit 모듈 사용법)

This content is licensed under CC BY-NC 4.0

댓글남기기