RxPY 소개 및 시작하기 

* RxPY 소개글을 써보았습니다. 앞부분의 개념 설명 부분은 건너 뛰고 "RxPY 시작하기" 부터 보는것도 좋을 거 같습니다. 먼저 코딩을 해보는게 정신건강에...도움이 될지도..


Rx? RxPY? FRP?  

RxPY란 Reactive Programming을 하기위한 개념인 Rx(Reactive Extensions)의 파이썬 라이브러리입니다. Rx (반응형 확장) 는 FRP (함수형 반응형 프로그래밍) 개념을 가지고 있는 시스템으로써 (완전한 FRP 는 아니며, 사용자의 편의를 위한 장치가 많은 반면 표시적 의미론이 부족하여 합성성이 떨어진다는 의견도 있습니다.)  마이크로소프트가 닷넷용으로 개발한 라이브러리인 Rx.NET으로부터 시작되어서 자바판인 RxJava, 자바스크립트판인 RxJS, RxSwift,RxGo,RxScala 도 있으며, 기타 언어로 확장 구현 되였습니다. 즉 Rx 는 특정 언어에 얽매여 있지 않기 때문에 하나만 익히면 어떤 언어 환경에서든 쉽게 사용 할 수 있을 것 입니다. 

주요 개념을 간단히 살펴보면 Rx 는 주로 Observable 이라는 인터페이스를 중심으로 한 비동기 API 를 제공하는데 Observable 은 상태변경에 해당되는 이벤트를 발생/전파시키는 스트림으로써 onNext - 값, onError - 오류, onComplete - 스트림의 끝 이라는 세가지 이벤트를 지원합니다. (순수 FRP 에서는 나머지 2개는 불필요하다고 보며, 오직 한가지 이벤트 타입인 값만 지원) Rx 는 비동기 프로그래밍의 어려움도 완화 시켜 주지만 (덕분에 비동기 프로그래밍의 헬인 자바스크립트에 RxJS 라는 이름으로 빠르게 지원됨) Observer 패턴을 대체하기도 합니다.

상태변경에 따른 상호작용에 많이 사용되는 고전적인 Observer 패턴은 여러가지 문제점들이 많이 있는데, 그 문제점들은 FRP 나 Rx 를 통해 완화됩니다. 제가 얼마전에 쓴 글인데 참고를:  굿바이~ Observer 패턴 

더 자세한 정보는 아래 다양한 레퍼런스를 참고하세요.

창시자(?) 인 에릭마이어의 동영상

Rx 와 FRP 의 비교도 참고하시죠

https://github.com/ReactiveX/reactivex.github.io/issues/130
http://lambda-the-ultimate.org/node/4982

한글 블로그입니다. 왜 마소에서 Rx 를 만들었을까에 대한 근원을 찾아가 보는 소개 글입니다.

http://huns.me/development/2051

Rx 는 FRP 의 부분확장팩 라이브러리라고도 볼수 있는데, FRP 를 쉽게 풀어 놓은 책이 얼마전에 번역됨.

함수형반응형프로그래밍

Rx자체에 대한 책도 있습니다.

Reactive Extensions in Action

RxJava를 활용한 리액티브 프로그래밍

Rx 의 근간이 되는 Functor, Monad 에 대한 설명 
https://gist.github.com/jooyunghan/e14f426839454063d98454581b204452

마지막으로 제가 작성한 스칼라에서의 Rx

스칼라 강좌 (39) 동시성을 위한 Observable



Rx vs FRP 의 간단한 의미

위에서는 좀 생각나는 데로 막 쓴 거 같은데요. 아래에는 핵심만 간추려 보겠습니다.

Rx

Rx 의 주요한 화두는 비동기 이벤트(데이터) 처리 라고 생각합니다. 즉 비동기를 어떻게 하면 잘 처리 할 수 있을 까라는 고심에서 나온 라이브러리이며, 비동기를 처리함에 있어서 함수적 처리를 통해 잘 구성할 수 있도록 해주는게 목표입니다. (본질은 조금 다른 거 같지만 실용적으로는 말이죠.) 
그 과정에서 데이터를 비동기로 보내주는 쪽을 Observable 이라고 명시하며, 받아서 처리하는 쪽을 Observer 라고 명시합니다. 데이터 스트림을 가지고 어떻게 합성(zip등) 하고 동기화하고 반응 하게 할 것이냐도 스케쥴러통해 선택 할 수 있으며 나름 구체적인 지침이 있는 라이브러리라고 보면 될 거 같습니다.

FRP

Rx 가 구체적인 라이브러리인 반면, FRP 는 개념에 더 가깝다고 볼 수 있습니다.  Functional reactive programming (FRP) 은 함수적 빌딩블럭(map.reduce,filter,fold) 를 이용하여 리액티브 프로그래밍 (비동기 데이터흐름 프로그래밍) 을 위한 파라다임인데요. 주로 GUI, 로보틱스,뮤직,IoT 센서데이터처리 등 명시적으로 시간을 모델링하는 프로그램을 단순화 시키기 위해 사용됩니다.  -위키백과-

코날엘리엇이라는 FRP 를 발명한 사람의 말에 따르면 FRP 의 가장 중요한 포인트는 표시적/시간연속적 입니다. 

표시적
은 각각의 타입과 구성요소의 의미를 정확히 지정해주는 엄밀하고,단순하며, 구현과는 무관한 합성 가능한 의미론을 기초로 해야한다는 것 입니다. 시스템에 대한 정확한 명세를 제공하며, 모든 경우의 모든 구성요소에 대해 합성성이라는 중요한 특성이 성립함을 증명해주는것이 랍니다. 그냥 "합성성" 이라는 단어만 머리에 넣어 두세요. 

시간연속적은 각자 공부해보는것으로 합시다. (저도 마찬가지..) 

왜 이런 방법론들이 나오게 되는것인지에 대한 개발자로써 쉽게 와닿을 수 있는 예시를 하나 보겠습니다.

{
  a();
  b():
  c();

   ..

}

절차지향에 익숙한 우리는 위의 함수들이 순서대로 발생할 것이라는 것을 염두해 두코 코딩을 하게 됩니다.
즉 c() 함수는 a() 함수와 b() 함수가 무엇인가 처리를 한 후에 그것을 가지고 처리한다는 거죠. 물론 순서가 중요하지 않을 때 도 있습니다.

이때 나중에 참여한 개발자가 저것을 다 파악하기란 힘들 것입니다. (물론 소스가 복잡해 진 다면 말이죠) 
또한 옵저버패턴이 사용된다면 그 순서란 더 감춰지기 마련이니 파악하기 힘들어 질 것입니다. 

의존 관계 와 순서는 이렇게 우리가 짜왔던 절차지향,객체지향에서는 혼동을 주게 되는데요. 
최근에 코어를 더 적극적으로 활용해야하는 시대에 와서 멀티쓰레드/비동기라는 동시성을 다루는 부분에 있어서 더더욱 순서와 의존관계를 파악하기 힘들어 졌습니다.

FRP 와 Rx 의 반응형이라는 말에 뒤에는 이런 순서&의존관계를 명확하게 인지 시키준다는 의미도 포함되어 있습니다. 앞으로 FRP 와 Rx 를 공부하는데 있어서 이 점을 머리속에 넣어 두면 좋을 거 같습니다.


RxPY 정의 

파이썬에서 LINQ 스타일의 쿼리 연산과 Observable 콜렉션들을 사용하여 비동기,이벤트-기반 프로그램을 구성하기 위한 라이브러리입니다.  참고로 O'Reilly 출판사는 Reactive Python for Data Science 동강을 만들었으며 아래는 소개 글을 퍼왔습니다 O'Reilly Safari

액티브 프로그래밍은 데이터 모델링의 미래를 말하고 있습니다. 리액티브를 사용하면 정적 데이터를 간결하게 처리하고 분석 할 수있을 뿐 아니라 실시간의 무한 피드(PUSH)에 의한 데이터를 효과적으로 처리 할 수 있습니다. Reactive Extensions (Rx)는 2009 년에 처음으로 탄생 하였으며,12 개 주요 언어 및 플랫폼에 이식되었습니다. 이 과정에서는 Python 데이터 분석 워크 플로우라는 문제에 대해 가벼운 Rx 파이썬 라이브러리인 RxPy를 사용하는 방법을 배우게 됩니다.

  • 데이터 과학에서 반응형 프로그래밍의 이점에 대한 상세한 고찰 
  • 푸시 기반 (push-based) vs 풀 기반 (pull-based) 이터레이션을 사용하여 문제를 "reactive way"으로 해결하는 방법.
  • 리액티브 프로그래밍이 왜 강력하고 간단하며 탄력적인 코드 모델을 생성하는지에 대한 이해 
  • 클러스터 컴퓨팅 하드웨어를 사용 할 수 없을 때 동시성을 위해 RxPy 활용 방법을 배웁니다.
  • RxPy 사용법을 익히고 모든 데이터 과학 작업을 위한 더 강력한 Python 코드 작성


RxPY 시작하기  

솔직히 저런 개념같은 거 다 집어 치우고, 그냥 냅다 코딩하는게 더 나을거 같습니다. 쉽게 가져다가 만들어 놓은 라이브러리를 그 개념을 완전히 이해해야 한다면 응용개발자들 입장에선 손해니까요~ 시간과 기회는 기다려 주지 않습니다. 각자의 본질적인 서비스에 주력하시고, 저런 이쁘니는 가져다가 사용하는데 주력합시다.


RxPY 설치하기


import rx 해서 사용하면 됩니다. - 끝 -

에러나는데요?

쏘오뤼~~

pip install rx

설치해주셔용~ ^^


RxPY Hello World

from rx import Observable, Observer


def push_hello_world(observer):
observer.on_next("hello")
observer.on_next("world")
#observer.on_error("error")
observer.on_completed()


class PrintObserver(Observer):
def on_next(self, value):
print("Received {0}".format(value))

def on_completed(self):
print("Done!")

def on_error(self, error):
print("Error Occurred: {0}".format(error))


if __name__ == '__main__':
source = Observable.create(push_hello_world)
source.subscribe(PrintObserver())

머리속으로 옵저버패턴을 생각하고 봐주세요. 거의 똑같습니다.
Observable 은 데이터를 가지고 있으며, 데이터를 Observer 에게 전파(Push) 시킵니다. 
데이터를 
종류에 따라서 next, completed,, error 로 Push 받은 Observer는 그것을 출력해 줍니다. 

"별거 없네~ 뭐 그냥 옵저버 패턴이구만" 이라고 생각하고 그냥 사용하세요. 좋은건 일단 쓰고 보는겁니다.
쓰다보면서 조금 다르네, 먼가 더 좋다~~ 라고 느끼면 되는겁니다.

아래는 옵저버를 하나 더 추가 해 봤습니다.

from rx import Observable, Observer


def push_hello_world(observer):
observer.on_next("hello")
observer.on_next("world")
#observer.on_error("error")
observer.on_completed()


class PrintObserver(Observer):
def on_next(self, value):
print("Received {0}".format(value))

def on_completed(self):
print("Done!")

