7 tips to Time Python scripts and control Memory & CPU usage

7 tips to Time Python scripts and control Memory & CPU usage

위 글의 번역.
파이썬 스크립트의 시간을 측정하고, 메모리와 CPU 사용량을 컨트롤하는 7가지 팁.

실행하는데 오랜 시간이 걸리는 복잡한 파이썬 프로그램을 돌릴 때, 이 실행시간 즉 퍼포먼스를 향상시키고 싶어진다. 그럼 어떻게 할 것인가?

먼저, 코드의 바틀넥을 찾아내는 툴이 필요하다. 그래야 그 부분을 향상시키는데에 집중할 수 있다.

그리고 또한, 메모리와 CPU 사용량을 컨트롤해야한다 - 그것이 퍼포먼스를 향상시킬 새로운 방법을 제시할 것이다.

그러므로, 이 포스트에서는 함수의 실행시간과 메모리 및 CPU 사용량을 측정하고 향상시킬 수 있는 7가지 툴을 소개할 것이다.

1. Use a decorator to time your functions

함수의 실행시간을 측정하는 데코레이터를 활용해라:

import time
from functools import wraps


def fn_timer(function):
    @wraps(function)
    def function_timer(*args, **kwargs):
        t0 = time.time()
        result = function(*args, **kwargs)
        t1 = time.time()
        print ("Total time running %s: %s seconds" %
               (function.func_name, str(t1-t0))
               )
        return result
    return function_timer

이제 이렇게 쓸 수 있다:

import random

@fn_timer
def random_sort(n):
    return sorted([random.random() for i in range(n)])


if __name__ == "__main__":
    random_sort(2000000)


# 결과:
Total time running random_sort: 1.41124916077 seconds

2. Using the timeit module

또다른 옵션은 timeit모듈을 사용하는 것이다. 이 모듈은 평균 시간을 측정해준다. 실행하기 위해서, 아래 커맨드를 터미널에서 쳐 보자:

$ python -m timeit -n 4 -r 5 -s "import timing_functions" "timing_functions.random_sort(2000000)"
...
4 loops, best of 5: 2.08 sec per loop

timing_functions는 우리가 만든 스크립트 이름이다. -n 4옵션으로 4번 실행해서 평균내고, -r 5옵션으로 각 테스트를 5번 반복하여 그중 best를 출력한다. 아래를 보면 확실히 알 수 있다:

# 이렇게 실행하면 2번 실행해서 평균내고,
$ python -m timeit -n 2 -r 1 -s "import timing_functions" "timing_functions.random_sort(2000000)"
Total time running random_sort: 1.89807391167 seconds
Total time running random_sort: 2.80085301399 seconds
2 loops, best of 1: 2.42 sec per loop

# 이렇게 실행하면 2번 실행해서 best를 보여준다.
$ python -m timeit -n 1 -r 2 -s "import timing_functions" "timing_functions.random_sort(2000000)"
Total time running random_sort: 1.94069004059 seconds
Total time running random_sort: 2.66689682007 seconds
1 loops, best of 2: 2.01 sec per loop

# timeit 모듈이 위에서 wrapping하기 때문에 시간 측정시 약간 차이는 있다.

-n-r을 지정하지 않는다면 default로 10 loops와 5 repetitions를 돌게 된다.

3. Using the time Unix command

그러나 이 decoratortimeit모듈은 둘다 파이썬에 기반한다. 이렇게 파이썬에 의지하지 않고도 시간측정을 할 수 있다:

$ time -p python timing_functions.py
Total time running random_sort: 1.88052487373 seconds
real 2.01
user 1.80
sys 0.20

출력의 첫줄은 데코레이터가 출력한 것이고, 두번째줄부터는:

  1. real은 이 스크립트를 실행하는데 걸린 총 시간을 의미한다.
  2. user는 이 스크립트를 실행하기 위해 CPU가 사용된 시간(CPU time spent)이다.
  3. sys는 kernel-level function에서 사용된 시간이다.

즉, 실제로 이 스크립트가 수행된 시간은 user시간이고 sys시간은 이 스크립트를 래핑하는 처리과정에 소모되는 시간이다. 그리고 real - (user + sys) 는 I/O나 다른 태스크의 종료까지 기다리는데 걸린 시간이라고 할 수 있다.

4. Using the cProfile module

만약 시간이 각각의 펑션과 메소드에서 얼마나 걸리는지 알고 싶다면, 그리고 각각이 몇번이나 불리는지 알고 싶다면 cProfile 모듈을 사용할 수 있다:

$ python -m cProfile -s cumulative timing_functions.py

Total time running random_sort: 4.75063300133 seconds
         2000067 function calls in 4.817 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.065    0.065    4.817    4.817 timing_functions.py:1(<module>)
        1    0.000    0.000    4.751    4.751 timing_functions.py:7(function_timer)
        1    1.698    1.698    4.751    4.751 timing_functions.py:18(random_sort)
        1    1.535    1.535    1.535    1.535 {sorted}
  2000000    1.433    0.000    1.433    0.000 {method 'random' of '_random.Random' objects}
        1    0.084    0.084    0.084    0.084 {range}
        1    0.001    0.001    0.001    0.001 random.py:40(<module>)
        1    0.000    0.000    0.000    0.000 hashlib.py:55(<module>)
        6    0.000    0.000    0.000    0.000 hashlib.py:94(__get_openssl_constructor)
        1    0.000    0.000    0.000    0.000 random.py:91(__init__)
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_md5}
        1    0.000    0.000    0.000    0.000 random.py:100(seed)
        1    0.000    0.000    0.000    0.000 {math.exp}
        1    0.000    0.000    0.000    0.000 {posix.urandom}
        1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)
        1    0.000    0.000    0.000    0.000 timing_functions.py:6(fn_timer)
        1    0.000    0.000    0.000    0.000 random.py:72(Random)
        1    0.000    0.000    0.000    0.000 functools.py:17(update_wrapper)
        2    0.000    0.000    0.000    0.000 {math.log}
        1    0.000    0.000    0.000    0.000 {function seed at 0x7f0a14976a28}
       11    0.000    0.000    0.000    0.000 {getattr}
        2    0.000    0.000    0.000    0.000 {time.time}
        7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)
        1    0.000    0.000    0.000    0.000 {math.sqrt}
        1    0.000    0.000    0.000    0.000 {binascii.hexlify}
        6    0.000    0.000    0.000    0.000 {globals}
        1    0.000    0.000    0.000    0.000 functools.py:39(wraps)
        3    0.000    0.000    0.000    0.000 {setattr}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha224}
        1    0.000    0.000    0.000    0.000 random.py:651(WichmannHill)
        1    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha384}
        1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha1}
        1    0.000    0.000    0.000    0.000 random.py:801(SystemRandom)
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha512}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha256}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

