본문 바로가기

프로그래머/Python

[Effective Python 복습] Chapter 6. 내장 모듈

파이썬 코딩의 기술

내장 모듈

42. functools.wraps로 함수 데코레이터를 정의하자

  • 데코레이터는 런타임에 한 함수로 다른 함수를 수정할 수 있게 해주는 파이썬 문법이다
  • 데코레이터를 사용하면 디버거와 같이 객체 내부를 조사하는 도구가 이상하게 동작할 수도 있다
  • 직접 데코레이터를 정의할 때 이런 문제를 피하려면 내장 모듈 functools의 wraps 데코레이터를 사용하자
def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print('%s(%r, %r) -> %r' %
            (func.__name__, args, kwargs, result))
        return result
    return wrapper

@trace
def fibonacci(n):
    if n in (0,1):
        return n
    return (fibonacci(n-2) + fibonacci(n-1))

print(fibonacci)    # trace 함수는 그 안에 정의된 wrapper를 반환
help(fibonacci)        # 그 wrapper 함수가 fibonacci라는 이름에 할당 됨


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        #...
    return wrapper

@trace
def fibonacci(n):
    # ...

help(fibonacci)

43. 재사용 가능한 try/finally 동작을 만드려면 contextlib와 with문을 고려하자

  • with문을 이용하면 try/finally 블록의 로직을 재사용할 수 있고, 코드를 깔끔하게 만들 수 있다
  • 내장 모듈 contextlib의 contextmanager 데코레이터를 이용하면 직접 작성한 함수를 with 문에서 쉽게 사용할 수 있다
  • 컨텍스트 매니저에서 넘겨준 값은 with 문의 as 부분에 할당된다. 컨텍스트 매니저에서 값을 반환하는 방법은 코드에서 특별한 컨텍스트에 직접 접근하려는 경우에 유용하다
def my_function():
    logging.debug('Some debug data')
    logging.error('Error log here')
    logging.debug('More debug data')

@contextmanager
def debug_logging(level):
    logger = logging.getLogger()
    old_level = logger.getEffectiveLevel()
    logger.setLevel(level)
    try:
        yield
    finally:
        logger.setLevel(old_level)

with debug_logging(logging.DEBUG):
    print('Inside:')
    my_function()    # 디버그 메시지가 모두 화면에 출력
print('After:')
my_function()        # 디버깅 메시지가 출력되지 않음

with 타깃 사용하기

@contextmanager
def log_level(level, name):
    logger = logging.getLogger(name)
    old_level = logger.getEffectiveLevel()
    logger.setLevel(level)
    try:
        yield logger
    finally:
        logger.setLevel(old_level)

with log_level(logging.DEBUG, 'my-log') as logger:
    logger.debug('This is my message!')
    logging.debug('This will not print')

logger = logging.getLogger('my-log')
logger.debug('Debug will not print')
logger.error('Error will print')    

44. copyreg로 pickle을 신뢰할 수 있게 만들자

  • 내장 모듈 pickle은 신뢰할 수 있는 프로그램 간에 객체를 직렬화하고 역직렬화하는 용도로만 사용할 수 있다
  • pickle 모듈은 간단한 사용 사례를 벗어나는 용도로 사용하면 제대로 동작하지 않을 수도 있다
  • 빠뜨린 속성 값을 추가하거나 클래스에 버전 관리 기능을 제공하거나 안정적인 임포트 경로를 제공하려면 pickle과 함께 내장 모듈 copyreg를 사용해야 한다
class GameState(object):
    def __init__(self):
        self.level = 0
        self.lives = 4

state = GameState()
state.level += 1
state.lives += 1

state_path = '/tmp/game_state.bin'
with open(state_path, 'wb') as f:
    pickle.dump(state, f)

with open(state_pth, 'rb') as f:
    state_after = pickle.load(f)
print(state_after.__dict__)

기본 속성값

class GameState(object):
    def __init__(self, level=0, lives=4, points=0):
        self.level = level
        self.lives = lives
        self.points = points