def on_error(self, error):
print("Error Occurred: {0}".format(error))

class ListObserver(Observer):

def __init__(self):
self.my_list = []
def on_next(self, value):
self.my_list.append(value)

def on_completed(self):
print(self.my_list)
print("Done!")

def on_error(self, error):
print("Error Occurred: {0}".format(error))

if __name__ == '__main__':
source = Observable.create(push_hello_world)
source.subscribe(PrintObserver())
source.subscribe(ListObserver())


정리하면 

  • Observable 은 데이터를 생산하는 컴포넌트이며, 시간이 흐름에 따라서 여러 데이터 (혹은 이벤트, 상태)을 바깥으로 내보내고 (Push,Emit) 있습니다.  Observer 가 구독(subscribe) 하는 순간 부터 데이터를 내보냅니다.

  • Observer 는 Observable 에서 내보내는 3가지의 데이터(이벤트) 구분하여 처리 합니다. 

 - on_next :  일반적인 데이터(이벤트)를 처리합니다.
 - on_completed : 모든 데이터를 다 내보내었을 경우에 대해 처리합니다. 
 - on_error : Observable 에서 어떤, 예외가 생겼을 때 처리해 줍니다.

맛뵈기를 보았구요,  이제 RxPY 홈페이지에 있는 진짜베기 시작하기 문서를 보시죠.


진짜 RxPY 시작하기 

Rx 는 이벤트의 흐름을 다루는것에 관한 것입니다. Rx 를 통해 당신은 :

  • 다루기 원하는 데이터가 무엇인지 말하세요. (Observable) 
  • 어떻게 다루고 싶나요? (A composition of operators)
  • 결과(데이터) 를 가지고 하고 싶은것은 무엇입니까? (Observer)

Rx를 사용하여, 이벤트가 도착하는 시점에 이벤트를 통해 원하는 것을 설명 하는 것은 매우 중요합니다. 그것은 연산에 대한 선언적 구성으로써, Observer 에 도착할 때 이벤트를 처리하게 될 것입니다. 즉 아무 일도 일어나지 않으면 아무 것도 처리하지 않습니다.

1. Rx 모듈 임포트 


import rx
from rx import Observable, Observer


2. 시퀀트 제네레이팅 

이벤트를 발생시키는 방법중에 꽤 간단한 from_iterable 을 사용합니다.

class MyObserver(Observer):
    def on_next(self, x):
        print("Got: %s" % x)
        
    def on_error(self, e):
        print("Got error: %s" % e)
        
    def on_completed(self):
        print("Sequence completed")

xs = Observable.from_iterable(range(10))
d = xs.subscribe(MyObserver())
Got: 0
Got: 1
Got: 2
Got: 3
Got: 4
Got: 5
Got: 6
Got: 7
Got: 8
Got: 9
Sequence completed

subscribe 메소드에는 Observer 객체가 들어가지만, print 도 들어 갈 수 있으며 익명 Observer 로써 on_next 에 해당하는 역할을 하게 됩니다.

xs = Observable.from_(range(10))
d = xs.subscribe(print)
0
1
2
3
4
5
6
7
8
9

3. 시퀀스 필터링 (Filtering)

filter 메소드의 매개변수로 람다식을 사용하여 홀수만 Observer에게 전파하고 있습니다.
xs = Observable.from_(range(10))
d = xs.filter(
        lambda x: x % 2
    ).subscribe(print)
1
3
5
7
9

4. 시퀀스 변형 (Transforming)

map메소드의 매개변수로 람다식을 사용하여 데이터에 2배를 하여 Observer에게 전파하고 있습니다.
xs = Observable.from_(range(10))
d = xs.map(
        lambda x: x * 2
    ).subscribe(print)
0
2
4
6
8
10
12
14
16
18

map메소드의 두번째 파라미터로 인덱스를 넘겨 줄 수 도 있습니다.

xs = Observable.from_(range(10, 20, 2))
d = xs.map(
        lambda x, i: "%s: %s" % (i, x * 2)
    ).subscribe(print)

0: 20
1: 24
2: 28
3: 32
4: 36

5. 병합  (Merge)

2개의 Observable에서 부터 흘러 나오는 데이터를 모두 처리합니다. 순서는 보장 못합니다.
xs = Observable.range(1, 5)
ys = Observable.from_("abcde")
zs = xs.merge(ys).subscribe(print)
a
1
b
2
c
3
d
4
e
5

6. Rx의 시공간 (SpaceTime)

위의 모든 예에서 모든 이벤트는 동일한 순간에 발생하며 이벤트는 순서에 따라 분리됩니다. 위의 병합 작업 결과에 다음과 같은 몇 가지 유효한 결과가 있을 수 있으므로 많은 신규 사용자들이  Rx에 혼동에 빠지곤 합니다.

a1b2c3d4e5
1a2b3c4d5e
ab12cd34e5
abcde12345

Rx가 해주는 유일한 보장은 1이 2보다 앞에 나오며, 1은 ys 데이터 중간에 어디서나 나올 수 있다는 것입니다. 그것은 어떤 이벤트가 먼저 가야 할지를 결정하기 위해 스케줄러의 정렬 안정성을 향상시킵니다. 실시간 데이터 스트림의 경우 실제 시간으로 이벤트가 분리되므로 문제가 되지 않습니다. 예상 한 결과를 얻으려면 Rx로 재생할 때 이벤트 사이에 시간을 추가하는 것이 좋습니다.

7. Marbles and Marble 다이어그램


이전 섹션에서 언급되었듯 Rx 및 RxPY로 데이터를 전파 할 때 시간을 추가하는 것이 좋습니다. 가장 좋은 방법은 Marble 다이어그램을 가지고 놀 수 있는 Marble 테스트 모듈을 사용하는 것인데 Marble 모듈은 Observable에 두 가지 새로운 확장 메소드를 추가하는데 각각은 from_marbles () 및 to_marbles ()입니다.

예제:

  1. res = rx.Observable.from_marbles("1-2-3-|")
  2. res = rx.Observable.from_marbles("1-2-3-x", rx.Scheduler.timeout)

문자열은 특별한 문자들로 구성됩니다:

    - = Timespan of 100 ms (100ms 간격) 
    x = on_error()
    | = on_completed()
모든 문자는 문자열에서 발견되는 순간에 on_next () 이벤트로 처리됩니다. 다중 문자 값을 나타내야 할 경우 "1- (42) -3"과 같이 대괄호로 그룹화 할 수 있습니다.

제대로 해보죠.
from rx.testing import marbles

xs = Observable.from_marbles("a-b-c-|")
xs.to_blocking().to_marbles()

'a-b-c-|'

이제 Marble 문자열에 x를 삽입하여 짝수 스트림에 오류를 추가하여 보겠습니다.

xs = Observable.from_marbles("1-2-3-x-5")
ys = Observable.from_marbles("1-2-3-4-5")
xs.merge(ys).to_blocking().to_marbles()

'11-22-33-4x'

8. Subject and Stream

observable stream 을 만드는 간단한 방법은 Subject 을 사용하는 것입니다. 참고로 GOF 의 Design Patterns 책의 Observer 패턴에는 Observable 부분을 Subject 라고 부릅니다만 어쨌든 여기서 subject 는 Observable 처럼 subscribe 를 제공하며, on_next 를 통해 손쉽게 전파할 데이터를 입력 받을 수 있습니다.

from rx.subjects import Subject

stream = Subject()
stream.on_next(41)

d = stream.subscribe(lambda x: print("Got: %s" % x))

stream.on_next(42)

d.dispose()
stream.on_next(43)

Got: 42


9. Multicasting

Observable 에 대한 각 구독자들은 종종 별도의 방출 스트림을 수신 받게 됩니다. 예를 들어, Observable에 두 명의 구독자가 있고, 세 개의 임의의 정수가 방출 될 때, 각 구독자들은 서로 다른 숫자를 갖게 됩니다. 아~~뭔 설명이 더 헤깔리게 하네요. 개발자는 코드죠. 그냥 코드를 보면 쉽게 이해 됩니다.

from rx import Observable from random import randint three_emissions = Observable.range(1, 3) three_random_ints = three_emissions.map(lambda i: randint(1, 100000)) three_random_ints.subscribe(lambda i: print("Subscriber 1 Received: {0}".format(i))) three_random_ints.subscribe(lambda i: print("Subscriber 2 Received: {0}".format(i)))

OUTPUT:

Subscriber 1 Received: 79262
Subscriber 1 Received: 20892
Subscriber 1 Received: 69197
Subscriber 2 Received: 66574
Subscriber 2 Received: 41177
Subscriber 2 Received: 47445

Observable 체인의 특정 지점에 모든 구독자에게 동일한 값을 Push 하기 위해 publish()를 호출하여 ConnectableObservable을 반환 할 수 있습니다. 그런 다음 구독자를 설정(subscribe) 하고 connect ()를 호출 하면 됩니다. 역시 소스를 보시죠.

from rx import Observable from random import randint three_emissions = Observable.range(1, 3) three_random_ints = three_emissions.map(lambda i: randint(1, 100000)).publish() three_random_ints.subscribe(lambda i: print("Subscriber 1 Received: {0}".format(i))) three_random_ints.subscribe(lambda i: print("Subscriber 2 Received: {0}".format(i))) three_random_ints.connect()

OUTPUT:

Subscriber 1 Received: 90994
Subscriber 2 Received: 90994
Subscriber 1 Received: 91213
Subscriber 2 Received: 91213
Subscriber 1 Received: 42335
Subscriber 2 Received: 42335

connect ()를 호출하기 전에 모든 Observers를 설정해야합니다.

mutural을 구현하는 또 다른 방법은 ConnectableObservable에서 auto_connect () 연산자를 사용하는 것입니다.
매개변수로 넣은 값에 따라서 구독자가 subscribe 되면 데이터(이벤트)를 내보내기 시작합니다.

from rx import Observable from random import randint three_emissions = Observable.range(1, 3) three_random_ints = three_emissions.map(lambda i: randint(1, 100000)).publish().auto_connect(2) three_random_ints.subscribe(lambda i: print("Subscriber 1 Received: {0}".format(i))) three_random_ints.subscribe(lambda i: print("Subscriber 2 Received: {0}".format(i))) # second subscriber triggers firing


10. Combining Observables

Observable.merge (), Observable.concat (), Observable.zip () 및 Observable.combine_latest ()와 같은 팩터 리를 사용하여 서로 다른 Observables를 함께 작성할 수 있으며  Observables가 다른 스레드 (subscribe_on () 및 observe_on () 연산자 사용)에서 작업하는 경우에도 안전하게 결합됩니다.

다음 예에서는 Observable.zip ()을 사용하여  5 개의 문자열과 다른 Observable 에서 나오는 값을 튜플로 zip 하여 처리 할 수 있게 됩니다.  두개의 Observable 중 작은 개수의 데이터 스트림만큼 zip 됩니다.

from rx import Observable letters = Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]) intervals = Observable.interval(1000) Observable.zip(letters, intervals, lambda s, i: (s, i)).subscribe(lambda t: print(t)) input("Press any key to quit\n")

OUTPUT:

('Alpha', 0)
('Beta', 1)
('Gamma', 2)
('Delta', 3)
('Epsilon', 4)

11. Concurrency

동시성 위해 RxPY 는 subscribe_on ()observe_on () 두 연산자를 사용하며 두 작업 모두 작업을 수행하기 위해 각 구독에 대한 스레드를 제공하는 스케줄러가 필요합니다 (아래 스케줄러 섹션 참조). ThreadPoolScheduler는 재사용 가능한 작업자 스레드 풀을 만드는 좋은 선택이 될 수 있습니다.

파이썬의 GIL은 다중 스레드가 동일한 코드 행을 동시에 액세스 할 수 없으므로 동시성 성능을 저하시킬 수 있습니다. NumPy와 같은 라이브러리는 GIL을 해제 할 때 병렬 집약적인 계산을 위해 이를 완화 할 수 있습니다. RxPy 또한 스레드 오버랩을 어느 정도 최소화 할 수 있습니다. 동시성으로 애플리케이션을 테스트하고 성능이 향상되는지 확인하십시오.

subscribe_on ()은 사용 할 스케쥴러 체인의 시작 부분에서 Observable 소스를 가르킵니다. 이 연산자를 넣는 위치는 중요하지 않습니다만 observe_on ()은 Observablechain의 해당 시점에 다른 스케줄러로 전환하여 한 스레드에서 다른 스레드로 효과적으로 이동시킵니다. Observable.interval () 및 delay ()와 같은 일부 Observable 팩토리 및 연산자에는 이미 기본 스케줄러가 있으므로 사용자가 지정한 subscribe_on ()을 무시합니다 (일반적으로 스케줄러를 인수로 전달할 수 있음).

아래에서는 observe_on ()뿐만 아니라 subscribe_on ()을 순차적으로 사용하는 대신 세 가지 다른 프로세스를 동시에 실행하는 모습을 보여줍니다.

import multiprocessing
import random
import time
from threading import current_thread

from rx import Observable
from rx.concurrency import ThreadPoolScheduler


def intense_calculation(value):
    # sleep for a random short duration between 0.5 to 2.0 seconds to simulate a long-running calculation
    time.sleep(random.randint(5, 20) * .1)
    return value


# calculate number of CPU's, then create a ThreadPoolScheduler with that number of threads
optimal_thread_count = multiprocessing.cpu_count()
pool_scheduler = ThreadPoolScheduler(optimal_thread_count)

# Create Process 1
Observable.from_(["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]) \
    .map(lambda s: intense_calculation(s)) \
    .subscribe_on(pool_scheduler) \
    .subscribe(on_next=lambda s: print("PROCESS 1: {0} {1}".format(current_thread().name, s)),
               on_error=lambda e: print(e),
               on_completed=lambda: print("PROCESS 1 done!"))

# Create Process 2
Observable.range(1, 10) \
    .map(lambda s: intense_calculation(s)) \
    .subscribe_on(pool_scheduler) \
    .subscribe(on_next=lambda i: print("PROCESS 2: {0} {1}".format(current_thread().name, i)),
               on_error=lambda e: print(e), on_completed=lambda: print("PROCESS 2 done!"))

# Create Process 3, which is infinite
Observable.interval(1000) \
    .map(lambda i: i * 100) \
    .observe_on(pool_scheduler) \
    .map(lambda s: intense_calculation(s)) \
    .subscribe(on_next=lambda i: print("PROCESS 3: {0} {1}".format(current_thread().name, i)),
               on_error=lambda e: print(e))

input("Press any key to exit\n")

OUTPUT:

Press any key to exit
PROCESS 1: Thread-1 Alpha
PROCESS 2: Thread-2 1
PROCESS 3: Thread-4 0
PROCESS 2: Thread-2 2
PROCESS 1: Thread-1 Beta
PROCESS 3: Thread-7 100
PROCESS 3: Thread-7 200
PROCESS 2: Thread-2 3
PROCESS 1: Thread-1 Gamma
PROCESS 1: Thread-1 Delta
PROCESS 2: Thread-2 4
PROCESS 3: Thread-7 300
...

12. Schedulers

RxPY에서는 비동기적으로 실행되도록 선택하거나 스레드를 사용하여 작업 및 시간 초과를 예약하도록 결정할 수도 있습니다.  좋아하는 Python 프레임 워크에서 RxPY를보다 쉽게 사용할 수 있도록 Python 관련 메인 루프 스케줄러가 많이 있습니다.

  • ThreadPoolScheduler to create a fixed sized pool of Schedulers.
  • NewThreadScheduler to create a new thread for each subscription
  • AsyncIOScheduler for use withAsyncIO. (requires Python 3.4 ortrollius, a port of asyncio compatible with Python 2.6-3.5).
  • EventLetEventScheduler for use with Eventlet.
  • IOLoopScheduler for use withTornado IOLoop. See theautocomplete and konamicode examples for how to use RxPY with your Tornado application.
  • GEventScheduler for use with GEvent. (Python 2.7 only).
  • TwistedScheduler for use with Twisted.
  • TkinterScheduler for use with Tkinter. See the timeflies example for how to use RxPY with your Tkinter application.
  • PyGameScheduler for use with PyGame. See thechess example for how to use RxPY with your PyGame application.
  • QtScheduler for use withPyQt4,PyQt5, andPySide. See thetimeflies example for how to use RxPY with your Qt application.
  • GtkScheduler for use withPython GTK+ 3. See thetimeflies example for how to use RxPY with your GTK+ application.
  • WxScheduler for use with wxPython. See thetimeflies example for how to use RxPY with your wx application.


이상 시작하기 부분은 모두 끝마쳤습니다.~ 더 자세한 내용은 소스와 홈페이지를 참고하세요.


레퍼런스:

https://github.com/ReactiveX/RxPY
https://jakubturek.com/functional-reactive-programming-in-python/



파이썬 로깅의 모든것 

파이썬 로깅에 대한 "모든것은" 사실 낚시구요. ㅎㅎ (유희열이 진행했던 All that music 이라는 라디오 프로그램에서 내가 좋아하는 국악이나 시부야계는 거의 나오지도 않았었다...먼소리야 이게 ;;) 그래도 블로그용으로는 꽤나 많은 정보를 담으려고 애를 썼습니다. 파이썬에서 자체 제공하는 기본 로깅 시스템에 대한 내용이구요. 버전이 올라감에 따라 내용이 달라질수 있으며, 오류도 있을 것이고 더 손쉬운 방법등이 있을 수 있음을 인지하시고 항상 다른것들도 같이 확인 하세요. 

본론으로 바로 들어가시죠. 

먼저 제가 원하는 로깅 시스템은 이렇습니다.  IoT 의 게이트웨이 안에서 활약 할 놈으로써~

  1. logging.conf 파일을 통해서 외부에서 설정 가능

  2. 레벨을 자유롭게 설정하고 추가 할 수 있어야 한다.
    (CRITICAL, ERROR,INFO, DEBUG )

  3. 파일,스트림에 동시에 출력 할 수 있어야 한다.

  4. 다양한 목적에 따라 다양한 파일에 출력 할 수 있어야 한다.

  5. 로깅 시간 출력 및 다양한 정보에 대한 추가가 가능해야 한다.

  6. 하루에 한번씩 파일을 생성 해야하며, 지난 파일은 압축하여 보관 해야한다.

  7. 하루에 한번씩 파일을 생성 해야하며, 오래된 파일을 삭제 할 수 있어야 한다.

  8. 파일 용량이 너무 커질 경우에는 자동으로 분리 시켜야 한다.

  9. 멀티프로세스를 활용해야 하는 파이썬의 특성상 멀티프로세스에서도 로그를 취득할 수 있어야 한다.

  10. 핸들러를 커스터마이징 할 수 있어야 한다. 즉 콘솔,파일 출력 뿐 만 아니라, DB 및 소켓을 통하여도 발송 할 수 있어야 한다.

  11. 사용하기 쉬워야 한다. (문서화가 잘 되 있어야 한다) , 기본 로깅 모듈을 사용해야한다.

이것을 해결 하기 위해 알아야 할 것들에 대해 공부해 보는것이 이 글의 목표 입니다. 즉 이 글을 모두 읽고 나면 모든 기능을 본인의 프로젝트에서 사용 할 수 있게 될 것이라 봅니다.

* 참고로 자바개발자에게 익숙한 log4j 같은 거대한 로깅 시스템에 비하면 제공되는 기능이 조금 부족합니다. 몇가지는 직접만들어야합니다.
* 2.x / 3.x 상관없으나, 몇몇 부분은 2.7 기준입니다. 3.x 버전의 새기능 활용 안함. 

초급 

로깅이란? 

현재 우리의 프로그램이 어떤 상태를 가지고 있는지, 외부출력을 하게 만들어서 개발자등이 눈으로 직접 확인하는 것 입니다. 이때 

print (" 난 지금 매우 좋아 ") 
print (" 30이 들어왔어")

이렇게 콘솔로 출력하는 방법이 가장 쉽고 편하긴 합니다만, 파일로의 출력 등 좀 더 다양한 기능을 사용하고 싶을 때 우리는 로깅 시스템을 따로 만들거나, 기존에 구축되어진 라이브러리를 가져와서 사용합니다. 

로깅 모듈 사용하기

파이썬에서 로깅 모듈은 기본 라이브러리에 포함되어 있기 때문에, 굳이 따로 설치 할 필요는 없습니다.

import logging

if __name__ == '__main__':
logging.error("something wrong!!") 결과: ERROR:root:somthing wrong!!

이렇게 logging 모듈을 임포트해서 그냥 사용하면 됩니다. 
위에서는 아무 전처리도 하지 않고 그저  logging 모듈 안의 error 함수를 호출 했습니다. 

로깅 레벨

위에서는 출력 할 때 error 함수를 사용했는데요. 이름에서 느껴지다 시피 먼가 큰 문제가 생겼음을 알려주기 위한 출력이었습니다. 하지만 평범한 정보를 알려 주기 위해서 사용하고 싶을 때가 있을 것입니다.

" 지금 서버가 시작됬어요."
" 서버의 대기포트는 이 번호로 시작 됬습니다"
" 어떤 함수가 호출 되었어요"
" 100이 들어왔어요"

이럴 때는 info 같은 레벨이 좋을거 같습니다. 즉 출력하는 정보의 등급을 나누어서, 구분 지으면 나중에 확인 할 때 편할 거 같습니다.보통 DEBUG, INFO, WARNING, ERROR, CRITICAL 의 5가지 등급이 기본적으로 사용됩니다.

import logging

if __name__ == '__main__':
logging.info("hello world")

응?  출력이 안되네요? 

여기서 예를 하나 들어보죠. 만약 print 로 모든 로깅을 출력한다면, 실제 프로그램이 배포 되었을 경우, 수많은 정보를 출력하게 되고  그것이 오히려 프로그램에 성능 부담이 갈 수도 있겠죠. 또한 출력을 파일로 한다면 파일이 너무 커져서 시스템을 멈출 수도 있을 거 같습니다. 따라서 설정 같은 것을 통해 로깅을 출력을 하지 말아라~ 라고 설정해서 로깅이 출력이 안되게 하면 매우 편할 거 같습니다. 

보통 DEBUG 레벨에서 굉장히 많은 정보들을 출력하게 되는데, 개발시에만 DEBUG 레벨의 정보를 보고 싶고, 실제 서비스를 할 경우는 DEBUG 레벨은 보고싶지 않을 수 있습니다.

INFO 레벨보다 심각한 것만 출력하게 하라!! 라고 로깅 시스템을 사용하면 설정 가능합니다.

파이썬 로깅의 기본 설정은 WARNING 입니다. 따라서  DEBUG < INFO < WARNING < ERROR < CRITICAL  
이 순서에서 더 높은 레벨인 ERROR 는 출력이 되지만, 하위레벨 (INFO,DEBUG) 은 출력이 안됩니다.
즉 이 레벨을 DEBUG 나 INFO 로 낮추어 설정 한후에 사용해야 합니다. 설정하는 방법은 조금 있다가 살펴보죠.


나의 로깅 모듈 사용하기 - 1 (생성)
제일 처음에  logging.error("somthing wrong") 이렇게 error 함수를 바로 사용 했었는데요.
근데 이렇게 직접 함수를 호출하기도 하지만, 자신만의 특정한 로거를 따로 만들어서 사용하는게 보통입니다.

import logging

if __name__ == '__main__':
mylogger = logging.getLogger("my")

이렇게 호출하면 "my" 라는 특정 로거를 생성하게 됩니다.

나의 로깅 모듈 사용하기 - 2 (레벨 설정)

import logging

if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)