이렇게 각 펑션이 얼마나 불렸는지 디테일한 디스크립션을 볼 수 있다. 이 디스크립션은 누적시간(cumulative time spent)으로 정렬된다. 총 실행시간이 이전보다 높아진 걸 볼 수 있는데, 이는 위와 같이 디테일하게 측정하기 위해서 소모되는 측정시간이다.

5. Using line_profiler module

line_profiler는 우리 스크립트의 각 라인에 대해 CPU time spent를 측정한다. 먼저, 인스톨부터 하자.

$ sudo pip install line_profiler

이제, 스크립트에서 프로파일링을 하고 싶은 함수에 @profile데코레이터를 걸어주자. 어떤 import를 할 필요는 없다!

@profile
def random_sort2(n):
    l = [random.random() for i in range(n)]
    l.sort()
    return l

if __name__ == "__main__":
    random_sort2(2000000)

이제 이 스크립트를 이렇게 실행시켜주자:

$ kernprof -l -v timing_functions.py

-l 플래그는 line-by-line 분석을 의미하고, -v플래그는 verbose(장황한) output을 의미한다고 한다. -l을 빼면 @profile에서 에러가 나고, -v를 빼면 파일로 출력을 하는데 알 수 없는 인코딩이다. 즉, 그냥 둘 다 써주도록 하자 -.-;;

@profile을 빼고 -l을 빼면, 4번의 cProfile 모듈을 사용했을 때와 비슷한 결과가 나온다. 암튼 둘다 써줘야 원하는 결과를 얻을 수 있다 - 바로 이렇게:

$ kernprof -l -v timing_functions.py
Wrote profile results to timing_functions.py.lprof
Timer unit: 1e-06 s

Total time: 3.84659 s
File: timing_functions.py
Function: random_sort2 at line 22

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    22                                           @profile
    23                                           def random_sort2(n):
    24   2000001      2409084      1.2     62.6      l = [random.random() for i in range(n)]
    25         1      1437499 1437499.0     37.4      l.sort()
    26         1            4      4.0      0.0      return l

총 3.85초 중에서 랜덤 배열을 생성하는 데 62.6%의 시간이 소모되었으며 sort()함수는 37.4%의 시간을 소모한 것을 확인할 수 있다. 이 방법도 마찬가지로 시간 측정에 들어가는 오버헤드 때문에 스크립트 실행시간이 길어진다.

6. Use the memory_profiler module

memory_profiler모듈은 우리 스크립트의 메모리 사용량을 line-by-line으로 분석해준다. 그러나, 이 모듈은 프로그램을 더더욱 느리게 만든다.

먼저 두 모듈을 깔아주자:

$ sudo pip install memory_profiler
$ sudo pip install psutil

psutilmemory_profile의 성능을 향상시키기 위해 설치한다. 그리고 분석한다:

$ python -m memory_profiler timing_functions.py
Total time running random_sort2: 275.303025007 seconds
Filename: timing_functions.py

Line #    Mem usage    Increment   Line Contents
================================================
    22    9.801 MiB    0.000 MiB   @fn_timer
    23                             @profile
    24                             def random_sort2(n):
    25  134.113 MiB  124.312 MiB       l = [random.random() for i in range(n)]
    26  126.359 MiB   -7.754 MiB       l.sort()
    27  126.359 MiB    0.000 MiB       return l

해 보면 알겠지만 어마어마하게 오래 걸린다 -_- 분명 psutil은 깔았는데… 처음에 그냥 했다가 하도 오래 걸려서 시간을 측정해보고자 @fn_timer까지 달아서 다시 돌렸다. 아무튼 끝나긴 하니까 계속 돌려보면 위와 같은 결과를 얻을 수 있다. 메모리 사용량이 MiB(mebibyte1)로 측정된다.

7. Using the guppy package

마지막으로, 스크립트의 각 스테이지에서 어떤 오브젝트(str, tuple, dict …)들이 얼마나 생성되었는지를 트래킹 하고 싶을때 사용할 수 있는 패키지 guppy가 있다.

$ pip install guppy

설치 후 아래와 같은 코드를 추가하자:

from guppy import hpy


def random_sort3(n):
    hp = hpy()
    print "Heap at the beginning of the function\n", hp.heap()
    l = [random.random() for i in range(n)]
    l.sort()
    print "Heap at the end of the function\n", hp.heap()
    return l


if __name__ == "__main__":
    random_sort3(2000000)

그리고 이 코드를 실행시키면 아래와 같은 결과를 얻을 수 있다.

$ python timing_functions.py
Heap at the beginning of the function
Partition of a set of 27118 objects. Total size = 3433904 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  12302  45   979136  29    979136  29 str
     1   6136  23   495528  14   1474664  43 tuple
     2    323   1   250568   7   1725232  50 dict (no owner)
     3     76   0   228640   7   1953872  57 dict of module
     4   1704   6   218112   6   2171984  63 types.CodeType
     5    206   1   217424   6   2389408  70 dict of type
     6   1646   6   197520   6   2586928  75 function
     7    206   1   183272   5   2770200  81 type
     8    125   0   136376   4   2906576  85 dict of class
     9   1050   4    84000   2   2990576  87 __builtin__.wrapper_descriptor
<94 more rows. Type e.g. '_.more' to view.>
Heap at the end of the function
Partition of a set of 2027129 objects. Total size = 68213504 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0 2000083  99 48001992  70  48001992  70 float
     1    181   0 16803240  25  64805232  95 list
     2  12304   1   979264   1  65784496  96 str
     3   6135   0   495464   1  66279960  97 tuple
     4    329   0   252248   0  66532208  98 dict (no owner)
     5     76   0   228640   0  66760848  98 dict of module
     6   1704   0   218112   0  66978960  98 types.CodeType
     7    206   0   217424   0  67196384  99 dict of type
     8   1645   0   197400   0  67393784  99 function
     9    206   0   183272   0  67577056  99 type
<94 more rows. Type e.g. '_.more' to view.>

이렇게 원할 때 메모리의 heap 영역을 찍어볼 수 있다. 이를 통해서 우리 스크립트의 오브젝트 생성 및 삭제 플로우를 확인할 수 있다.


  1. mebibyte. MB(megabyte)가 종종 1024가 아니라 1000을 단위로 하기 때문에 문제가 된다. 이를 정확하게 1024를 단위로 한 것이 MiB이다. 즉 MiB가 MB의 Strict한 버전이라고 볼 수 있다. 마찬가지로 GiB, KiB 또한 존재한다. MiB ⊂ MB.

'Python' 카테고리의 다른 글

7 tips to Time Python scripts and control Memory & CPU usage  (0) 2014.12.03
제약을 넘어 : Gevent  (0) 2014.11.15
PyCon: 위대한 dict 이해하고 사용하기  (0) 2014.11.10
Python String Format Cookbook  (0) 2014.11.09
logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28

제약을 넘어 : Gevent

제약을 넘어 : Gevent

