Effective PYTHON 2nd - Ch09
포스트
취소

Effective PYTHON 2nd - Ch09

Python 기초를 다지기 위해 효과적인 파이썬이란 책을 공부 중입니다.

9장 테스트와 디버깅

  • 파이썬은 선택적인 타입 애너테이션을 지원하며 이를 활용해 정적 분석을 수행할 수 있다.
    • 파이썬은 컴파일 시점에 정적 타입 검사를 수행하지 않는다.
    • 또한 컴파일 시점에 프로그램이 제대로 작동할 것이라고 확인할 수 있는 요소가 전혀 없다.

75) 디버깅 출력에는 repr 문자열을 사용하라

  • repr 을 호출하면 출력 가능한 문자열을 얻는다. 이를 eval 함수에 전달하면 원래 값을 돌려받을 수 있다.
  • 형식화 문자열 %s 는 str 과 같이 문자열을 만들어낸다.
  • 직접 클래스의 __repr__ 메서드를 정의해서 원하는 표현을 만들 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print( repr(5) )    # 5
print( repr('5') )  # '5'

print( '%r'%5 )     # 5
print( '%r'%'5' )   # '5'

# f-문자열에는 !r 타입변환을 사용할 것
int_value = 5
str_value = '5'
print(f'{int_value!r} != {str_value!r}')
# >>> 5 != '5'

b = eval( repr(int_value) )
assert int_value == b
c = eval( repr(str_value) )
assert str_value == c
1
2
3
4
5
6
7
8
9
10
11
12
13
class BetterClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'BetterClass({self.x!r},{self.y!r})'

obj = BetterClass(2, 'what')
print(obj)
# >>> BetterClass(2, 'what')
print(obj.__dict__)
# >>> {'x':2, 'y':'what'}

76) TestCase 하위 클래스를 사용해 프로그램에서 연관된 행동 방식을 검증하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# utils.py
def to_str(data):
    if isinstance(data, str):
        return data
    elif isinstance(data, bytes):
        return data.decode('utf-8')
    else:
        raise TypeError('str 또는 bytes 를 전달해야 합니다, found value: %r'%data)


# utils_test.py
from unittest import TestCase, main
from utils import to_str

# TestCase 의 하위클래스 정의
class UtilsTestCase(TestCase):
    # 테스트 메서드
    def test_to_str_bytes(self):
        self.assertEqual('hello', to_str(b'hello'))

    def test_to_str_str(self):
        self.assertEqual('hello', to_str('hello'))

    # 실패 리포트 출력함
    def test_failing(self):
        self.assertEqual('incorrect', to_str('hello'))


if __name__ == '__main__':
    main()      # 테스트 케이스 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# assert_test.py
from unittest import TestCase, main
from utils import to_str

class AssertTestCase(TestCase):
    # Value 까지 출력하면서 오류 출력 (더 도움된다)
    def test_assert_helper(self):
        expected = 12
        found = 2 * 5
        self.assertEqual(expected, found)

    # AssertionError 출력
    def test_assert_statement(self):
        expected = 12
        found = 2 * 5
        assert expected == found


if __name__ == '__main__':
    main()      # 테스트 케이스 실행

77) setUp, tearDown, setUpModule, tearDownModule을 사용해 각각의 테스트를 격리하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# environment_test.py
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest import TestCase, main

class EnvironmentTest(TestCase):
    def setUp(self):
        self.test_dir = TemporaryDirectory()
        self.test_path = Path(self.test_dir.name)

    def tearDown(self):
        self.test_dir.cleanup()

    def test_modify_file(self):
        with open(self.test_path / 'data.bin', 'w') as f:
            # ...
            pass

if __name__ == '__main__':
    main()



# integration_test.py
from unittest import TestCase, main

def setUpModule():
    print('** setup module')

def tearDownModule():
    print('** reset module')

class IntegrationTest(TestCase):
    def setUp(self):
        print('** setup Test')

    def tearDown(self):
        print('** reset Test')

    def test_end_to_end1(self):
        print('** Test1')

    def test_end_to_end2(self):
        print('** Test2')

if __name__ = '__main__':
    main()

1
2
3
4
5
6
7
8
9
$ python3 integration_test.py
* 모듈 설정
 * 테스트 설정
  * 테스트 1
 * 테스트 정리
 * 테스트 설정
  * 테스트 2
 * 테스트 정리
* 모듈 정리

78) 목을 사용해 의존 관계가 복잡한 코드를 테스트하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DatabaseConnection:
    # ...
    pass

def get_animals(database, species):
    # 데이터베이스에 질의한다
    # ...
    # (이름, 급양시간) 튜플 리스트를 반환한다
    pass


database = DatabaseConnection('localhost', '4444')

get_animals(database, '미어캣')
# >>>
# DatabaseConnectionError: Not connected