setLevel 메소드를 통해서 INFO 레벨 이상은 출력 하도록 설정 하였습니다. (DEBUG 출력 안함)

나의 로깅 모듈 사용하기 - 3 (핸들러 설정)

import logging

if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)

stream_hander = logging.StreamHandler()
mylogger.addHandler(stream_hander)

mylogger.info("server start!!!") 결과: server start!!!

핸들러란 내가 로깅한 정보가 출력되는 위치 설정하는 것을 말합니다. 위에서는 콘솔을 통해 출력하도록 설정 했지만, 파일,DB,소켓,큐등을 통해 출력 하도록 설정 할 수도 있습니다.

나의 로깅 모듈 사용하기 - 4 (파일 핸들러 설정)

파일로도 동시에 출력하게 해봅시다. 매우 간단합니다.

#!/usr/bin/python

import logging

if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)

stream_hander = logging.StreamHandler()
mylogger.addHandler(stream_hander)

file_handler = logging.FileHandler('my.log')
mylogger.addHandler(file_handler)

mylogger.info("server start!!!")

logging.FileHandler 클래스를 통해 객체를 만들어서 나의 로거에 추가해주면 됩니다. 현재 디렉토리에 파일(my.log)이 만들어 질 것 입니다.

노트:  파일은 a 모드로 열리는게 디폴트입니다. a 모드란 같은 이름의 파일이 이미 있다면, 파일 끝에 추가하여 쓰고, 없다면 쓰기용으로 파일 만들라는 뜻입니다.

나의 로깅 모듈 사용하기 - 5 (출력 포매팅 설정)

server start!!! 라는 메세지 말고도, 이 메세지가 언제 쓰여졌는지, 어떤 모듈에서 쓰여졌는지 등 기타 정보를 같이 출력하고 싶을 것입니다. 이때 포매팅이란 것을 하게 되는데요.

import logging

if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

stream_hander = logging.StreamHandler()
stream_hander.setFormatter(formatter)
mylogger.addHandler(stream_hander)

file_handler = logging.FileHandler('my.log')
mylogger.addHandler(file_handler)

mylogger.info("server start!!!") 결과: 2017-08-07 12:00:29,141 - my - INFO - server start!!!

포매팅에 추가한 정보를 모든 로그 데이터에 추가해서 출력하게 됩니다.  

asctime 시간
name 로거이름
levelname 로깅레벨 
message 메세지 

이것 말고도 모듈이름, 파일이름, 출력라인넘버등 다앙하게 많으니, 더 많은 정보는 메뉴얼을 참고 하시구요. 위에서는 포매터를 콘솔출력에 한정해서 적용했기에 파일은 기존 그대로 출력 될 것입니다. 파일도 포매팅되게 하고 싶으면 역시 포매터를 파일 핸들러에도 추가하세요.

중급 

이제 중급부터는 좀 더 인사이드에 대한 고찰이 시작됩니다. 사실 파이썬이라는 언어는 읽고 이해하기 간결한 언어이기 때문에 실제 로깅 소스 모듈을 직접 읽어 보시는게 가장 명쾌하게 이해 할 수 있을 것입니다. 저도 내용 검증은 직접 소스를 확인 하였으며 참고로 파이참을 쓰실 경우 컨트롤 + 좌클릭으로 손쉽게 소스를 확인 할 수 있습니다. 

로깅 모듈 생성에 관한 고찰 

자 위에서 나만의 로깅 객체를 만들 때 logging.getLogger("my") 라고 만들었었는데요.  
그냥 매개변수 없이 logging.getLogger() or logging.getLogger("") 이렇게 만들 수도 있습니다. 

이렇게 하면 루트로거 를 리턴하게 되며, 루트로거는 로깅시스템의 기본로거이고 기본레벨은 warning 을 가지고 있습니다. 우리가 젤 처음에 로거를 만들지 않고 그냥 logging.error("...") 식으로 사용했었죠? 내부에서는 루트로거를 사용하고 있답니다.

로깅 생성에 대한 특성을 정리 해 보면 아래와 같습니다. 

* logging.getLogger() 를 통해 루트 로거 얻어서 사용 할수 있습니다.(기본레벨은 warning)
* logging.error() 를 직접 호출하면 내부에서 루트로거를 이용합니다.
* Logger 인스턴스는 이름으로 식별됩니다.
* 동일 모듈 뿐만 아니라, 여러 모듈에서 동일한 인스턴스이름 logging.getLogger('someLogger')를 여러 번 호출해도 동일한 로거 객체에 대한 참조가 반환됩니다.
* 빈 문자열인 "" 라는 이름의 루트 로거가 있으며, 다른 Logger 들은 모두 루트 Logger 의 자식들입니다. 따라서 루트 로거에 기본적인 설정을 해두고 사용하면 자식들은 모두 동일한 설정을 사용하게 됩니다.(핸들러,포매터등) 
*이름은 마침표(.) 로 구분되는 문자열이며 계층 구조를 형성한다. 즉 a 라는 로거를 만들고, a.b 라는 로거를 만들면 a.b는 a 로거의 자식입니다. 
하나의 모듈에서 상위 로거를 정의 및 구성하고 별도의 모듈에서 하위 로거를 생성 할 수 있으며 하위에 대한 모든 로거 호출은 상위 노드로 전달됩니다.
* 가능하면 루트 로거를 사용하기 보다는 각 클래스나 모듈마다 별도의 로거를 만드는 것을 추천합니다.
* 로깅 메세지는 부가정보가 합쳐져서 LogRecords 로 생성되며, 이 객체를 가지고 각각의 handler 는 파일,콘솔,TCP 등으로 로 emit (출력) 합니다

로깅 모듈 생성 실제 - 로거 범위 

보통 main 에서 로깅 시스템 (대표설정) 을 만들어서 사용하게 됩니다. 이것을 디폴트로 자식 로거들에서 사용하게 되며, 자식별로 필요하면 특정 설정을 하게 됩니다. 또한 핸들러는 버퍼링을 하고 있기 때문에 어플리케이션이 갑자기 중지되기 전에 logging.shutdown() 을 호출해서 모두 출력해 주면 좋습니다. (fianllly 절에서 수행) 

이제 로거를 만드는 대표적인 범위를 살펴보시죠.

@ 모듈명 : 많은 함수와 클래스들을 포함하는 모듈의 전역 Logger 인스턴스로 생성 

import logging
logger = logging.getLogger(__name__)

@객체인스턴스 : __init__() 메소드 내에서 Logger 를 생성. 인스턴스 마다 고유하다. 

def __init__(self, name)
self.logger = logging.getLogger("{0}.{1}".format(self.__class__.qualname__, name))

@클래스명 : __class__.__qualname__  만으로 생성 

@함수명 :  잘 사용되지 않는 큰 함수라면 함수 내에서 로그를 생성 할 수도 있다.

def main():
    logger = logging.getLogger("main") 

파이썬에서는 하나의 파일이 하나의 모듈과 일치합니다. 따라서 파일의 제일 위쪽에서 모듈명으로 로거객체를 만들 수 있으며, 함수별/클래스별/객체별로도 만들 수 있습니다. 자신의 로깅 전략에 따라서 만들면 될 거 같습니다.

로깅 모듈 생성 실제 - 로거 부모/자식 관계 

위에서 로거는 부모/자식관계를 갖는다고 말했는데요. 즉 루트로거는 모든 로거의 부모이며,  a.b 라는 로거는 a 라는 로거의 자식입니다. ( a라는 로거가 없는데 a.b 로거를 만들 수도 있습니다. 내부적으로 a 로거는 자동으로 place holding 됩니다) 

저런 관계에 따른 이상 행동을 예측 하기 위해서는 부모/자식간의 행동을 파악할 필요가 있는데요.
다음 코드를 보시죠. 

# 루트 로거 생성 rootlogger = logging.getLogger()
rootlogger.setLevel(logging.INFO)
stream_hander = logging.StreamHandler()
rootlogger.addHandler(stream_hander)
# "my" 로거 생성
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)
stream_hander2 = logging.StreamHandler()
mylogger.addHandler(stream_hander2)
# "my" 로거 생성
mylogger.info("message from mylogger")

루트로거를 a 모듈에서 만들고, "my" 로거를 다른 곳에서 만들었다고 칩시다.( 위에 소스에서는 보기 쉽게 같은 곳에 두었음)  "my" 로거에서 "message from mylogger" 를 출력 했는데, 결과는 어떻게 될까요? 

message from mylogger
message from mylogger
 
이렇게 2개의 메세지가 출력되는데 상위로 전파되었기 때문입니다.
이것을 막으려면, 간단하게는 루트로거에서 핸들러를 추가해 주지 않아도 되며, 혹은
mylogger.propagate = 0
propagate 를 0 으로 세팅하면 상위로 전파하지 않습니다.

로깅 모듈 생성 실제 - 파일을 통한 설정 

외부 파일을 통해서, 로깅레벨, 핸들러 (파일로 할것인지, 콘솔로 할 것인지 등) 와 포매터등을 설정 할 수도 있습니다. 이렇게 되면 굳이 소스를 건드리지 않고도 간편히 설정가능하겠지요? 
파일 포맷은 JSON, INI, YAML 등 다양하게 할 수 있습니다만 여기서는 JSON을 살펴보겠습니다.

logging.json 파일은 아래와 같습니다.

{
"version": 1,
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},

"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout"
},

"info_file_handler": {
"class": "logging.FileHandler",
"level": "DEBUG",
"formatter": "simple",
"filename": "info.log"
}
},

"root": {
"level": "DEBUG",
"handlers": ["console", "info_file_handler"]
}
}