파이썬은 GIL 때문에 coroutine을 써야 한다. 파이썬만 그런게 아니라 하이레벨 언어는 대부분 그렇다는데, 자세히는 모르겠고… 아무튼 파이썬은 그렇다. gevent는 이 coroutine을 래핑한 라이브러리… 로 알고 있다. 자세한 건 이제 공부해보자.

pycon 2014 발표다.

Example

일단 예시부터 보고 들어간다.

def handle_request(s):
    try:
        s.recv(1024)
        s.send('HTTP/1.0 200 OK\r\n')
        s.send('Content-Type: text/plain\r\n')
        s.send('Content-Length: 5\r\n')
        s.send('\r\n')
        s.send('hello')
        s.close()
    except Exception, e:
        logging.exception(e)

def test():
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    s.bind(('0.0.0.0', 8000))
    s.listen(512)

    while True:
        cli, addr = s.accept()
        logging.info('accept ', addr)
        t = threading.Thread(target=handle_request, args=(cli, ))
        t.daemon = True
        t.start()

리퀘스트가 들어오면 정해진 리스폰스를 보내는 아주 간단한 서버다.
이 소스를, 단 두줄 만으로 성능을 두배로 향상시킬 수 있다!

from gevent.monkey import patch_all
patch_all()

Wow!

How?

컴퓨팅 작업은 크게 두가지로 나눌 수 있다.

  • CPU BOUND: CPU 작업이 수행시간의 주인 경우 - 압축, 정렬 …
  • I/O BOUND: I/O 작업이 수행시간의 주인 경우 - 네트워크, 디스크 …

즉 대부분의 Web App은 I/O BOUND다!

I/O BOUND라는 소리는 결국 CPU가 논다는 소리고, 우리는 이걸 더 잘 활용해야 할 의무가 있다. 놀면 아까우니까. 이를 해결하기 위한 방법이 크게 두가지가 있다.

  • 동시성(Concurrency)
  • 병렬성(Parallelism)

발표자는 이 두 개념을 차선과 버스를 이용해서 설명한다. 1차선에 버스로 사람을 수송하는데, 수송량을 늘리고 싶다면? 첫째, 버스의 수송인원을 늘린다. 둘째, 차선을 늘린다. 첫번째 방법이 동시성이고, 두번째 방법이 병렬성이다. 병렬성은 멀티쓰레딩을 의미하며 차선을 늘린다는 건 쓰레드를 늘린다는 것을 의미한다. 뭐, 그다지 와닿지는 않는 설명이다.

아무튼, 파이썬은 GIL 때문에 멀티쓰레딩은 별로 좋지 않다. 즉, 파이썬의 특성상 병렬성은 버리고 동시성에 집중해야 한다.

Gevent

그럼 어떻게 동시성을 향상시키는가? node.js를 생각하면 간단하다. 이벤트가 발생하면, 이를 처리하다가 I/O 작업이 발생하면 이를 커널에 요청하고 다른 이벤트를 처리한다. 커널은 I/O 작업을 끝내면 콜백을 통해 I/O 작업이 끝났음을 알린다. 우리는 그럼 이 콜백에서 작업을 마무리하면 된다. 즉, 원래는 I/O 작업을 할 때 커널의 리스폰스를 계속 기다리고 있는데 그 시간에 다른 이벤트를 처리하게 하여 CPU를 더 활용하겠다는 거다.

파이썬에서 그렇게 하기 위한 라이브러리가 바로 gevent다. gevent는 scheduler + event loop로 구성된 라이브러리다.



greenlet

scheduler. micro-thread이고, coroutine을 이용하여 implicit scheduling을 하지 않는다.

coroutine?
함수가 끝나지 않아도 제어권을 넘길 수 있는 기능.

-> Cooperative multitasking
명시적인 스케줄링

libdev

event-loop. 현재 시점에서 가장 적절한 이벤트를 선택한다.
이벤트 루프란?

while True:
    events = wait_for_events()
    for event in events:
        handle_event(event)

동시성을 향상시키기 위해 이벤트를 처리하다가 I/O 작업이 있으면 커널에 넘기고 다른 이벤트를 처리하고, 이러기 위해서 필요한 게 바로 이벤트 루프다. 이벤트 루프는 커널과 프로세스의 중간에서 이를 가능하게 해 준다. 원래 I/O 작업을 요청하면 리스폰스를 기다려야 하는게 정석이지만, 커널에서 이러한 이벤트 구조를 지원하기 때문에 이벤트 루프는 그런 커널의 기능을 이용한다.

monkey_patch()

위 예시에서 멍키패치란걸 하는데, 이 멍키패치는 기존의 소켓이라던가 I/O 작업을 하는 애들을 non-blocking으로 바꿔준다. 즉, 아래에서 보듯이 sock.recv가 gevent를 고려하여 만들어진게 아니기 때문에 이를 gevent와 함께 사용할 수 있도록 바꾸어 주는 것이다.

예를 들면, 소켓의 sock.recv를 이렇게 바꾼다:

def recv(self, *args):
    while True:
        try:
            return sock.recv(*args)
        except error as ex:
            if ex.args[0] != EWOULDBLOCK:
                raise
            self._wait(self._read_event)

원래는 sock.recv에서 block이 걸려서 기다리는데, 이를 non-blocking으로 바꿔, block이 걸리면 error로 잡힌다. 그럼 아래에서 어떤 error인지 검사하고, 그게 EWOULDBLOCK, 즉 block이 걸렸다는 _wait함수를 호출하여 이벤트를 기다린다.

참고

위에서는, 즉 세션에서는 저 두 줄만 추가하면 된다고 했지만 실제로 그렇진 않다. gevent를 명시적으로 호출해 주어야 한다. 위 예시에서는 어떻게 되는지 잘 모르겠지만, 아무튼 일반적으로 그렇다. 30분만에 따라하는 동시성 웹 스크래퍼라는 세션도 들었는데, 거기서는 gevent.pool을 사용한다. pool이든 뭐든, 아무튼 gevent를 호출하여 nonblocking을 명시해 주어야 한다.

blocking

# blocking
...
talks = [talk_from_page(url) for url in talk_links]

non-blocking

# non-blocking
from gevent.monkey import patch_all; patch_all()
from gevent.pool import Pool
...
pool = Pool(20)
talks = pool.map(talk_from_page, talk_links)

flask?

그럼 flask에는 어떻게 적용해야 하는가? 여러 리퀘스트를 non-blocking으로 동시에 때리는 방법은 알겠는데, 플라스크에서 적용하려면 요청이 들어올 때마다 각각 따로 non-blocking으로 때려 줘야 한다. 인터넷을 뒤지다 보니 별별 내용이 많아서 따로 공부하기로 한다.

'Python' 카테고리의 다른 글

7 tips to Time Python scripts and control Memory & CPU usage  (0) 2014.12.03
제약을 넘어 : Gevent  (0) 2014.11.15
PyCon: 위대한 dict 이해하고 사용하기  (0) 2014.11.10
Python String Format Cookbook  (0) 2014.11.09
logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28

PyCon: 위대한 dict 이해하고 사용하기

