본문 바로가기

프로그래머/Python

[Effective Python 복습] Chapter 8. 제품화

파이썬 코딩의 기술

Chapter 8. 제품화

54. 배포 환경을 구성하는 데는 모듈 스코프 코드를 고려하자

  • 종종 프로그램을 여러 배포 환경에서 실행해야 하며, 각 환경마다 고유한 전제와 설정이 있다
  • 모듈 스코프에서 일반 파이썬 문장을 사용하여 모듈 콘텐츠를 다르배포 환경에 맞출 수 있다
  • 모듈 콘텐츠는 sys와 os 모듈을 이용한 호스트 조사 내역 같은 외부 조건의 결과물이 될 수 있다

프로그램의 일부를 오버라이드

# dev_main.py
TESTING = True
import db_connection
db = db_connection.Database()

# prod_main.py
TESTING = False
import db_connection
db = db_connection.Database()

# db_connection.py
import __main__

class TestingDatabase(object):
    # ...

class RealDatabase(object):
    # ...

if __main__.TESTING:
    Database = TestingDatabase
else:
    Database = RealDatabase
  • 배포 환경이 복잡해지면 파이썬 상수에서 별도의 설정 파일로 옮기는 방안을 고려
  • 내장 몯류 configparser와 같은 도구를 이용하면 제품 설정을 코드와 분리해서 관리

os.environ에 들어 있는 환경 변수를 기반으로 모듈 정의

# db_connection.py
import sys

class Win32Database(object):
    # ...

class PosixDatabase(object):
    # ...

if sys.platform.startswith('win32'):
    Database = Win32Database
else:
    Database = PosixDatabase

55. 디버깅 출력용으로는 repr 문자열을 사용하자

  • 파이썬 내장 타입의 print를 호출하면 사람이 이해하기는 쉽지만 타입 정보는 숨은 문자열 버전의 값이 나온다
  • 파이썬 내장 타입에 repr을 호출하면 출력할 수 있는 문자열 버전의 값이 나온다
  • 이 repr 문자열을 eval 내장 함수에 전달하면 원래 값으로 되돌릴 수 있다
  • 포맷 문자열에서 %s는 str처럼 사람이 이해하기 쉬운 문자열을 만들어내며, %r은 repr처럼 출력하기 쉬운 문자열을 만들어낸다
  • __ repr __ 메서드를 정의하면 클래스의 출력 가능한 표현을 사용자화하고 더 자세한 디버깅 정보를 제공할 수 있다
  • 객체의 __ dict __ 속성에 접근하면 객체의 내부를 볼 수 있다
print('foo bar')
print('$s' % 'foo bar')

print(5)        # 5
print('5')        # 5

a = '\x07'        
print(repr(a))    # '\x07'

b = eval(repr(a))
assert a = b

print(repr(5))        # 5
print(repr('5'))    # '5'