포매터를 작성했으며, 핸들러를 두개 만들었습니다. root 로거에 대한 설정도 했네요.
파일핸들러를 통해서 info.log 파일에 DEBUG 레벨 이상일 경우 출력합니다. 저것을 실제 서비스시 INFO로 바꾸면 디버깅 출력은 disable 되겠죠. 

루트 로거의 레벨이 DEBUG 이기 때문에 모든 레벨을 다 출력하게 됩니다만, 자식로거에서 레벨을 높히게 되면 자식이 우선순위를 갖게 됩니다.

소스에서는 아래와 같이 logging.json 을 가져다가 사용합니다.


import logging
import logging.config
import json
import os

if __name__ == '__main__':

with open('logging.json', 'rt') as f:
config = json.load(f)

logging.config.dictConfig(config)

logger = logging.getLogger()
logger.info("test!!!")

파일을 읽어서 logging.config.dictConfig 를 통해서 설정을 세팅하고, 루트로거를 가져다가 사용하게 됩니다.
만약 설정 파일에 아래와 같은 특정 로거를 세팅해 주게되면 

"loggers": {
"my_module": {
"level": "ERROR",
"handlers": ["console"],
"propagate": "no"
}
},

logging.getLogger("my_module") 식으로 가져와서 사용하면 저 속성을 갖게 됩니다.

로깅 모듈 생성 실제 - 부가정보 출력하기
로깅 호출에 전달 된 매개 변수 외에도 컨텍스트 정보를 포함하도록 로깅 출력을 원하는 경우가 있습니다.
이때 LoggerAdapters 를 사용 할 수 있는데요.

class LoggerAdapter(logging.LoggerAdapter):
def __init__(self, prefix, logger):
super(LoggerAdapter, self).__init__(logger, {})
self.prefix = prefix

def process(self, msg, kwargs):
return '[%s] %s' % (self.prefix, msg), kwargs

if __name__ == '__main__':

with open('logging.json', 'rt') as f:
config = json.load(f)

logging.config.dictConfig(config)

logger = logging.getLogger("")

logger = LoggerAdapter("SAS", logger)
logger.info("test!!!") 결과: 2017-08-07 12:54:55,911 - root - INFO - [SAS] test!!!!

logging.LoggerAdapter 를 상속받아서 만든 어댑터를 통해서 SAS 라는 문구를 메세지 앞에 고정 시킬 수 있게 됩니다. process 메소드에 prefix 를 추가한것을 확인하세요. 

로깅 모듈 생성 실제 - 커스텀 로그레벨 설정하기 
저같이 센서로 부터 데이터를 받는 IoT 서비스에서 로깅을 출력 할 때 DATA 만을 확인해 보고 싶을 경우가 있습니다. 이 경우 DEBUG 보다 한단계 아래에 로깅 설정을 할 수 있게 되는데요 
CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0
위처럼 각 레벨에는 숫자가 10단위로 적용되어 있습니다. 예상 할 수 있다시피, 내부에서는

if level > 세팅된 레벨:
   출력 

식으로 되어 있어서 세팅된 레벨보다 출력레벨이 클 경우에 한 해서만 출력하고 있습니다.

if ERROR  > INFO :   세팅이 INFO 면 ERROR 는 출력
if DEBUG > INFO :    세팅이 INFO 면 DEBUG 는 출력하지 않음 

따라서 DATA = 15 이렇게 설정해 두면, DEBUG 정보는 출력하지 않고, DATA 레벨만  출력하게 됩니다.

사용예는 아래와 같습니다. 


logging.addLevelName(15, "DATA")
logging.DATA = 15

logger.log(logging.DATA, "message")


멀티쓰레드에서의 로깅 
스레드에서 로깅하는 것에 대해서도 특별한 노력이 필요하지는 않습니다. 각각의 쓰레드에서 동일한 이름의 로거를 가져 다 사용 해도 됩니다. 포매팅을 통해 쓰레드별로 출력 할 수 있습니다.

다음 예제는 기본 스레드와 멀티 스레드에서 로깅을 보여줍니다. 둘이 같은 루터 로거를 사용했습니다.
import logging
import threading
import time

def worker(arg):
while not arg['stop']:
logging.debug('Hi from myfunc')
time.sleep(0.5)

def main():
logging.basicConfig(level=logging.DEBUG, format='%(relativeCreated)6d %(threadName)s %(message)s')
info = {'stop': False}
thread = threading.Thread(target=worker, args=(info,))
thread.start()
while True:
try:
logging.debug('Hello from main')
time.sleep(0.75)
except KeyboardInterrupt:
info['stop'] = True
break
thread.join()

if __name__ == '__main__':
main()
실행하면 스크립트는 다음과 같이 출력 합니다.

  1 MainThread Hello from main
     1 Thread-1 Hi from myfunc
   503 Thread-1 Hi from myfunc
   751 MainThread Hello from main
  1003 Thread-1 Hi from myfunc
  1503 MainThread Hello from main
  1503 Thread-1 Hi from myfunc
  2004 Thread-1 Hi from myfunc


블럭되는 핸들러 유연하게 다루기 
파이썬 로깅 시스템에서 핸들러를 사용 하여 출력을 하게 되는데, 중간에 버퍼링을 하고 있지 않기 때문에, 쓰기에서 오랜 지연시간이 걸리는 행동을 하게 된다면,  전체 시스템은 느려지게 마련입니다. 주로 이메일 전송의 경우 이런 행동이 자주 나타나곤 하는데, 이 경우 중간 큐를 사용하여 버퍼링을 해 줄 필요가 있습니다. 
 
이때 QueueHandler 와 QueueListener 를 사용하면 됩니다. 문제는 파이썬 2.7에서는 이것을 지원하지 않기 때문에 직접 비스무리하게 만들어야 합니다. 


import Queue
import threading
import traceback
import logging
import sys

class QueueListener(threading.Thread):

def __init__(self, queue, stream_h):
threading.Thread.__init__(self)
self.queue = queue
self.daemon = True
       self.logger = logging.getLogger("main")
self.logger.addHandler(self.handler)
def run(self):
while True:
try:
record = self.queue.get()
self.logger.callHandlers(record)
except (KeyboardInterrupt, SystemExit):
raise
except EOFError:
break
except:
traceback.print_exc(file=sys.stderr)



class QueueHandler(logging.Handler):

def __init__(self, queue):
logging.Handler.__init__(self)
self.queue = queue

def emit(self, record):
self.queue.put(record)


if __name__ == '__main__': # 리스터 부분
logging_q = Queue.Queue(-1)
stream_h = logging.StreamHandler()
log_queue_reader = QueueListener(logging_q,stream_h)
log_queue_reader.start()
# 핸들러 부분
handler = QueueHandler(logging_q)
root = logging.getLogger()
root.addHandler(handler)
     # 사용
root.error("queue handler test!!")

어떻게 활용하는지 설명드리면 

큐 생성)
먼저 버퍼로 사용할 큐를 -1 을 매개변수로 만들고, 

데이터 입력)
큐에 데이터를 담아줄 QueueHandler 를 만듭니다. 핸들러는 기본적으로 emit 메소드를 가지고 있어서 이 메소드를 통해서 log 정보를 (LogRecord 객체) 콘솔로 쏴주거나, 파일로 쏘게 됩니다만, 여기서는 큐에 데이터를 넣습니다.

데이터 처리 ) 
큐에서 데이터 (LogRecord 객체) 를 가져와서, 핸들러에게 넘겨주는 역할을 하는 QueueListener 를 만듭니다.
실제 처리를 담당할 핸들러를 위에서는 스트림 핸들러를 만들어서 매개변수로 넣어주었습니다.

그 후에는 그냥 로거 사용하듯이 사용하면 됩니다. 그러면 중간에 큐를 거쳐서 출력하게 되겠지요. 
위에서는 쓰레드를 사용했지만, 멀티프로세싱을 통해서 할 수도 있으며, 이 방식은 파이썬에서 문제가 되는 멀티프로세싱 로깅을 해결 할 수 있는 실마리를 제공해줍니다. 
이 부분은 고급에서 배우게 될 것입니다.

노트: 
고급을 진행하기 전에 멀티프로세스에 관해서 알아보자. 아시다시피 파이썬에서는 멀티쓰레드가 I/O 를 제외하고 동시계산하는 곳에서는 그닥 효율적이지 않기 때문에, 멀티프로세스를 사용하게 된다. 이때 로깅도 프로세스 각각에서 사용하게 되는데, 이때 동일한 파일이 두 프로세스에 의해 열려져 있기 도 한다.이런 상황에서 여러 문제가 발생하고 해결 해야 하는 문제가 있다.
이전 로그파일을 압축하려고 할 때 우리는 , 압축 할 파일을 잠시 close 하고  압축을 하게 되는데, 윈도우즈에서는 압축이 안되는 현상 및 기존 로그 파일을 날짜가 붙은 새로운 이름으로 변경 하러 할 때도 안되는 현상이 있다.이것은 다른 프로세스에서 그 로그파일을 열고 있기 때문인데 우분투에서는 잘되었다. 아마 파일 디스크립터는 다르기 때문으로 추정한다.  아무튼 운영체제별 이런 차이들이 있어서 좀 골치 아프다. 마지막으로 멀티프로세스 환경의 로깅에서는 
각각 따로 쓰면 파일에 써지지 않기 때문에  멀티프로세스용 큐를 이용하여 하나로 모아서 파일에 쓰게 되는 것을 보게 될 것이다


윈도우) 
- 2개의 프로세스에서 하나의 파일에 쓰기를 할 경우 윈도우는 예측 불가능이고 (하나의 프로세스 것만 써질 수도 있고, 부분 누락이 되며 순서대로 써질 수도 있고)
- 파일 삭제는 불가능
- 압축파일에 파일 옮겨 쓰는것은 예측 불가하다.
- 다만 하나의 프로세스에서 파일을 close 하면 ,그 이후로 다른프로세스의 데이터는 파일에 써지며, close 한 프로세스에서 쓴 데이터의 일부는 압축도 된다.   

리눅스) 
- 2개의 프로세스에서 하나의 파일에 쓰기를 할 경우 리눅스는 하나의 프로세스 것만 써진다.  
- 파일 삭제는 가능하다.
- 압축파일로 옮겨 쓰는 것도 가능하다. 다만 파일을 close 해주고 해야한다. 물론 메인프로세스에서 쓴 내용만 압축된다.


고급

고급은 아래와 같은 주제를 다룹니다.  조금 숨 좀 돌리고 작성 하도록 할께요~다음 기회에 봐요 ~:-) 

* 추가 컨텍스트 정보 출력 하기 / 필터 사용하기 
* 소켓핸들러 작성 
* 멀티프로세싱을 위한 로깅 작성
* 용량 넘어선 로깅파일 나누기 
* 어제로깅파일을 압축하기
* 한달전 로깅파일을 삭제하기  


참고로 기본 로깅보다 조금 더 구색을 갖춘 pylogging 이란 것도 있습니다.
https://github.com/Clivern/pylogging

그래도 먼가 많은 기능들이 다 있으며, 표준처럼 활용하는 멋진 로깅라이브러리가 없는건 아쉽네요.