위대한 dict 이해하고 사용하기

파이콘 2014에서 발표되었던 내용이다. 좋은 내용이 많은 것 같아서 몇 가지만 정리한다. 슬라이드가 잘 되어 있으니 슬라이드를 살펴보길 추천한다. 동영상도 있고.

basic usage

dict의 기본적인 활용예

comprehension

>>> a
{'a': 1, 'c': 3, 'b': 2}
>>> {v: k for k, v in a.items()}
{1: 'a', 2: 'b', 3: 'c'}

위와 같이 dict comprehension은 { }로 한다. list는 [ ]다.

get()

dict에서 key-value확인은 몇가지 방법이 있다.

>>> a['a']
1
>>> a['d']
KeyError: 'd'
>>> 'a' in a
True
>>> 'd' in a
False
>>> a.get('a')
1
>>> a.get('d')
None
>>> a.get('d', 0)
0

key가 존재하는지 in으로 확인할 수 있다. value를 가져오려면 dict[key]를 쓰던가, dict.get(key) 로 가져올 수 있다. dict[key]는 key가 없으면 에러가 나고, get은 None을 리턴한다. 위에서 보듯 없을때의 리턴값을 정해줄 수 있다.

이를 활용하면 아래와 같은 게 가능하다.

# a['d'] += 1 이 하고 싶은데, 'd'가 있는지 없는지 모른다면?

>>> a['d'] = a.get('d', 0) + 1
>>> a
{'a': 1, 'c': 3, 'b': 2, 'd': 1}

dict[key] += 1은 상당히 자주 사용한다.

setdefault()

말 그대로 default를 세팅하는 함수다.

{'a': 1, 'c': 3, 'b': 2}
>>> a.setdefault('d', 0)
0
>>> a.setdefault('a', 0)
1
>>> a
{'a': 1, 'c': 3, 'b': 2, 'd': 0}

key가 없으면 value에 default를 넣고, value를 리턴한다. 이를 이용해서도 dict[key] += 1문제를 해결할 수 있다. get()과 비슷한 것 같지만 슬라이드를 보면 다르게 활용하는 예가 나온다. 참고하자.

삭제

>>> del a['d']
>>> a
{'a': 1, 'c': 3, 'b': 2}

순회

>>> a.keys()
['a', 'c', 'b']
>>> [k for k in a.keys()]
['a', 'c', 'b']
>>> a.values()
[1, 3, 2]
>>> a.items()
[('a', 1), ('c', 3), ('b', 2)]

iterkeys(), itervalues(), iteritems() 도 있다. 아마 range()와 xrange()의 차이일 것이다.

understand

dict는 hash 기반으로 작동한다. 이건 뻔하지만, 슬라이드를 보면 더 깊숙히 들어간다. 여유가 있다면 동영상을 보도록 하자. 아무튼 결론은, dict는 (거의) O(1)이다.

dictlike classes

dict의 확장인지 뭔지 아무튼 dict스러운 클래스들이 있다.

  • collections.OrderedDict
  • collections.defaultDict
  • collections.Counter
  • shelve.Shelf

OrderedDict

>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d['a'] = 1
>>> d['b'] = 2
>>> d['c'] = 3
>>> d['d'] = 4
>>> d.items()
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]

순서가 있는 dict.

defaultDict

>>> from collections import defaultdict
>>> d = defaultdict(lambda: [])
>>> d['a'].append(1)
>>> d['a']
[1]
>>> d['b']
[]

dict의 default값을 지정해줄 수 있다. setdefault()를 dict 전체에 적용한 것. 이를 활용하면 2차원 dict도 쉽게 만들 수 있고 그 이상도 가능하다.

>>> a = defaultdict(lambda: defaultdict(list))
>>> a['hi']['hoi'] = 'hey'
>>> a
defaultdict(<function <lambda> at 0x10058da28>, {'hi': defaultdict(<type 'list'>, {'hoi': 'hey'})})

이렇게 신박한 것도 가능하다!

>>> infinite_dict = lambda: defaultdict(infinite_dict)
>>> inf = infinite_dict()
>>> inf['a'] = 1
>>> inf['b']['c'] = 2
>>> inf['c']['d']['e'] = 3
>>> inf
defaultdict(<function <lambda> at 0x10058daa0>, {'a': 1, 'c': defaultdict(<function <lambda> at 0x10058daa0>, {'d': defaultdict(<function <lambda> at 0x10058daa0>, {'e': 3})}), 'b': defaultdict(<function <lambda> at 0x10058daa0>, {'c': 2})})

신박신박.

shelve.Shelf

shelve는 iOS의 UserDefaults같은 기능이다. 데이터를 쉽게 디스크에 저장하여 보존할 수 있도록 도와준다.

>>> from shelve import open
>>> shelf = open('test') # test.db에 dict를 기록한다
>>> shelf['hello'] = 1
>>> shelf['hi'] = 2
>>> shelf[1] = 3
TypeError: dbm mappings have string indices only
>>> shelf.close()
  • close() 를 해야 한다: with와, context manager인 contextlib.closing을 사용하자.
  • 문자열 키만 가능하다: 위에서도 확인할 수 있다.
  • test.db에 저장된다: 실제로 test.db가 생겨 거기에 저장된다.

'Python' 카테고리의 다른 글

7 tips to Time Python scripts and control Memory & CPU usage  (0) 2014.12.03
제약을 넘어 : Gevent  (0) 2014.11.15
PyCon: 위대한 dict 이해하고 사용하기  (0) 2014.11.10
Python String Format Cookbook  (0) 2014.11.09
logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28

Python String Format Cookbook

Python String Format Cookbook

자세한 건 위 링크를 참고.

Order % style

Python 2.6에서 str.format()이 등장했다. 이전에는 %를 이용해서 출력했는데 이러한 방식의 단점을 보완한 것 같다. 이전 방식은 아래와 같다:

>>> "%s %s" % ("hi", "hoi")
'hi hoi'

Stackoverflow: Python string formatting: % vs. .format 이 링크에 가 보면 %방식의 단점을 알 수 있다.

str.format

그래서 str.format()을 소개한다.

Basic

>>> "glazed with {} water beside the {} chickens".format("rain", "white")
'glazed with rain water beside the white chickens'
>>> " {0} is better than {1} ".format("emacs", "vim")
' emacs is better than vim '
>>> " {1} is better than {0} ".format("emacs", "vim")
' vim is better than emacs '

이렇게 그냥 차례대로 쓸 수도, 순서를 지정할 수도 있다.

Not Enough Arguments

>>> " ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}) ".format(1,2,3,4,5,6,7,8,9,10)
' (1, 2, 3, 4, 5, 6, 7, 8) '
>>> " ({}, {}, {}, {}, {}, {}, {}, {}) ".format(1,2,3,4,5,6,7,8,9,10)
' (1, 2, 3, 4, 5, 6, 7, 8) '