def pickle_game_state(game_state):
    kwargs = game_state.__dict__
    return unpickle_game_state, (kwargs,)

def unpickle_game_state(kwargs):
    return GameState(**kwargs)

# GameStae 객체와 직렬화 함수 등록
copyreg.pickle(GameState, pickle_game_state)

state = GameState()
state.points += 1000
serialized = pickle.dumps(state)
state_after = pickle.loads(serialized)
print(state_after.__dict__)

클래스 버전 관리

# lives 제거
class GameState(object):
    def __init__(self, level=0, points=0):
    # ...

def pickle_game_state(game_state):
    kwargs = game_state.__dict__
    kwargs['version'] = 2
    return unpickle_game_state, (kwargs,)

def unpickle_game_state(kwargs):
    version = kwargs.pop('version', 1)
    if version == 1:
        kwargs.pop('lives')
    return GameState(**kwargs)

copyreg.pickle(GameState, pickle_game_state)
state_after = pickle.loads(serialized)
print(state_after.__dict__)

안정적인 임포트 경로

# 클래스 이름 변경
class BetterGameState(object):
    def __init__(self, level=0, points=0, magic=5):
        # ...

copyreg.pickle(BetterGameState, pickle_game_state)
state = BetterGameState()
serialized = pickle.dumps(state)

45. 지역 시간은 time이 아닌 datetime으로 표현하자

  • 서로 다른 시간대를 변환하는 데는 time 모듈을 사용하지 말자
  • pytz 모듈과 내장 모듈 datetime으로 서로 다른 시간대 사이에서 시간을 신뢰성 있게 변환하자
  • 항상 UTC로 시간을 표현하고, 시간을 표시하기 전에 마지막 단계로 UTC 시간을 지역 시간으로 변환하자
from datetime import datetime, timezone