레퍼런스) 

https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes 
https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/
https://stackoverflow.com/questions/8467978/python-want-logging-with-log-rotation-and-compression
https://www.amazon.com/Mastering-Object-oriented-Python-Steven-Lott/dp/1783280972/ref=sr_1_2?ie=UTF8&qid=1501725014&sr=8-2&keywords=mastering+python


docker vs vagrant vs virtualenv 


도커도 안쓰고 virtualenv도 안쓰고 잘 지내 왔다. 사실 지금도 굳이 도커나 virtualenv를 써서 괜한 복잡도를 올릴 필요는 없는 상황이긴 하지만, (현재는 virtualbox 같은 이미지레벨도 아닌, os 자체를 클론질라로 이미지 떠서 관리하고 있다)  앞으로 둘 중 하나를 반드시 써야할 상황은 올것이다. (분산 텐서플로우를 사용한다던지.. 도커안에 virtualenv 를 사용 할 수도)


참고로 virtualenv 를 가상환경이라고 다들 말해서 , 먼가 거창해보이지만 그냥 유저독립적 python 환경일뿐이다. 따라서 virtualenv 를 윈도우즈,우분투등에서 사용하다보면 OS의 다름 때문에 먼가 잘 안될 때도 있는거 같다. 그 경우에는 레벨이 한단계 아래인 도커가 나을것이며, 도커보다 더 한 단계 아래의 작업 (OS 부팅시 자동로그인되며,브라우저들이 자동으로 뜨고, 각종 서비스들이 시작되는등) 을 하고 싶다면 나처럼...OS 를 굽는수 밖에..


여튼 기타 자세한 사항등은 나중에 정말 필요 할 때 찾아보는 것으로 하고 간략 정리+번역1개 하고 마무리~:-)


참고로 도커로 Pycharm 사용하는것에 관한 글은 아래를 참고 

 

PyCharm + Docker로 파이썬 개발환경 셋업하기 



짧은 정의 

docker

- 리눅스의 컨테이너 기반의 가상화 플랫폼 
컨터이너의 스타트업 시간은 겨우 몇초
- 각각의 내용을 포함한 이미지를 가지고, 그 이미지를 실행함 

vagrant 

-  OS 기반의 가상화 플랫폼 (리눅스,윈도우등) 
- 기존 VirtualBox 등을 통한 가상화를 좀 더 편하게 해주기 위함. 
- 도커보다 무겁다. (스타트업 시간 몇분) 

virtualenv

- 초간단 (하지만 아무것도 안하는거보단 복잡 ㅎ) 
- 독립 파이썬 인터프리터/라이브러리 환경 (라이브러리 설치시 독립적으로 관리된다) 
- OS 독립이 아니다. 즉 우분투에서 만든 virtualenv 환경이 윈도우에서 잘 안될 수도. 
- 윈도우 기반의 virtualenv 에서는 pycrypto 같은거 설치하기 어려움
- 당연하게도 파이썬 코드,파이썬 패키지,모듈 이외의 프로그램 (DB,미들웨어등) 에 필요한 요소들에 대해선 따로   먼갈 해줘야한다. 그냥 Docker 쓰면 되는데..

Docker vs Vagrant 



Vagrant와 Docker가 경쟁자로 보일지라도, 진취적인 관리자들은 서로를 보완하기 위해 함께 사용하는 방법을 발견했다. 이러한 시나리오에서는 Vagrant를 사용하여 기본 VM을 만들고 이 기본 VM을 모두 사용하는 다른 구성을 만들어야 할 때 Docker를 사용하여 다양한 경량 버전을 준비하고 만들며 당연하게 이 두개의 구성위에 virtualenv 를 통해 독립적인 파이썬 환경을 만들 수도 있겠다.


Docker vs Virtualenv

위에 간략정리에서 봤다시피 이 두개의 요소도 노는물이 다르다. 즉 경쟁상대가 아니다. 
그냥 전반적인 개발환경을 도커로 만들어두고, 내부에 파이썬 프로젝트가 하나만 있으면 virtualenv 사용하지 말고, 내부 프로젝트가 늘어날 예정이거나, 여러가지면 virtualenv 를 사용한다. 



virtualenv 는 살아있다 ! [Foot번역] 


https://hynek.me/articles/virtualenv-lives/


PyPI를 통해 패키지를 설치 할 수 있도록 Python을 설치하는 것은 짜증나고 시간이 오래 걸릴 수 있다. 더 나쁜 것은 OS가 숨겨진 오류 메시지를 던지기 시작하는 것인데  특히 데스크톱은 그 경향이 있으며, reddit에서 들어보았을지 모를 어떤 반짝거리는 패키지를 설치하여 서버의 전체 툴 체인을 무너 뜨리는 것도 가능하다.

virtualenv 너머의 글로벌 site-packages 에는 아무것도 설치하지 마십시오.

혹시 님에게는 봉창두드리는 소리?  웬만하면 따르시구요. 적당히 하세요. 

virtualenv in 2014 

virtualenv는 얼마 동안 Python 소프트웨어를 설치하기 위해 받아 들여졌던 표준이었지만 슬프게도, 현재는 몇몇 선교사들이 대담하게 virtualenv의 끝을 선포하고 있다. 일반적으로 컨테이너, 한마디로 Docker 때문에..

나는 그것이 불행하고 근시안적인 것이라고 생각하며 솔직히, 그들은 전체 그림을 보지 못한다 : virtualenv의 임무는 프로젝트를 서로 분리하는 것이 아니라 그것의 임무는 운영체제의 Python 설치와 설치된 패키지와 섞여지지 않게  당신의 것을 분리시키는 것이다. (역주: 비슷한데? ) 
이제 아주 유명한 virtualenv-killer 인  Docker를 사용하여 이것이 왜 좋은 아이디어인지 살펴 보겠다. 이를 위해 python-pip를 신뢰할 수 있는 컨테이너 1에 설치 한 후 얻을 수있는 사전 설치된 패키지를 살펴 보겠다



argparse (1.2.1) chardet (2.0.1) colorama (0.2.5) html5lib (0.999) pip (1.5.4) requests (2.2.1) setuptools (3.3) six (1.5.2) urllib3 (1.7.1) wsgiref (0.1.2)

Surprised? 

새로운 requests , html5lib 또는 colorama를 설치하면 어떻게 됩니까?
나는 당신에게 말하고 싶은것은 :  먼가가 망가지기 시작하고 있으니 정신차리세요.

이러한 일은 언제든지 발생할 수 있으며 시스템을 허약하게 만들 수 있다. 모든 기능을 갖춘 우분투 서버는 당연히 더 많은 물건을 운반하며 파이썬으로 작성된 시스템 툴을 설치할 때마다 어떤 알 수 없는 종류의 파손이 일어날 수 있다. 데비안 패키지 개발자는 pip이 어떻게 작동하고 패치 할 것인지에 대해 마음에 들지 않는다고 결정할 때마다 "폭발 할 것인가?"라는 추첨의 일부로 무의식적으로 참여하게 된다.

OS X 도 전혀 다르지 않다. 그것은 수십 개의 Python 패키지와 함께 제공된다.
데스크탑에서도 - 플랫폼에 상관없이! - 상황이 더욱 열악하다. 보통의 사이트 패키지가 혼란스러우며 대부분의 사용자는 특정 패키지가 왜 설치되어 있는지 알지 못 하고 있다. 튜토리얼 멘토가 확인하는대로 설치 전체를 중단하는 단계는 매우 짧습니다.

운영체제 Python의 site-packages3는 운영체제에 속합니다.

나는 오랫동안 OS 벤더가 다른 곳에서 자신의 물건에 대한 가상 환경을 생성하고 사용자가 시스템 사이트 패키지를 갖도록 한다면 더 좋아할 것이라고 말하고있다. 그러나 그것은 일어나지 않으며  아무도 알지 못하는 일부 시스템 도구가 프로젝트 요구 사항과 호환되지 않는 버전의 라이브러리를 설치하지 않는다고 보장 할 수는 없다.

해당 OS의 버전 명시적으로 작성된 소프트웨어만 site-packages 에  넣어라. 즉, OS에서 제공하는 Python 패키지만 사용하는 시스템 도구이다. 다른 모든 것을 virtualenv에 보관하라. 

virtualenv 대 시스템 격리(도커) 에 대해 논의하는 것을 그만 두십시오. 한 번에 두 가지를 모두 사용해야하고 다른 한 가지로 대체 해서는 안됩니다.

1. Docker / lxc / jails / zones / kvm / VMware / ...를 사용하여 응용 프로그램 당 하나의 컨테이너 / VM으로 응용 프로그램 서버의 OS를 호스트에서 격리하라.

2. 하지만 그들 내부는 시스템  site-packages로 되있으므로  virtualenv를 사용하여 파이썬 환경을 격리시켜라.





http://www.markbetz.net/2014/01/17/python-if-you-have-docker-do-you-need-virtualenv/

https://www.theodo.fr/blog/2015/04/docker-and-virtualenv-a-clean-way-to-locally-install-python-dependencies-with-pip-in-docker/

https://www.quora.com/Is-there-any-advantage-of-running-Tensorflow-in-a-docker-container-rather-than-just-pip-installing-it

https://stackoverflow.com/questions/40177240/what-is-the-difference-between-vagrant-docker-virtualenv-or-just-a-virtual-mac

https://readme.skplanet.com/?p=13470



환상적인 파이썬에도 골칫거리가 있으니, 패키징이다.
너무 산만하다는게 좀 ...


깔끔하게 정리된 글들에 대한 링크를 골라 보았다.





[번역] 파이썬 나라의 앨리스


How To Package And Distribute Python Applications




Python virtualenv 정리 (Linux/Windows)

[Python] 파이썬 실행환경의 독립 virtualenv & PyCharm

setup.py와 requirements.txt의 차이점과 사용 방법

안녕프로그래밍 - 파이썬 프로젝트의 구조 


파이썬 프로젝트 시작하기 - Virtualenv

파이썬 프로젝트 시작하기 - Setuptools






Python 과 Mixin


파이썬은 다중 상속을 지원하며 그것에 의해 Mixins 를 만들 수 있다. Mixin 은 클래스에 추가적인 속성이나 메소드를 제공하는 것을 말하는데, 스칼라는 traits 를 통해서 제공하며, 루비등도 제공한다. 파이썬은 Mixin 을 위한 특별한 키워드는 없으며, 단지 다중상속을 통해서 만들기 때문에 이 과정에서 문제가 생길 소지가 생긴다. 스칼라의 경우 stackable traits pattern이라고 동일한 메소드가 있을 경우 순서대로 하나씩 실행되지만 파이썬의 경우 덮어 써 버린다. 

다음 예를 살펴보자 

class Mixin1(object):
    def test(self):
        print "Mixin1"

class Mixin2(object):
    def test(self):
        print "Mixin2"

class MyClass(BaseClass, Mixin1, Mixin2):
    pass