Order % style과 다르게 개수가 서로 맞지 않아도 상관없다. 물론, 출력하려고 하는 변수는 다 있어야 출력이 가능하다.

Named Arguments

>>> " I {verb} the {object} off the {place} ".format(verb="took", object="cheese", place="table")
' I took the cheese off the table '

이렇게 이름을 지정할 수도,

Reuse Same Variable

>>> "Oh {0}, {0}! wherefore art thou {0}?".format("Romeo")
'Oh Romeo, Romeo! wherefore art thou Romeo?'

이렇게 하나의 변수를 여러번 출력할 수도 있다.

Base Conversion

>>> "{0:d} - {0:x} - {0:o} - {0:b} ".format(21)
'21 - 15 - 25 - 10101 '

이렇게 형변환도 가능하다. 차례로 10진수, 16진수, 8진수, 2진수.

Use Format as a Function

>>> email_f = "Your email address was {email}".format
>>> email_f(email="bob@example.com")
'Your email address was bob@example.com'

무려 이런 것도 가능하다. 함수로 만들어서 활용한다.

Escaping Braces

>>> " The {} set is often represented as {{0}} ".format("empty")
' The empty set is often represented as {0} '

{ }를 출력하고 싶다면? 두번 써 주자.

Number Formatting

NUMBER FORMAT OUTPUT DESCRIPTION
3.1415926 {:.2f} 3.14 2 decimal places
3.1415926 {:+.2f} +3.14 2 decimal places with sign
-1 {:+.2f} -1.00 2 decimal places with sign
2.71828 {:.0f} 3 No decimal places
5 {:0>2d} 05 Pad number with zeros (left padding, width 2)
5 {:x<4d} 5xxx Pad number with x’s (right padding, width 4)
10 {:x<4d} 10xx Pad number with x’s (right padding, width 4)
1000000 {:,} 1,000,000 Number format with comma separator
0.25 {:.2%} 25.00% Format percentage
1000000000 {:.2e} 1.00e+09 Exponent notation
13 {:10d}           13 Right aligned (default, width 10)
13 {:<10d} 13 Left aligned (width 10)
13 {:^10d}      13 Center aligned (width 10)


'Python' 카테고리의 다른 글

제약을 넘어 : Gevent  (0) 2014.11.15
PyCon: 위대한 dict 이해하고 사용하기  (0) 2014.11.10
Python String Format Cookbook  (0) 2014.11.09
logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28
contextlib  (0) 2014.10.12

logging

logging

파이썬에는 기본적으로 지원하는 강력한 로그 모듈이 있으니 바로 logging이다. 자바의 log4j와 비슷하다고 하는데 써본적이 없어서 모르겠다. 안드로이드의 로그 남기는 방식과 비슷하다.

별 내용은 없어서 포스팅을 할까 고민했지만 간단히 남겨 둔다.

usage

import logging
import logging.handlers

# file handler
# fh = logging.FileHandler("logtest.log")
fh = logging.handlers.RotatingFileHandler("logtest.log", maxBytes=1024, backupCount=10)
fh.setLevel(logging.INFO)

# console handler
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# formatter = logging.Formatter("%(message)s") # default.
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)

logger = logging.getLogger("logger_name")
logger.addHandler(fh)
logger.addHandler(ch)
logger.setLevel(logging.INFO)

logger.info("test")
logger.info("why")

간단하게, loggerhandlerformatter를 부착해서 사용하는 구조다. 핸들러는 여러개를 붙일 수 있어, 콘솔과 파일에 동시에 출력할 수 있을 뿐만 아니라 여러 파일에 출력도 가능하다. formatter는 출력 포맷인데, 붙이지 않으면 메시지만 출력된다.

핸들러마다 로깅 레벨 설정을 달리할 수 있으며, 로거에도 레벨 설정이 가능하다. 로거에서 먼저 레벨로 필터링하고, 그 후에 각 핸들러마다 레벨을 체크하여 로깅한다. 즉, 로거 레벨에 맞지 않으면 핸들러와 상관없이 로깅되지 않는다.

level

Level Numeric value
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

로깅 레벨. 각 로깅시에 레벨을 잘 설정해 주면, 로거의 레벨을 달리하는것만으로 적절한 로그를 얻을 수 있다.

참고

Python 에서 Log 남기는 팁
위에서 사용한 RotatingFileHandler를 비롯하여 몇가지 유용한 팁이 있다. 참고하자.

docs: Logging Cookbook

'Python' 카테고리의 다른 글

PyCon: 위대한 dict 이해하고 사용하기  (0) 2014.11.10
Python String Format Cookbook  (0) 2014.11.09
logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28
contextlib  (0) 2014.10.12
Errors and Exceptions  (0) 2014.10.09

setup.py vs requirements.txt

setup.py vs requirements.txt

이 포스트는 위 링크의 번역 + 요약 + @ 이다.

setup.pyrequirements.txt간에는 수많은 오해들이 있다. 많은 사람들이 이 두가지가 중복된다고 생각한다.

Python Libraries

이 포스트에서 파이썬 라이브러리란 이미 디플로이되고 릴리즈되어 공개된 라이브러리를 의미한다. PyPI에서 이러한 라이브러리들을 찾아볼 수 있다. 라이브러리는 제대로 배포되기 위해서 여러 메타데이터를 가지고 있다. 이름이라던가, 버전, 디펜던시 등이 바로 그것이다. setup.py는 바로 이런 메타데이터에 대한 명세다:

from setuptools import setup

setup(
    name="MyLibrary",
    version="1.0",
    install_requires=[
        "requests",
        "bcrypt",
    ],
    # ...
)

헌데, 위 명세에서 제공하는 디펜던시에는 라이브러리를 가져올 url이라던가, 각 라이브러리의 버전 등이 명시되어 있지 않다. 이는 굉장히 중요한 정보이며 따라서 위와 같은 디펜던시 명세는 abstract dependencies라 부른다(정확하게는 글의 저자가 그렇게 부른다).

Python Applications

여기서 파이썬 어플리케이션이란 네가 deploy할 그것이다. 그건 PyPI에 존재할 수도 있고 아닐수도 있지만, 재사용할만한게 많지는 않은 그런 것이다. PyPI에 존재하지 않는 어플리케이션은 명확한 deploy config file이 필요하다. 이 섹션에서는 바로 그 deploy specific한 부분을 다룬다.

일반적으로 어플리케이션은 디펜던시들을 가지고, 종종 이 디펜던시는 굉장히 복잡하다. 디플로이된 특정한 인스턴스들은, 보통 이름도 없고 어떤 다른 패키징 메타데이터가 전혀 없다. 이는 pip requirement file에 반영된다. requirement file은 보통 이렇게 생겼다:

# This is an implicit value, here for clarity
--index-url https://pypi.python.org/simple/

MyPackage==1.0
requests==1.2.0
bcrypt==1.0.2