class OpaqueClass(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

obj = OpaqueClass(1, 2)
print(obj)

class BetterClass(object):
    def __init__(self, x, y):
        # ...

    def __repr__(self):
        return 'BetterClass(%d, %d)' % (self.x, self.y)

obj = BetterClass(1, 2)
print(obj)                # BetterClass(1, 2)

obj = OpaqueClass(4, 5)
print(obj.__dict__)        # {'y': 5, 'x': 4}

56. unittest로 모든 것을 테스트하자

  • 파이썬 프로그램을 신뢰할 수 있는 유일한 방법은 테스트를 작성하는 것이다
  • 내장 모듈 unittest는 좋은 테스트를 작성하는 데 필요한 대부분의 기능을 제공한다
  • TestCase를 상속하고 테스트하려는 동작별로 메서드 하나를 정의해서 테스트를 정의할 수 있다
  • TestCase 클래스에 있는 테스트 메서드는 test라는 단어로 시작해야 한다
  • 단위 테스트(고립된 기능용)와 통합 테스트(상호 작용하는 모듈용) 모두를 작성해야 한다

테스트 작성

# units.py
def to_str(data):
    if isinstance(data, str):
        return data
    elif isinstance(data, bytes):
        return data.decode('utf-8')
    else:
        raise TypeError('Must supply str or bytes, found: %r' % data)

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

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_to_str_bad(self):
        self.assertRaises(TypeError, to_str, object())

if __name__ == '__main__':
    main()
  • 테스트 메서드가 어떤 종류의 Exception을 일으키지 않고 실행된다면 테스트가 성공적으로 통과한 것이다
  • 동등성을 검사하는 assertEqual
  • 불 표현식을 검사하는 asssertTrue
  • 적절할 때 에외가 발생하는지 검사하는 asssertRaises

테스트 메서드 실행 전 테스트 환경 설정

class MyTest(TestCase):
    def setUp(self):
        self.test_dir = TemporaryDirectory()
    def tearDown(self):
        self.test_dir.cleanup()
    # 테스트 메서드
    # ...
  • 각 테스트 전에 임시 디렉터리를 생성
  • 각 테스트가 종료한 후에 그 내용을 삭제

57. pdb를 이용한 대화식 디버깅을 고려하자

  • import pdb; pdb.set_trace() 문으로 프로그램의 관심 지점에서 직접 파이썬 대화식 디버거를 시작할 수 있다
  • 파이썬 디버거 프롬프트는 실행 중인 프로그램의 상태를 조사하거나 수정할 수 있도록 해주는 완전한 파이썬 셸이다
  • pdb 셸 명령을 이용하면 프로그램 실행을 세밀하게 제어할 수 있다
  • 또한 프로그램의 상태를 조사하거나 프로그램 실행을 이어가는 과정을 교대로 반복할 수 있다
def complex_func(a, b, c):
    # ...
    import pdb; pdb.set_trace()

58. 최적화하기 전에 프로파일하자

  • 성능 저하를 일으키는 원인이 때로는 불분명하므로 파이썬 프로그램을 최적화하기 전에 프로파일해야 한다
  • cProfile이 더 정확한 프로파일링 정보를 제공하므로 profile 모듈 대신에 cProfile을 사용하자
  • Profile 객체의 runcall 메서드는 함수 호출 트리를 프로파일하는 데 필요한 모든 기능을 제공한다
  • Stats 객체는 프로그램 성능을 이해하는 데 필요한 프로파일링 정보를 선택하고 출력하는 기능을 제공한다
from random import randint

max_size = 10**4
data = [randint(0, max_size) for _ in range(max_size)]
test = lambda: insertion_sort(data)

from cProfile import Profile
profiler = Profile()
profiler.runcall(test)

stats = Stats(profiler)
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats()
  • ncalls : 프로파일링 주기 동안 함수 호출 횟수
  • tottime : 함수가 실행되는 동안 소비한 초 단위의 시간으로, 다른 함수 호출을 실행하는 데 걸린 시간은 배재한다
  • tottime percall : 함수를 호출하는 데 걸린 평균 시간이며 초 단위다. 다르함수의 호출 시간은 배제한다. tottime을 ncalls로 나눈 값이다
  • cumtime : 함수를 실행하는 데 걸린 초 단위 누적 시간이며, 다른 함수를 호출하는 데 걸린 시간도 포함된다
  • cumtime percall : 함수를 호출할 때마다 걸린 시간에 대한 초 단위 평균 시간이며, 다른 함수를 호출하는 데 걸린 시간도 포함한다. cumtime을 ncalls로 나눈 값이다
stats.print_callers()
  • 각 함수의 프로파일링 정보에 기여하는 호출자를 찾는 방법을 제공한다
  • 이 프로파일러 통계 테이블은 호출된 함수를 왼쪽에 보여주며, 누가 이런 호출을 하는지를 오른쪽에 보여준다

59. tracemalloc으로 메모리 사용 현황과 누수를 파악하자

  • 파이썬 프로그램이 메모리를 어떻게 사용하고, 메모리 누수를 일으키는지를 이해하기는 어렵다
  • gc 모듈은 어떤 객체가 존재하는지를 이해하는 데 도움을 주지만, 해당 객체가 어떻게 할당되었는지에 대한 정보는 제공하지 않는다
  • 내장 모듈 tracemalloc은 메모리 사용량의 근원을 이해하는 데 필요한 강력한 도구를 제공한다
  • tracemalloc은 파이썬 3.4와 그 이후 버전에서만 사용할 수 있다

참조를 유지하며 메모리를 낭비하는 프로그램

# using_gc.py
import gc
found_objects = gc.get_objects()
print('%d objects before' % len(found_objects))

import waste_money
x = waste_memory.run()
found_objects = gc.get_objects()
print('%d objects after' % len(found_objects))
for obj in found_objects[:3]:
    print(repr(obj)[:100])
  • gc.get_objects는 객체가 어떻게 할당되는지 아무런 정보도 제공하지 않는다

프로그램에서 메모리를 가장 많이 사용하는 세 부분 출력

# top_n.py
import tracemalloc
tracemalloc.start(10)    # 스택 프레임을 최대 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)
  • 어떤 객체들이 프로그램 메모리 사용량을 주로 차지하고, 소스 코드의 어느 부분에서 할당되는지를 쉽게 알 수 있다