파이썬에서는 오른쪽 부터 왼쪽으로 계승이 되는데, 즉 Mixin2 클래스가 가장 상위클래스가 되고 그 후에 Mixin1 , BaseClass 가 차례대로 하위 클래스가 되는데 일반적인 경우 덮어 써질 염려가 없지만 , 위 처럼 메소드 명이 같을 경우 가장 하위의 클래스가 적용되어 덮어 써진다. 

>>> obj = MyClass()
>>> obj.test()
Mixin1

따라서 -> 이 쪽으로 갈 수록 상위 클래스라는 것을 명심해야 버그를 줄일 수 있을 것이다.

class MyClass(Mixin2, Mixin1, BaseClass):
    pass
>>> obj = MyClass()
>>> obj.test()
Mixin2

개인적으로 상속이나 믹스인보다 컴포지션을 많이 사용한다. 객체지향에 서툴러서 인 걸까? 아니면 먼가 더 달라 붙어있는것을 싫어해서 일까? 대부분의 경우 유연성을 위한 복잡함의 증가로 피부에 와닿았지 훗날의 유지보수에서 득이 되는 경우를 많이 겪어보지 않아서 일까? 물론 is-a , has-a 라는 구분하에 적재적소에 가려 써야 겠지만....말은 참 쉽다. ㅎㅎ



파이썬 마이크로 실전 패턴  [번역]


(역주: Gof 의 디자인패턴은 개발하는 중간 중간 자주 사용되지는 않는 중급 규모의 설계의 구조를 담당한다면. 이 마이크로 패턴은 더 작고 개발하면서 매일 만날 수 있는 아주 익숙한 문제들의 모음이다. 이디엄과 겹치기도 한다.) 

일반적인 디자인패턴은 다음 링크(한글 번역) 를 참고하라 -> Pattern in Python 

May 09, 2013


이 글은 블라디미르 켈 레시프 (Vladimir Keleshev)의 "Python Best Practice Patterns"라는 제목으로, 2013 년 5 월 2 일 덴마크 Python Meetup에서 발표되었습니다. 원본 동영상은 here (약 38 분 길이)입니다.

참고 : 일부 코드 예제는 독자 의견을 기반으로 원래의 프레젠테이션에서 수정되었습니다 

참고 : Keleshev는 비디오에서 지적했듯이 코드 예제는 Python3 용으로 작성되어있습니다. object으로 부터 명시적으로 하위 클래스를 만들지 않았습니다. (역주: object 를 상속받는것은 python 2.x 버전의 new 스타일입니다. python 3.x 에서는 old -style 과 new - style 의 구분이 없어지고 모두 new-style 로 행동하므로 object 를 명시적으로 써 넣지 않아도 되게 되었지만 더 널리 퍼지기 전 까지는 오해를 방지하고자 작성해주는것을 권장하기도 합니다


컴포즈드 메소드(Composed method)

  • 하나의 일을 수행하는 메소드로 프로그램을 나눈다. 

    • 메소드안의 모든 연산을 동일한 추상레벨로 유지하라.
  • 적은 라인 수를 가진 많은 메소드로 만들라.

  • 추상레벨을 다르게 만들라: 비트맵, 파일 시스템 연산, 사운드 플레이 등

# Better
class Boiler:
def safety_check(self):
if any([self.temperature > MAX_TEMPERATURE,
self.pressure > MAX_PRESSURE]):
if not self.shutdown():
self.alarm()
def alarm(self):
with open(BUZZER_MP3_FILE) as f:
play_sound(f.read())
@property
def pressure(self):
pressure_psi = abb_f100.register / F100_FACTOR
return psi_to_pascal(pressure_psi)
...
view rawgistfile1.py hosted with ❤ by GitHub
  • safety_check 은 단지 온도와 압력만 다룬다.
  • alarm 은 파일과 사운드만 다룬다.
  • pressure 은 비트(bits) 와 그것들을 변환하는것을 다룬다


생성메소드(Constructor method)

  • 잘 구성된 인스턴스를 만드는 생성자들을 제공한다.

    • 요구되는 모든 파라미터를 생성자에 넘긴다. 
# At initiation `point` is not well-formed
point = Point()
point.x = 12
point.y = 5
# Better
point = Point(x=12, y=5)
view rawgistfile1.py hosted with ❤ by GitHub
  • 여러 방식의 생성자를 만들기 위해 클래스 메소드를 사용 할 수 있다.

    • 예) 카테시안 좌표와 극좌표 
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
@classmethod
def polar(cls, r, theta):
return cls(r * cos(theta),
r * sin(theta))
point = Point.polar(r=13, theta=22.6)
view rawgistfile1.py hosted with ❤ by GitHub


메소드 오브젝트(Method objects)

  • 많은 인자들과 임시변수들을 공유하는 굉장히 긴 라인을 가진 메소드에서의 코딩방법은?
def send_task(self, task, job, obligation):
...
processed = ...
...
copied = ...
...
executed = ...
100 more lines
view rawgistfile1.py hosted with ❤ by GitHub
  • 많은 수의 작은 메소드들을 만들어서 해결 될 수는 없다. (더 많은 코드를 사용해야할 듯) (역주: ??) 
class TaskSender:
def __init__(self, task, job ,obligation):
self.task = task
self.job = job
self.obligation = obligation
self.processed = []
self.copied = []
self.executed = []
def __call__(self):
self.prepare()
self.process()
self.execute()
...
view rawgistfile1.py hosted with ❤ by GitHub


역자 추가 : 참고

* 오리지널
Extract Method 를 사용할 수 없을만큼 꼬인 코드가 있다고 하자.
class Order {
//...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation.
//...
}
}

* method object 로 변경 

로컬변수는 멤버변수가 되었으며, 메소드 위주의 객체를 만들어서 다양한 메소드로 분리 할 수 있게 되었다.
class Order {
//...
public double price() {
return new PriceCalculator(this).compute();
}
}

class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;

public PriceCalculator(Order order) {
// copy relevant information from order object.
//...
}

public double compute() {
// long computation.
//...
}
}


메소드 주변에서 실행하기(python 에서는 컨텍스트 매니저)

  • 함께 해야 될 액션 쌍을 어떻게 표현 할 것인가?
f = open('file.txt', 'w')
f.write('hi')
f.close()
# Better
with open('file.txt', 'w') as f:
f.write('hi')
with pytest.raises(ValueError):
int('hi')
with SomeProtocol(host, port) as protocol:
protocol.send(['get', signal])
result = protocol.receive()
class SomeProtocol:
def __init__(self, host, port):
self.host, self.port = host, port
def __enter__(self):
self._client = socket()
self._client.connect((self.host,
self.port))
def __exit__(self, exception, value, traceback):
self._client.close()
def send(self, payload): ...
def receive(self): ...
view rawgistfile1.py hosted with ❤ by GitHub


Debug printing method

  • __str__는 사용자를 위해 사용하자.
    • e.g. print(point)
  • __repr__ 는 디버깅을 위해 사용하자.


Method comment

  • 작은 메소드는 주석보다 효과적이 될 수 있다.
if self.flags & 0b1000: # Am I visible?
...
# 더 좋습니다.
...
@property
def is_visible(self):
return self.flags & 0b1000
if self.is_visible:
...
view rawgistfile1.py hosted with ❤ by GitHub


Choosing message

# 좋지 않아요.

if type(entry) is Film:
responsible = entry.producer
else:
responsible = entry.author
# Shouldn't use type() or isinstance() in conditional --> smelly

# 좋습니다.

class Film:
...
@property
def responsible(self):
return self.producer
entry.responsible
view rawgistfile1.py hosted with ❤ by GitHub


Intention revealing message

  • 구현이 너무 간단하여 의도를 알리기 쉽지 않을 때 어떻게 할까?
  • 동일한 일을 하는 메소드를 이용하라 ( 더 읽기 쉽게 ) 
class ParagraphEditor:
...
def highlight(self, rectangle):
self.reverse(rectangle)
# 더 낫죠
class ParagraphEditor:
...
highlight = reverse # More readable, more composable
view rawgistfile1.py hosted with ❤ by GitHub


Constant method (constant class variable)

_DEFAULT_PORT = 1234
class SomeProtocol:
...
def __enter__(self):
self._client = socket()
self._client.connect(
(self.host,
self.port or _DEFAULT_PORT)
)
return self
# If you want to subclass SomeProtocol, you would have to overwrite every method!
# 더 낫죠
class SomeProtocol:
_default_port = 1234
...
def __enter__(self):
self._client = socket()
self._client.connect(
(self.host,
self.port or self._default_port))
view rawgistfile1.py hosted with ❤ by GitHub
  • 서브클래스를 만들 때도 상속되니 편리하다.


Direct and indirect variable access

  • Direct
    •  getters 와setters 가 필요 없다.
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
# Sometimes need more flexibility --> use properties
class Point:
def __init__(self, x, y):
self._x, self._y = x, y
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
view rawgistfile1.py hosted with ❤ by GitHub


Enumeration (iteration) method

# 나뻐요. 컬렉션의 타입을 바꿀 수 없습니다.




# e.g. can't change employees from a list to a set

class Department:
def __init__(self, *employees):
self.employees = employees
for employee in department.employees:
...
# 더 낫다
class Department:
def __init__(self, *employees):
self._employees = employees
def __iter__(self):
return iter(self._employees)
for employee in department: # More readable, more composable
...
view rawgistfile1.py hosted with ❤ by GitHub


Temporary variable

# Meh
class Rectangle:
def bottom_right(self):
return Point(self.left + self.width,
self.top + self.height)

# 가독성을 위해 임시변수를 사용하는것이 더 낫다.

class Rectangle:
...
def bottom_right(self):
right = self.left + self.width
bottom = self.top + self.height
return Point(right, bottom)
view rawgistfile1.py hosted with ❤ by GitHub


Sets

  • for 루프를  대신하는 sets 을 종종 사용 할 수 있다.
item in a_set
item not in a_set
# a_set <= other
a_set.is_subset(other)
# a_set | other
a_set.union(other)
# a_set & other
a_set.intersection(other)
# a_set - other
a_set.difference(other)
view rawgistfile1.py hosted with ❤ by GitHub


Equality method

obj == obj2
obj1 is obj2
class Book:
...
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return (self.author == other.author and
self.title == other.title)
view rawgistfile1.py hosted with ❤ by GitHub

  •  isinstance() 체크용으로 사용


Hashing method

class Book:
...
def __hash__(self):
return hash(self.author) ^ hash(self.other)
view rawgistfile1.py hosted with ❤ by GitHub


Sorted collection

class Book:
...
def __lt__(self, other):
return (self.author, self.title) < (other.author, other.title)
view rawgistfile1.py hosted with ❤ by GitHub


Concatenation

class Config:
def __init__(self, **entries):
self.entries = entries
def __add__(self, other):
entries = (self.entries.items() +
other.entries.items())
return Config(**entries)
default_config = Config(color=False, port=8080)
config = default_config + Config(color=True)
view rawgistfile1.py hosted with ❤ by GitHub


Simple enumeration parameter

  • 적절한 반복문에 사용되는 변수를 만들기 힘들 때 그냥 each 를 사용하라. 