이렇게 각 정확한 버전과 함께 디펜던시 명세를 작성한다. 라이브러리는 최신 버전을 사용하는 경향이 있는 반면 어플리케이션은 특정한 버전을 필요로 하는 경향이 있다. 예를 들어 requests의 어떤 버전을 사용하는지는 중요하지 않지만 production에서 사용하는 버전과 development에서 사용하는 버전은 같아야 한다.

위 파일에서 —index-url https://pypi.python.org/simple/를 볼 수 있다. PyPI를 사용하는 일반적인 requirements.txt에서는 이렇게 명확하게 표시하지 않는다. 그러나 이건 requirements.txt에서 중요한 부분이다. 이 한 줄이 abstract dependencyconcrete dependency로 바꾼다.

그래서, Abstract건 Concrete건 무슨 상관인데?

이 차이 - abstract냐 concrete냐 - 는 매우 중요하다. 이것은 회사가 PyPI같은 형태의 프라이빗 패키지 인덱스를 구축할 수 있게 해준다. 심지어 네가 라이브러리를 fork했다면 이 라이브러리를 사용할 수 있게도 해준다. abstract dependency는 오직 이름과 버전만 사용하기 때문에 너는 PyPI를 통해서나, 또는 Crate.io를 통해서나, 또는 너의 파일시스템으로부터 인스톨할 수 있다. 나아가 알맞은 이름과 버전만 명시한다면 라이브러리를 포크하고, 코드를 변경하여도 문제없이 사용할 수 있다.

abstract requirement를 사용해야 하는 곳에 concrete requirement를 사용할 때 나타나는 극단적인 문제는 Go에서 찾아볼 수 있다. Go에서는 import에서 아래와 같이 url을 사용할 수 있다:

import (
    "github.com/foo/bar"
)

보다시피 디펜던시를 위해 정확한 url을 사용할 수 있다. 이 상황에서, bar라이브러리에 버그가 있어 이를 포크하여 수정하여 사용한다면, bar만 포크하면 되는 것이 아니라 bar에 관련된 모든 라이브러리를 포크해야 한다. 단지 bar의 작은 변경을 위해서.

A Setuptools Misfeature

Setuptools에도 Go와 비슷한 특징이 있다. dependency_links라는 것이다:

from setuptools import setup

setup(
    # ...
    dependency_links = [
        "http://packages.example.com/snapshots/",
        "http://example2.com/p/bar-1.0.tar.gz",
    ],
)

setuptools의 특징은 이 디펜던시의 abstractness를 파괴한다. 이제 여기에도 위 Go의 문제와 같은 문제가 발생한다.

Developing Reusable Things or How Not to Repeat Yourself

라이브러리와 어플리케이션의 차이는 분명하지만, 네가 라이브러리를 개발한다면 그건 네 어플리케이션의 일부일 것이다. 특정한 로케이션에서, 특정한 디펜던시를 원한다면 setup.pyabstract dependency를, requirements.txtconcrete dependency를 넣어야 한다. 그러나 이 두 분리된 리스트를 관리하는게 싫다면 어떻게 해야 할까? requirements.txt에서는 이러한 케이스를 위한 기능을 지원한다. setup.py가 있는 디렉토리에 아래와 같이 requirements.txt를 만들 수 있다:


--index-url https://pypi.python.org/simple/

-e .

이제, pip install -r requirements.txt를 하면 이전과 동일하게 작동한다. file path . 에 있는 setup.py를 찾아, 거기에 있는 abstract dependency를 requirement파일의 --index-url과 결합시켜 concrete dependency로 바꿔 인스톨한다.

이 방식은 강력한 장점을 갖는다. 네가 조금씩 개발하고 있는 여러 라이브러리가 있다고 하자. 또는 하나의 라이브러리를 여러 부분으로 잘라 사용하고 있다고 하자. 어떻든 그 라이브러리의 공식 릴리즈 버전과는 다른 development version을 사용하고 있는 것이다. 그렇다면 requirements.txt를 이렇게 쓸 수 있다:

--index-url https://pypi.python.org/simple/

-e https://github.com/foo/bar.git#egg=bar
-e .

먼저 명시된 url인 github 으로부터 bar 라이브러리를 설치한다. 그리고 나서 --index-url로부터 나머지 라이브러리들을 설치한다. 이 때 bar에 대한 디펜던시는 이미 해결되었기 때문에 추가적인 인스톨을 하지 않는다. 즉, bar라이브러리에 대한 development version을 사용할 수 있는 것이다.

'Python' 카테고리의 다른 글

Python String Format Cookbook  (0) 2014.11.09
logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28
contextlib  (0) 2014.10.12
Errors and Exceptions  (0) 2014.10.09
decorator (2) - extension  (0) 2014.10.06

contextlib

contextlib

데코레이터와 마찬가지로 파이썬에서 지원하는 강력한 기능중 하나. 파이썬을 쓰다보면 with라는 키워드를 볼 수 있는데, 이에 관련된 라이브러리가 바로 contextlib이다. with는 시작과 끝이 있는 경우에 사용하는데, file이나 database처럼 open 또는 connect 후 close가 필요한 경우가 대표적이다.

intro

import time

class demo:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_ty, exc_val, exc_tb):
        end = time.time()
        print('{}: {}'.format(self.label, end - self.start))

with demo('counting'):
    n = 10000000
    while n > 0:
        n -= 1

# counting: 1.36000013351

보면 알겠지만 __enter__with문이 시작할 때, __exit__는 끝날 때 실행된다.

이제 이걸 contextlib을 사용해서 간단하게 바꿀 수 있다.

from contextlib import contextmanager
import time

@contextmanager
def demo(label):
    start = time.time()
    try:
        yield
    finally:
        end = time.time()
        print('{}: {}'.format(label, end - start))

with demo('counting'):
    n = 10000000
    while n > 0:
        n -= 1

# counting: 1.32399988174

yieldwith문이 감싸는 코드를 실행시킨다. yield를 통해서 오브젝트를 전달할 수 있다.

closing

앞에서 말한 것처럼, with문은 close와 함께 많이 활용되고 이를 위해 closing이란 게 있다.

from contextlib import closing
import MySQLdb

con = MySQLdb.connect("host", "user", "pass", "database")
with closing(con.cursor()) as cur:
    cur.execute("somestuff")
    results = cur.fetchall()

    cur.execute("insert operation")
    con.commit()

con.close()

db connect할 때 이렇게 많이 쓰이는 것 같다.

apply

from contextlib import contextmanager

@contextmanager
def mysql_connect():
    con = mdb.connect(host, user, passwd, db)
    cur = con.cursor()

    try:
        yield (con, cur)
    finally:
        cur.close()
        con.close()

with mysql_connect() as (con, cur):
        cur.execute("""SELECT * FROM USER""")
        tu = cur.fetchall()
        con.commit()

이렇게 적용하는 것이 가장 심플해 보인다. yield의 용법과 2개 이상의 오브젝트를 전달하는 방법도 참고하자.

참고

Python - 수준 있는 디자인 패턴 (Advanced Design Patterns in Python)