# 현재 시각을 UTC로 얻어와서 지역시간(태평양 연안 표준시)로 변환
now = datetime(2014, 8, 10, 18, 18, 30
now_utc = now.replace(tzinfo = timezone.utc)
now_local = now_utc.astimezone()
print(now_local)

# 지역시간을 다시 UTC의 유닉스 타임스탬프로 변경
time_str = '2014-08-10 11:18:30'
now = datetime.strptime(time_str, time_format)
time_tuple = now.timetuple()
utc_now = mktime(time_tuple)
print(utc_now)

# NYC 도착시간을 UTC datetime으로 변환
arrival_nyc = '2014-05-01 23:33:24'
nyc_dt_naive = datetime.strptime(arrival_nyc, time_format)
eastern = pytz.timezone('US/Eastern')
nyc_dt = eastern.localize(nyc_dt_naive)
utc_dt = pytz.utc.normalize(nyc_dt.astimezone(pytz.utc))
print(utc_dt)

# 샌프란시스코 지역 시간으로 변환
pacific = pytz.timezone('US/Pacific')
sf_dt = pacific.normalize(utc_dt.astimezone(pacific))
print(sf_dt)

46. 내장 알고즘과 자료 구조를 사용하자

  • 알고리즘과 자료구조를 표현하는 데는 파이썬의 내장 모듈을 사용하자
  • 이 기능들을 직접 재구현하지는 말자. 올바르게 만들기가 어렵기 때문.

double ended queue(deque)

import collections

fifo = deque()
fifo.append(1)        # 생산자
x = fifo.popleft()    # 소비자
  • deque는 큐의 처음과 끝에서 아이템을 삽입하거나 삭제할 때 항상 일정한 시간이 걸리는 연산을 제공한다

정렬된 딕셔너리(OrderedDict)

a = {}
a['foo'] = 1
a['bar'] = 2

# 무작위로 'b'에 데이터를 추가해서 해시 충돌을 일으킴
while True:
    z = randint(99, 1013)
    b = {}
    for i in range(2):
        b[i] = i
    b['foo'] = 1
    b['bar'] = 2
    for i in range(z):
        del b[i]
    if str(b) != str(a):
        break

    print(a)        # {'foo' : 1, 'bar' : 2}
    print(b)        # {'bar' : 2, 'foo' : 1}
    print(a == b)    # True

a = OrderedDict()
a['foo'] = 1
a['bar'] = 2

b = OrderedDict()
b['foo'] = red
b['bar'] = blue

for value1, value2 in zip(a.values(), b.values()):
    print(value1, value2)

# 1 red
# 2 blue

기본 딕셔너리

import collections

stats = {}
key = 'my_counter'
if key not in stats:
    stats[key] = 0
stats[key] += 1

stats = defaultdict(int)
stats['my_counter'] += 1
  • 딕셔너리를 사용할 때 한 가지 문제는 어떤 키가 이미 존재한다고 가정할 수 없다는 것
  • collectioins 모듈의 defaultdict 클래스는 키가 존재하지 않으면 자동으로 기본값을 저장
  • int는 0을 반환, list, set은 빈 list, 빈 set으로 초기화

힙 큐

  • 힙(heap)은 우선순위 큐(priority queue)를 유지하는 유용한 자료다
  • heapq 모듈은 표준 list 타입으로 힙을 생성하는 heappush, heappop, nsmallest 같은 함수를 제공
  • 이진트리(binary tree) 기반의 최소 힙 자료구조
  • 가장 작은 값은 언제나 인덱스 0, 즉 이진 트리의 루트에 위치
a = []
heappush(a, 5)
heappush(a, 3)
heappush(a, 7)
heappush(a, 4)

print(heappop(a), heappop(a), heappop(a), heappop(a))    # 3 4 5 7


a = []
heappush(a, 5)
heappush(a, 3)
heappush(a, 7)
heappush(a, 4)
assert a[0] == nsmallest(1, a)[0] == 3

print(a)    # [3, 4, 7, 5]    -> 이진 트리 구조
a.sort()
print(a)    # [3, 4, 5, 7]

바이섹션

x = list(range(10**6))
i = x.index(991234)

i = bisect_left(x, 991234)
  • list에서 아이템을 검색하는 작업은 index 메서드 호출 시 길이에 비례하여 선형적 증가
  • 바이너리 검색의 복잡도는 로그 형태로 증가
  • 이진분할 알고리즘 사용

이터레이터 도구

  • 내장모듈 itertools는 이터레이터를 구성하거나 이터레이터와 상호 작용하는 데 유용한 함수를 다수 포함
  • 이터레이터 연결
    • chain : 여러 이터레이터를 순차적인 이터레이터 하나로 결합
    • cycle : 이터레이터의 아이템을 영원히 반복
    • tee : 이터레이터 하나를 병렬 이터레이터 여러 개로 나눈다
    • zip_longest : 길이가 서로 다른 이터레이터들에도 잘 동작하는 내장 함수 zip의 변형
  • 이터레이터에서 아이템 필터링
    • islice : 복사 없이 이터레이터를 숫자로 된 인덱스로 슬라이스
    • takewhile : 서술 함수가 True를 반환하는 동안 이터레이터의 아이템을 반환
    • dropwhile : 서술 함수가 처음으로 False를 반환하고 나면 아터레이터의 아이템을 반환
    • filterfalse : 서술 함수가 False를 반환하는 이터레이터의 모든 아이템을 반환. 내장 함수 filter의 반대 기능
  • 이터레이터에 있는 아이템들의 조합
    • product : 이터레이터에 있는 아이템들의 cartesian product을 반환. 길게 중첩된 리스트 컴프리헨션에 대한 훌륭한 대안
    • permuattions : 이터레이터에 있는 아이템을 담은 길이 N의 순서 있는 순열을 반환
    • combinations : 이터레이터에 있는 아이템을 중복되지 않게 담은 길이 N의 순서 없는 조합을 반환
import itertools

letters = ['a', 'b', 'c', 'd', 'e', 'f']
booleans = [1, 0, 1, 0, 0, 1]
decimals = [0.1, 0.7, 0.4, 0.5]

print(list(itertools.chain(letters, booleans, decimals)))
# ['a', 'b', 'c', 'd', 'e', 'f', 1, 0, 1, 0, 0, 1, 0.1, 0.7, 0.4, 0.5]  
from itertools import count , izip
for number, letter in izip(count(0, 10), ['a', 'b', 'c', 'd', 'e']):
    print('{0}: {1}'.format(number, letter))
    # 0: a  10: b  20: c  30: d  40: e
from itertools import izip
print(list(izip[1,2,3], ['a','b','c']))
# [(1, 'a'), (2, 'b'), (3, 'c')]
  • 기존 zip 보다 약간의 성능 향상
from itertools import imap

print list(imap(lambda x: x * x, xrange(10)))
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  • map()과 거의 비슷한 방식으로 작동

xrange vs range

# 10을 초과하는 숫자는 생성하지 않음
odd_numbers_to_10 = itertools.takewhile(lambda i: i <= 10, (x for x in xrange(1000) if x % 2))

# 1000개의 수가 담긴 큰 리스트를 생성
odd_numbers_tllo_10 = itertools.takewhile(lambda i: i <= 10, (x for x in range(1000) if x % 2))
from itertools import islice

for i in islice(range(10), 5):
    print(i)
# 0 1 2 3 4

for i in islice(range(100), 0, 100, 10):
    print(i)
# 0 10 20 30 40 50 60 70 80 90
from itertools import tee

i1, i2, i3 = tee(xrange(10), 3)
print(list(i1))        # [0,1,2,3,4,5,6,7,8,9]
print(list(i2))        # [0,1,2,3,4,5,6,7,8,9]
print(list(i3))        # [0,1,2,3,4,5,6,7,8,9]
print(list(i1))        # []
print(list(i2))        # []
print(list(i3))        # []

r = (x for x in range(10) if x < 6>)
i1, i2, i3 = tee(r, 3)
print(list(r))        # [0,1,2,3,4,5]
print(list(i1))        # []
print(list(i2))        # []
print(list(i3))        # []
  • 한 번 사용된 레퍼런스는 더 이상 값을 참조하지 않는다
  • 원본 제너레이터인 r을 실행시키면 나머지 복제본들도 다 참조가 끊어지는 특성이 있다
from itertools import cycle, izip
for number, letter in izip(cycle(range(2)), ['a','b','c','d','e']):
    print('{0}: {1}'.format(number, letter)
    # 0: a
    # 1: b
    # 0: c
    # 1: d
    # 0: e
from itertools import repeat
print(list(repeat('Hello, world!', 3)))
# ['Hello, world!', 'Hello, world!', 'Hello, world!']
from itertools import dropwile
print(list(dropwhile(lambda x: x < 10, [1,4,6,7,11,34,66,100,1])))
# [11, 34, 66, 100, 1]
from itertools import takewhile
print(list(takewhile(lambda x: x < 10, [1, 4, 6, 7, 11, 34, 66, 100, 1])))
# [1, 4, 6, 7]
from itertools import ifilter
print(list(ifilter(lambda x: x < 10, [1, 4, 6, 7, 11, 34, 66, 100, 1]))
# [1, 4, 6, 7, 1]
from operator import itemgetter
from itertools import groupby

attempts = [
    ('dan', 87),
    ('erik', 95),
    ('jason', 79),
    ('erik', 97),
    ('dan', 100)
]

# Sort the list by name for groupby
attempts.sort(key=itemgetter(0))

# Create a dictionary such that name: scores_list
print({key: sorted(map(itemgetter(1), value)) for key, value in groupby(attempts, key=itemgetter(0))})
# {'dan': [87, 100], 'jason': [79], 'erik': [95, 97]}
from collections import defaultdict

counts = defaultdict(list)
attempts = [('dan', 87), ('erik', 95), ('jason', 79), ('erik', 97), ('dan', 100)]

for (name, score) in attempts:
    counts[name].append(score)

print(counts)
# defaultdict(<type 'list'>, {'dan': [87, 100], 'jason': [79], 'erik': [95, 97]})