# 어색함
for options_shortcut in self.options_shortcuts:
options_shortcut.this()
options_shortcut.that()
# 좋습니다.
for each in self.options_shortcuts:
each.this()
each.that()
view rawgistfile1.py hosted with ❤ by GitHub


Cascades

    리턴 값이 없는 메소드를 작성하는 대신에, self 를 리턴해보라.

# Instead of this
self.release_water()
self.shutdown()
self.alarm()
class Reactor:
...
def release_water(self):
self.valve.open()
return self
self.release_water().shutdown().alarm()
view rawgistfile1.py hosted with ❤ by GitHub


Interesting return value

# Meh
def match(self, items):
for each in items:
if each.match(self):
return each
# Is this supposed to reach the end? Is this a bug?
# Better
def match(self, items):
for each in items:
if each.match(self):
return each
return None
view rawgistfile1.py hosted with ❤ by GitHub
  • 암시적인 것보단 명시적인게 낫다.
  • None이라도 리턴하라.


더 읽어볼꺼리

  • Smalltalk Best Practice Patterns   (김창준: 마이크로 패턴. 개발자의 탈무드. 감동의 연속)

    • 스몰토크에 관한것이 아니다. Python, Ruby 등 다른언어에도 통찰력을 줄것이다.





파이썬에서 가장 쉽게 범할 수 있는 10가지 실수들 [번역]

https://www.toptal.com/python/top-10-mistakes-that-python-programmers-make
(번역중 제가 간략하게 의역한 부분도 있고, 10가지중 앞의  '6가지 실수' 를 번역되었습니다.) 


실수 #1: 디폴트 함수인자에 대한 실수 

파이썬은 함수인자로 디폴트 값을 사용 할 수 있게 해주는데 , 꽤나 괜찮은 문법이지. 다만 그 디폴트값이 mutable 일 경우 혼동을 주기도 하는데 다음 예를 보자고.

>>> def foo(bar=[]):       # bar 는 디폴트로 [] 를 갖는데 아직 구체화 되지 않았어.
...        bar.append("baz")    # 그 경우 이 라인은 좀 문제가 될 수 있지..
...        return bar

일반적인 실수는 옵셔널 인자가 구체적인 디폴트 식으로 세팅 될 것이라고 생각하는 것인데, 함수는 사실 구체적 매개변수가 제공되지 않은 상태에서 호출 될 수 있거든.

위의 코드에서 누군가 매개변수 없이 foo() 를 반복적으로 호출하면 항상 "baz" 를 리턴 한다고 생각하는게 당연한데, (즉 항상 새로운 빈 리스트가 할당될 것이라고 생각할테지? ) 그건 적어도 파이썬에선 착각이야. 

자 어떤 일이 일어나는지 확인해 보자고. 

>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

어때?  골때리지? 

그냥 이전의 "baz" 요소를 계속 지닌채로 작동하잖아? 함수를 호출 할 때마다 쌓이고~
이유는 말이지. 함수 매개변수에 대한 디폴트 값은 함수가 정의되는 시점에 오직 한번만 평가가 되어져. 그래서 bar 매개변수는 foo() 함수가 처음 정의 될 때 
디폴트로 초기화 되지. 그리고 그 뒤로 foo() 함수가 호출 되면 (bar 매개변수가 구체화 되지 않은 채) 계속해서 동일한 리스트를 사용 하게 되는거야. 

따라서 보통 다음처럼 구현하는게 좋아.
(역주: 파이썬의 이디엄이라 볼 수 있을거 같다. 디자인 패턴이 객체지향 언어 모두의 공통된 습관이라고 보면, 이디엄은 특정 언어에 해당하는 좀 좁은 의미의 습관으로 볼 수 있다. 예를들어 C++ 의 스마트 포인터 같은것이 이디엄이다.) 

>>> def foo(bar=None):
...       if bar is None:		
...          bar = []             # 호출 될 때마다 새로 만든다.
...        bar.append("baz")
...        return bar
...
>>> foo()
["baz"]
>>> foo()
["baz"]
>>> foo()
["baz"]

실수 #2: 클래스 변수 사용에 대한 실수 

다음 예를 살펴보자

>>> class A(object):
...       x = 1
...
>>> class B(A):
...       pass
...
>>> class C(A):
...       pass
...
>>> print A.x, B.x, C.x
1 1 1

다음 처럼 작동하길 기대하겠지

>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1

좋아~!!  근데 아래는 ~??? 

>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3

왓더~??  난 오직 A.x 만 바꾸었는데 왜 C.x 도 바뀐거야? 아니면 부모가 바뀌었으니 그냥 다 바뀌거나~

그건 말이쥐~ 파이썬에서는 말야 클래스 변수는 내부적으로 딕셔너리이며 Method Resolution Order (MRO)로 작동한다고 알려져있지.  예제를 보면 먼저 B.x 는 B.x 에 값을 할당하는 순간부터는 A.x 와는 다른 독립적인 속성이되. 그래서 A.x =3 이 대입되어도 영향을 안받은거고,  C 의 경우는 일단 C 의 클래스에서 x 가 발견되지 않으면, 부모로 올라가서 찾아보게 되는데, 그 곳에서 찾으면 일단 C.x 값은 A.x 의 값을 참조하게되. 결국 이 상태에서 A.x 에 무엇인가 대입을 하게 되면  C.x 도 따라서 바뀌게 되는거지. 물론 C.x 도 안바뀌게 하려면 B클래스처럼 스스로 대입받으면 되는거야. 

더 자세한것은 요기를 참고하도록:  class attributes in Python.

실수#3: 예외 블럭에서 파라미터 리스트에 대한 실수 

다음과 같은 코드가 있다고 하자.

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except ValueError, IndexError:  # 둘 중에 하나의 예외를 잡는다? 
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
IndexError: list index out of range

여기서 문제는except구문은 특정 예외의 리스트를 가질 수 없다는것이다. 파이썬 2.x 에서 except Exception, e구문은 옵셔널 파라미터로 예외에 바인드 되어서 예외가 가진 속성을 조사하기 위해서 사용 된다. 결과적으로 위의 코드는IndexError예외는except구문에서 잡히지 않는다.

여러가지 예외를 하나의 except 구문에서 사용하기 위한 방법은 예외들을 포함한  tuple 로써 구성하는것인데 as 키워드를 사용할 수 있으며, (예외에 대한 구체적인 조사가 필요 없다면 as e 는 할 필요 없다)  python 2, 3에서 지원된다. 코드는 다음과 같다.

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except (ValueError, IndexError) as e:  
...     pass
...
>>>

실수 #4: 파이썬 스코프 룰에 대한 오해 

파이썬 스코프 범위는 LEGB 룰에 준한다. 간략히 말해서 Local, Enclosing, Global, Built-in. 의 약자이며, 그대로 이해하면 될 것이다. 하지만 약간의 미묘한 부분이 있는데 아래를 살펴보자.

>>> x = 10
>>> def foo():
...       x += 1
...       print x
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

무슨 문제일까?

The 위에 발생된 문제는 변수에 값을 할당하려다 생긴 문제인데, 저것은 x = x+1 아닌가? 여기서 x+1 을 먼저하게되고 이때 x 를 찾지 못한다는 얘기다. 우리가 보통 생각 할때는 로컬에 없으면 글로벌로 자동적으로 찾아가지 않을까 생각하는데 파이썬에서는 그렇지 않다.

특히 이문제는 리스트를 사용함에 있어서 종종 나타난다. 다음 코드를 보자.

>>> lst = [1, 2, 3]
>>> def foo1():
...       lst.append(5)   # 좋습니다.
...
>>> foo1()
>>> lst
[1, 2, 3, 5]

>>> lst = [1, 2, 3]
>>> def foo2():
...       lst += [5]      # 문제가 생겼네요 ;;
...
>>> foo2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment

엥? 왜foo2 는 문제가 생길까?foo1 는 괜찮아 보이는데 말이지

대답은foo1에서는lst를 할당(assignment) 하지 않는다. 반면foo2하고 있다. 알다시피lst += [5] 는 lst = lst + [5] 의 약자이다. 즉lst 에 값을 assign 하고 있다. 그래서 아하~ 로컬 스코프에서 찾아야하는구나~~ 라고 행동하지만 로컬에는 아직 정의되지 않았기 때문에 "펑~사망하였습니다" 라는 외마디 비명소리만..

참고로 global 로 지정해주면 해결 할 수 있으며, 글로벌에서 찾으라고 지침해준다.

lst = [1,2,3]
def foo2():
global lst
lst += [5]
print lst # [1, 2, 3, 5]


실수#5: 리스트를 반복(iterating)하다 수정하면서 생기는 문제 

아래코드에서 생기는 문제는 꽤나 명백해 보입니다.

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> for i in range(len(numbers)):
...     if odd(numbers[i]):
...         del numbers[i]  # BAD: Deleting item from a list while iterating over it
...
Traceback (most recent call last):
  	  File "<stdin>", line 2, in <module>
IndexError: list index out of range

순회하던 중간에 지워 버렸으니, 리스트 길이는 줄어들었는데 순회는 갯수는 바꾸지 않았기 때문에 IndexError 가 생겨버렸습니다. 사실 이런문제는 다양한 언어에서도 마찬가지로 발생되죠. 언어별로 조심해야 할 부분입니다.

운좋게도 파이썬에서는 좀 더 우아한 처리 방식이 있는데요.  list comprehensions. 라고 합니다.동일한 일을 하는 코드를 그것으로 만들어보면 아래와 같습니다.

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> numbers[:] = [n for n in numbers if not odd(n)]  # ahh, the beauty of it all
>>> numbers
[0, 2, 4, 6, 8]


실수#6:  클로저안에 변수들을 바인딩하는 동작에 관한 문제 

다음 예제를 보시죠.

>>> def create_multipliers():
...     return [lambda x : i * x for i in range(5)]  # 빨강색 람다함수 5개가 생깁니다. 
>>> for multiplier in create_multipliers(): # 람다함수 5개중 하나씩 토해냄. 
...     print multiplier(2) # 토해진 람다함수의 i 값은 0,1,2,3,4 가 아니라 모두 4가 됩니다.
...

아래와 같기를 기대 할 테지만

0
2
4
6
8

사실 다음과 같습니다.

8
8
8
8
8

깜놀했죠?

이것은 파이썬의 늦은 바인딩 때문입니다. 즉 함수 호출 할 때 클로저에서의 변수 값들은 안쪽 함수가 호출 될 때 찾아 진다는 것입니다. 그래서 위의코드는 리턴된 함수가 호출 될 때마다, i 의 값이 해당 시간에 둘러쌓인 스코드 내에서 찾아지는데 이미 그 값은 (가장 마지막 값인 ) 4로 할당되어 있지요.

다음처럼 꼬아서 해결 할 수 있습니다.

>>> def create_multipliers():
...     return [lambda x, i=i : i * x for i in range(5)]
...
>>> for multiplier in create_multipliers():
...     print multiplier(2)
...
0
2
4
6
8

해결은 됬지만, 좀 꼬아놓은 듯한 모양새 때문에 찬반이 갈립니다. 쿨하다~~ 가독성이 떨어져서 지저분하다, 굳이 저렇게 까지? 


+ Recent posts