python-docs: 27.7. contextlib — Utilities for with-statement contexts : 공식 문서
MySQLdb & closing

'Python' 카테고리의 다른 글

logging  (0) 2014.10.29
setup.py vs requirements.txt  (0) 2014.10.28
contextlib  (0) 2014.10.12
Errors and Exceptions  (0) 2014.10.09
decorator (2) - extension  (0) 2014.10.06
decorator와 closure  (2) 2014.10.04

Errors and Exceptions

python: Errors and Exceptions

자바를 제대로 공부한 적이 없어서, try-catch 형태의 패턴에 익숙하지 않다. 물론 try catch가 자바에서만 쓰는 건 아니지만 지금까지 다른 언어로 개발하면서 딱히 필요성을 느낀 적이 없었다. 에러가 나면 고치면 되는 일이고.

헌데 파이썬에선 많이 쓰는, 써야 하는 것으로 보인다.

try-except

파이썬에서는 try except 형태다. try에서 에러가 나면, except에서 받는다.

try:
    ret = db.users.insert({
        '_id': 1,
        'name': 'cjb'
        })

    print "성공? ", ret
except pymongo.errors.DuplicateKeyError, e:
    print "중복키 에러", e
except:
    print "원인을 알수 없셤 : " + str(sys.exc_info())

mongodbinsert를 하고 에러가 나면 그에 따른 처리를 하는 코드다. 첫번째 except와 같이 에러를 명시해 주면 해당 에러만 받아들이며, 뒤에 에러 변수를 지정해 주면 에러 내용을 받아볼 수 있다[^1].

두번째 except는 그 외의 모든 에러를 받는데, 위와 같이 sys.exc_info()를 출력하여 에러 내용을 볼 수 있다.

raise

except가 있다면 throw도 있어야 인지상정. 파이썬에서는 raise라 한다.

def test_raise():
    raise NameError("whynot")

if __name__ == "__main__":
    try:
        test_raise()

        print "성공? ", ret
    except NameError, e:
        print "네임에러", e
    except:
        print "원인을 알수 없셤 : " + str(sys.exc_info())

이렇게 에러를 명시적으로 발생시킬 수 있다. 위와 같이 하면 e에 “whynot”이 들어간다.

[^1]: python 2 기준. python 3에서는 , e 대신에 as e 라고 쓴다.

Exception as e

이 글을 쓰고 난 지 한참이 지나서야 Exception으로 받을 수 있다는걸 알게 되었다 -_-;;
참고로 지금까지의 소스는 python 2 기준이었지만 아래는 python 3이다.

import sys

def test_raise():
    raise NameError("whynot")

if __name__ == "__main__":
    try:
        test_raise()
    except Exception as e:
        print("[Exception] {}".format(e))
        print("[sys.exc_info()] {}".format(str(sys.exc_info())))

// [Exception] whynot
// [sys.exc_info()] (<class 'NameError'>, NameError('whynot',), <traceback object at 0x0000000002573888>)

일반적인 경우에는 Exception as e를 쓰자.

'Python' 카테고리의 다른 글

setup.py vs requirements.txt  (0) 2014.10.28
contextlib  (0) 2014.10.12
Errors and Exceptions  (0) 2014.10.09
decorator (2) - extension  (0) 2014.10.06
decorator와 closure  (2) 2014.10.04
String option - u와 r  (0) 2014.09.21

decorator (2) - extension

decorator (2) - extension

확장이라고 하기에는 별 거 없지만, 실제로 적용하면서 알게된 몇가지 더 정리해 보았다.
@wraps는 항상 적용해 주는 게 좋은 것 같고, 그 이외에는 적용하면서 알아가면 되는 부분이지 미리 학습할 필요까진 없어 보인다.

@wraps

플라스크에 데코레이터를 적용하려고 찾아봤더니, @wraps라는 게 보인다. 이게 뭐지?

from functools import wraps

def without_wraps(func):
    def __wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return __wrapper

def with_wraps(func):
    @wraps(func)
    def __wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return __wrapper


@without_wraps
def my_func_a():
    """Here is my_func_a doc string text."""
    pass

@with_wraps
def my_func_b():
    """Here is my_func_b doc string text."""
    pass

'''
# Below are the results without using @wraps decorator
print my_func_a.__doc__
>>> None
print my_func_a.__name__
>>> __wrapper

# Below are the results with using @wraps decorator
print my_func_b.__doc__
>>> Here is my_func_b doc string text.
print my_func_b.__name__
>>> my_func_b
'''

위 코드를 찬찬히 읽어 보면 뭔지 알 수 있다. 기존 함수를 데코레이터로 래핑하게 되면, 데코레이트된 함수의 속성을 요청하면 기존 함수의 속성이 나오는 게 아니라 데코레이트된 래퍼의 속성이 나오는 것이다. @wraps를 사용하면 이 문제를 해결할 수 있다.

pass parameter on func

래퍼인 데코레이터에서 감싸진 원 함수로 파라메터를 넘기고 싶다면?
kwargs로 넘기면 된다.

from inspect import getargspec