나은 방법은 데이터베이스를 모킹(mocking)하는 것이다.
목(mock)은 자신이 흉내 내려는 대상에 의존하는 다름 함수들이 어떤 요청을 보내면 어떤 응답을 보내야 할지 알고, 요청에 따라 적절한 응답을 돌려준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from datatime import datetime
from unittest.mock import Mock

mock = Mock(spec=get_animals)
expected = [
    ('점박이', datetime(2020, 6, 5, 11, 15)),
    ('털보', datetime(2020, 6, 5, 11, 15)),
    ('제제', datetime(2020, 6, 5, 11, 15)),
]
mock.return_value = expected


database = object()
result = mock(database, '미어캣')
assert result == expected
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from unittest.mock import ANY

mock = Mock(spec=get_animals)
mock('database 1', '토끼')
mock('database 2', '들소')
mock('database 3', '미어캣')

mock.assert_called_with(ANY, '미어캣')


class MyError(Exception):
    pass

mock = Mock(spec=get_animals)
mock.side_effect = MyError('에구모니나! 큰 문제 발생')
result = mock(database, '미어캣')
# >>>
# MyError: 에구머니나! 큰 문제 발생
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def get_food_period(database, species):
    # 데이터베이스에 질의한다
    # ...
    # 주기를 반환한다

def feed_animals(database, name, when):
    # 데이터베이스에 기록한다
    # ...

def do_rounds(database, species):
    now = datetime.datetime.utcnow()
    feeding_timedelta = get_food_period(database, species)
    animals = get_animals(database, species)
    fed = 0

    for name, last_mealtime in animals:
        if (now-last_mealtime) > feeding_timedelta:
            feed_animal(database, name, now)
            fed += 1
    return fed
####

def do_rounds(database, species, *,
    now_func=datetime.utcnow,
    food_func = get_food_period,
    animals_func = get_animals,
    feed_func = feed_animal
):
    now = now_func()
    feeding_timedelta = food_func(database, species)
    animals = animals_func(database, species)
    fed = 0

    for name, last_mealtime in animals:
        if (now-last_mealtime) > feeding_timedelta:
            feed_func(database, name, now)
            fed += 1
    return fed

####

from datetime import timedelta

now_func = Mock(spec=datetime.utcnow)
now_func.return_value = datetime(2020, 6,5,15,45)

food_func = Mock(spec=get_food_period)
food_func.return_value = timedelta(hours=3)

animals_func = Mock(spec=get_animals)
animals_func.return_value = [
    ('점박이', datetime(2020, 6, 5, 11, 15)),
    ('털보', datetime(2020, 6, 5, 11, 15)),
    ('제제', datetime(2020, 6, 5, 11, 15)),
]

feed_func = Mock(spec=feed_animal)

result = do_rounds(
    database,
    '미어캣',
    now_func=now_func,
    food_func = food_func,
    animals_func=animals_func,
    feed_func = feed_func
)
assert result == 2

####

from unittest.mock import call

food_func.assert_called_once_with(database, '미어캣')

animals_func.assert_called_once_with(database, '미어캣')

feed_func.assert_has_calls([
    call(database, '점박이', now_func.return_value),
    call(database, '털보', now_func.return_value),
],
any_order=True)

####

from unittest.mock import patch

print('patch external:', get_animals)

with patch('__main__.get_animals'):
    print('patch internal:', get_animals)

print('again patch external:', get_animals)

79) 의존 관계를 캡슐화해 모킹과 테스트를 쉽게 만들라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ZooDatabase:
    # ...

    def get_animals(self, species):
        # ...

    def get_food_period(self, species):
        # ...

    def feed_animals(self, name, when):
        # ...

####

from datetime import datetime

def do_rounds(database, species, *, utcnow=datetime.utcnow):
    now = ntcnow()
    feeding_timedelta = database.get_food_period(species)
    animals = database.get_animals(species)
    fed = 0

    for name, last_mealtime in animals:
        if (now - last_mealtime) >= feeding_timedelta:
            database.feed_animal(name, now)
            fed += 1
    return fed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from unittest.mock import Mock

database = Mock(spec=ZooDatabase)
print(database.feed_animal)
database.feed_animal()
database.feed_animal.assert_any_call()

####

from datetime import timedelta
from unittest.mock import call

now_func = Mock(spec=datetime.utcnow)
now_func.return_value = datetime(2019, 6, 5, 15, 45)

database = Mock(spec=ZooDatabase)
database.get_food_period.return_value = timedelta(hour=3)
database.get_animals.return_value = [
    ('점박이', datetime(2020, 6, 5, 11, 15)),
    ('털보', datetime(2020, 6, 5, 11, 15)),
    ('제제', datetime(2020, 6, 5, 11, 15)),
]

result = do_rounds(database, '미어캣', utcnow=now_func)
assert result == 2