# decorator.
def usertoken_required(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        user_token = request.headers.get('user_token')
        cur.execute("""SELECT * FROM USER WHERE user_token='%s'""" % user_token)
        user = cur.fetchone()

        if user is None:
            return Response(response="user_token is wrong!", 
                            status=401)

        argspec = getargspec(func)
        if argspec[2] is not None: # kwargs를 받는 함수에게만 전달
            kwargs['user_info'] = user

        return func(*args, **kwargs)

    return decorated


@app.route('/')
@usertoken_required
def hello_world(**kwargs):
    return 'Hello,' + request.headers.get('user_token') + ' user:' + str(kwargs['user_info'])

usertoken을 검사하는 데코레이터를 만들었는데, 데코레이터에서 찾은 유저 정보를 버리기가 아까워 kwargs에 담아 넘겼다. 감싸지는 원 함수인 func에서 kwargs를 받지 않는 경우를 대비해서, getargspec으로 args의 스펙을 받아 kwargs를 받는 경우에만 user_info를 넘기도록 했다.

more in flask: return

플라스크에서 데코레이터를 찾아보면 적절한 예제 페이지가 하나 나온다.

from functools import wraps
from flask import g, request, redirect, url_for

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function


@app.route('/secret_page')
@login_required
def secret_page():
    pass

이 외에도 링크에 들어가 보면 실제로 데코레이터를 어떻게 사용하는지 적절한 예제들이 잘 나와 있다. 데코레이터를 쓸 생각이면 한번 살펴보도록 하자.

위 예제에서는 return 이 있는 함수를 데코레이트 하는 방법을 볼 수 있다.

decorator with parameter

마찬가지로 위 플라스크 페이지에서 가져온 예제다. flask@app.route처럼, 데코레이터에 파라메터를 넘겨서 처리하고 싶다면?

from functools import wraps
from flask import request

def templated(template=None):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            template_name = template
            if template_name is None:
                template_name = request.endpoint \
                    .replace('.', '/') + '.html'
            ctx = f(*args, **kwargs)
            if ctx is None:
                ctx = {}
            elif not isinstance(ctx, dict):
                return ctx
            return render_template(template_name, **ctx)
        return decorated_function
    return decorator

@app.route('/')
def index():
    return render_template('index.html', value=42)

@app.route('/')
@templated('index.html')
def index():
    return dict(value=42)

@app.route('/')
@templated()
def index():
    return dict(value=42)

이렇게 한번 더 감싸줌으로써 처리할 수 있다.

참고

왜 파이썬 데코레이터를 만들때, @wraps어노테이션을 쓰는 것을 권장하는 걸까?
Flask: View Decorators

'Python' 카테고리의 다른 글

contextlib  (0) 2014.10.12
Errors and Exceptions  (0) 2014.10.09
decorator (2) - extension  (0) 2014.10.06
decorator와 closure  (2) 2014.10.04
String option - u와 r  (0) 2014.09.21
한글 in the dictionary (feat. pretty)  (0) 2014.09.17

decorator와 closure

decorator

데코레이터는 파이썬의 강력한 문법 중 하나다. 파이썬에 입문해서 이것저것 좀 하다 보면 여기저기서 많이 볼 수 있다. 데코레이터를 한 마디로 정리하자면, 함수를 래핑하여 앞뒤에서 전처리와 후처리를 하는 기능 이라고 할 수 있다. 파이썬에서는 이 데코레이터 기능을 간편하게 지원한다.

글이 쓸데없이 장황해졌는데, 윗 부분은 개념 설명이고 in python부터 보아도 무방하다.

function: first class object

파이썬에서, 함수는 first class 오브젝트다. 다시 말해 변수와 함께 동등한 레벨의 객체로 취급된다. 따라서 우리는 자유자재로 변수처럼 함수를 인자로 넘길 수 있다. 대표적으로 sorted함수를 사용할 때를 생각해보자.

>>> student_tuples = [
        ('john', 'A', 15),
        ('jane', 'B', 12),
        ('dave', 'B', 10),
]
>>> sorted(student_tuples, key=lambda student: student[2])   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

closure

파이썬은 function closure를 지원한다.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo()
1
>>> foo.func_closure
(<cell at 0x...: int object at 0x...>,)

즉 이런 경우다. foo는 inner를 리턴받았고, inner에서 사용하는 x는 inner 바깥의 outer에 있기 때문에 foo를 호출하는 시점에서는 x가 존재하지 않아야 한다. 그런데, 위에서 보이듯이 잘 실행된다. 이게 바로 function closure다. 펑션 클로저는 그 함수가 정의될 때 자신을 감싸고 있는 namespace가 어떻게 생겼는지 기억한다는 의미다. foo의 func_closure로 그 함수를 감싸고 있는 scope의 변수들을 볼 수 있다.

간단하게 말하면, 어떠한 함수를 객체로 받을 때 그 함수를 감싸는 scope의 변수들 또한 같이 가져간다는 의미다. 따라서 이러한 것도 가능하다:

>>> def outer(x):
...     def inner():
...         print x # 1
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

decorator

데코레이터는, 결론부터 말하자면, 이름 그대로 함수를 데코레이트 해준다. 함수를 인자로 받아 꾸며주는 기능을 지원한다. 바꿔 말하면 함수를 래핑하는 기능이라고도 할 수 있겠다.

def verbose(func):
    def new_func():
        print "Begin", func.__name__
        func()
        print "End", func.__name__
    return new_func

def my_function():
    print "hello, world."

>>> my_function = verbose(my_function)
>>> my_function()
Begin my_function
hello, world.
End my_function

verbose라는 데코레이터를 통해 my_function을 데코레이트했다. 원래는 hello, world만 출력하는 함수였지만 이젠 데코레이트되어 앞뒤로 시작과 끝을 출력한다.

in python

사실 지금까지는 이론이라고 할 수 있고, 이제부터가 진짜 코드 레벨이다. 파이썬 데코레이터라고 하면 바로 @를 의미한다. 파이썬 2.4에서 추가되었다고 한다. 파이썬 코드를 보다보면 아래와 같은 코드들을 종종 볼 수 있다.

@verbose
def my_function():
    print "hello, world."

이는 verbose라는 데코레이터로 my_function이라는 함수를 데코레이트 해준다는 것을 의미한다. 이 함수를 실행하면 같은 결과를 볼 수 있다.

>>> my_function()
Begin my_function
hello, world.
End my_function

헌데, my_function에 파라메터가 있으면 어떡하지? 위에서 했던 걸 생각해보면, 그냥 파라메터를 넣어 주면 된다.

def verbose(func):
    def new_func(name):
        print "Begin", func.__name__
        func(name)
        print "End", func.__name__
    return new_func

@verbose
def my_function(name):
    print "hello,", name

>>> my_function("hi")
Begin my_function
hello, hi
End my_function

*args, **kwargs

그런데 이렇게 되면 파라메터가 하나 있는 함수에 대해서만 이 데코레이터를 사용할 수 있다. 함수의 시작과 끝을 알리는 데 파라메터의 개수가 무슨 상관이란 말인가? 이런 쓸데없는 제약을 없애기 위해 사용할 수 있는 것이 바로 *args**kwargs다. 둘 다 지정되지 않는 파라메터들을 받지만, **kwargs는 딕셔너리로서 이름이 지정된 파라메터들을 받는다.

>>> def foo(x, *args, **kwargs):
...     print x
...     print args
...     print kwargs
>>> foo(1, 2, 3, 4, 5, y=2, z=3)
1
(2, 3, 4, 5)
{'y': 2, 'z': 3}

이를 이용하면 데코레이터를 최종적으로 확장할 수 있다.

def verbose(func):
    def new_func(*args, **kwargs):
        print "Begin", func.__name__
        func(*args, **kwargs)
        print "End", func.__name__
    return new_func

class

다른 함수들과 마찬가지로, 데코레이터 함수도 클래스로 구현할 수 있다.

class Verbose:
    def __init__(self, f):
        print "Initializing Verbose"
        self.func = f

    def __call__(self, *args, **kwargs):
        print "Begin", self.func.__name__
        self.func(*args, **kwargs)
        print "End", self.func.__name__


@Verbose
def my_function(name):
    print "hello,", name

실행 해 보면 결과는 동일하게 나온다.

참고

파이썬 데코레이터 이해하기 : 이론 위주 설명
파이썬 데코레이터 (decorator): 기초편 : 코드레벨 설명

'Python' 카테고리의 다른 글

Errors and Exceptions  (0) 2014.10.09
decorator (2) - extension  (0) 2014.10.06
decorator와 closure  (2) 2014.10.04
String option - u와 r  (0) 2014.09.21
한글 in the dictionary (feat. pretty)  (0) 2014.09.17
Python 2.x 한글 인코딩  (0) 2014.09.10