database.get_food_period.assert_called_once_with('미어캣')
database.get_animals.assert_called_once_with('미어캣')
database.feed_func.assert_has_calls([
    call(database, '점박이', now_func.return_value),
    call(database, '털보', now_func.return_value),
],
any_order=True)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
DATABASE = None

def get_database():
    global DATABASE
    if DATABASE is None:
        DATABASE = ZooDatabase()
    return DATABASE

def main(argv):
    database = get_database()
    species = argv[1]
    count = do_rounds(database, species)
    print(f'feed: {count} {species}')
    return 0

####

import contextlib
import io
from unittest.mock import patch

with patch('__main__.DATABASE', spec=ZooDatabase):
    now = datetime.utcnow()

    DATABASE.get_food_period.return_value = timedelta(hours=3)
    DATABASE.get_animals.return_value = [
        ('점박이', datetime(2020, 6, 5, 11, 15)),
        ('털보', datetime(2020, 6, 5, 11, 15)),
        ('제제', datetime(2020, 6, 5, 11, 15)),
    ]

    fake_stdout = io.StringIO()
    with contextlib.redirect_stdout(fake_stdout):
        main(['program name', '미어캣'])

    found = fake_stdout.getvalue()
    expected = 'feed: 2 미어캣\n'

    assert found == expected

80) pdb를 사용해 대화형으로 디버깅하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# always_breakpoint.py

import math

def compute_rmse(observed, ideal):
    total_err_2 = 0
    count = 0
    for got, wanted in zip(observed, ideal):
        err_2 = (got - wanted) ** 2
        breakpoint()    # 여기서 디버거를 시작함
        total_err_2 += err_2
        count += 1

    mean_err = total_err_2 / count
    rmse = math.sqrt(mean_err)
    return rmse

result = compute_rmse(
    [1.8, 1.7, 3.2, 6],
    [2, 1.5, 3, 5]
)
print(result)

# 실행시 파이썬 대화형 셀이 시작된다.
# $ python3 always_breakpoint.py
# > always_breakpoint.py(12)compute_rmse()
# -> total_err_2 += err_2
# (Pdb)

# Pdb 프롬프트에서 p <이름> 으로 지역변수 이름을 입력하면 값을 출력할 수 있다.

#####

# 조건부 디버깅
# conditional_breakpoint.py
def compute_rmse(observed, ideal):
    # ...
    for got, wanted in zip(observed, ideal):
        err_2 = (got - wanted) ** 2
        if err_2 >= 1:      # True 인 경우에만
            breakpoint()    # 디버거를 시작함
        total_err_2 += err_2
        count += 1
    # ...

#####

# 사후 디버깅
# postmortem_breakpoint.py

import math

# 평균제곱근오차(rmse)를 구함
def compute_rmse(observed, ideal):
    # ...

# $ python3 -m pdb -c continue postmortem_breakpoint.py
# Trackback (most recent call last)
# ...
# > postmortem_breakpoint.py(16)compute_rmse()
# -> rmse = math.sqrt(mean_err)
# (Pdb) mean_err
# (-5.97-17.5j)

81) 프로그램이 메모리를 사용하는 방식과 메모리 누수를 이해하기 위해 tracemalloc을 사용하라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# waste_memory.py
import os

class MyObject:
    def __init__(self):
        self.data = os.urandom(100)

    def get_data():
        values = []
        for _ in range(100):
            obj = MyObject()
            values.append(obj)

    def run():
        deep_values = []
        for _ in range(100):
            deep_values.append(get_data())
            return deep_values

#####

# using_gc.py
import gc

found_objects = gc.get_objects()
print('before:', len(found_objects))

import waste_memory

hold_reference = waste_memory.run()

found_objects = gc.get_objects()
print('after:', len(found_objects))

for obj in found_objects[:3]:
    print(repr(obj)[:100])

# gc.get_objects 의 문제점은 객체가 어떻게 할당됐는지를 알려주지 않는다는 것이다.
# 파이썬 3.4부터는 이런 문제를 해결해 주는 tracemalloc 내장모듈이 새로 도입됐다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# top_n.py
import tracemalloc

tracemalloc.start(10)                   # 스택 깊이 설정
time1 = tracemalloc.take_snapshot()     # 이전 스냅샷

import waste_memory

x = waste_memory.run()      # 이 부분의 메모리 사용을 디버깅함
time2 = tracemalloc.take_snapshot()     # 이후 스냅샷

stats = time2.compare_to(time1, 'lineno')   # 두 스냅샷을 비교
for stat in stats[:3]:
    print(stat)

#####

# with_trace.py
import tacemalloc

tracemalloc.start(10)
time1 = tracemalloc.take_snapshot()

import waste_memory

x = waste_memory.run()
time2 = tracemalloc.take_snapshot()

stats = time2.compare_to(time1, 'traceback')
top = stats[0]
print('가장 많이 사용하는 부분은:')
print('\n'.join(top.traceback.format()))

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.