파이썬은 인터프리터언어입니까?

최근에 많이 보게 되는 질문 중 하나가 ‘파이썬은 인터프리터 언어입니까? 컴파일언어입니까?’라는 것이다. 개인적으로 이 질문은 사람을 참 난감하게 하는데, 어떻게 답해야하나에 앞서 아직까지도 이 개념을 이렇게 잘못 가르치는 교재 혹은 과정이 대부분이라는 점 때문이다. 그럼 인터프리터 언어와 컴파일 언어가 무엇인지 알아보고, 과연 파이썬은 인터프리터 언어인지 생각해보자.

참고로, 보통 나는 이 질문에 ‘반만 맞다’고 말하거나 더 이상의 설명이 귀찮은 경우에는 ‘통상 인터프리터 언어라고 합니다.’라고 답한다.

파이썬은 인터프리터언어입니까? 더보기

한글의 음소분리 문제

한글 문자열의 초/중/종성을 분리하는 예제를 포스팅한 적이 있는데, 이 때는 미처 알지 못했지만, 중요하게 놓친 문제가 있다. 그러니까 초/중/종성에 해당하는 자모와 각각의 낱자가 다른 글자라는 점이다.

예를 들어 ‘한'(U+D55C)자를 보자. 이 글자는 ‘ㅎ’, ‘ㅏ’, ‘ㄴ’ 의 세가지 자모로 분리된다. 이 때 ‘ㅎ’이 초성일 때와 종성일 때에는 같은 ‘ㅎ’으로 보이기는 해도, 같은 코드가 아니다. (그리고 지금 ‘ㅎ’으로 낱자로 쓰고 있는 이 글자 역시 같은 코드가 아니다.

‘ㅎ’을 표현하는 방식에는 다음 세 가지 방식이 있다.

  1. 낱자로서의 ‘ㅎ’ (U+314E)
  2. 초성 자모로서 ‘ㅎ’ (U+1112)
  3. 종성 자모로서 ‘ㅎ’ (U+11C2)

이는 유니코드에서는 완성형과 조합형 한글을 모두 지원하기 때문이다. ‘가’에서부터 ‘힣’에 이르는 자모로 조합가능한 모든 한글글자는 Hangul Syllables 블럭에 정의되어 있다. 그리고 이러한 Syllables를 조합하는데 사용되는 자모들은 모두 Hangul Jamo에 정의된다. 낱자로서의 자모는 Hangul Compatibility Jamo 에 정의되어 있다. 기존의 초성 구하기 코드에서 구하는 답은 자모 코드에 해당한다.

이 자모 코드는 기본적으로 서로 결합하여 완성된 글자(Hangul Syllables)를 구성하는데 쓰인다. 따라서 자모 문자의 코드값으로부터 문자를 얻어서 출력했을 때에는 각 자모 낱자의 출력결과와 구분할 수 없기는 하나 정확한 답이 아닌 것이다.

한글의 음소분리 문제 더보기

[Python] 클래스 이해하기

클래스를 설명할 때 흔히 쓰는 표현은 ‘클래스는 거푸집에 해당하고 객체는 그 거푸집으로 찍어내는 벽돌에 해당한다.’는 것이다. 물론 완전히 틀린 설명은 아닌데, 이 개념에서 출발해서 클래스를 이해하는 것은 객체와 클래스의 관계와 클래스를 어떻게 다룰 것인지 등 여러 관점을 정립하는데 많은 어려움을 유발한다.

이 글은 파이썬 초보자들이 클래스에 대해 접근하고 이해하는데 도움을 주고자 작성됐다.

모든 것은 객체이다.

파이썬에서 통용되는 가장 중요한 대전제는 모든 것이 객체라는 것이다. 1, 2와 같은 숫자값도 C처럼 원시값이 아니라 int 타입의 객체이다. 함수 역시 객체이고 모듈이나 패키지도 객체처럼 취급된다. 모든 것이 객체라면, 클래스 그 자체도 객체라는 말이된다. 그럼 이 시점에서 다시 한 번 되물어보자. 도대체 객체란 무엇인가?

[Python] 클래스 이해하기 더보기

Lock을 사용하는 스레드 동기화 방법

아래는 어떤 “counter”라는 자원을 두 스레드가 동시에 사용하려할 때, Lock을 사용하는 상황을 시각적으로 묘사한 것입니다. 두 워커 스레드 A, B 는 자원에 접근하기 전에 Lock을 획득하려고 시도합니다. 두 스레드 모두 락 객체의 .acquire()를 호출합니다. 이 때 (아마도 간발의 차이로) A 가 락을 획득하게 되었다고 가정하면, A에서 호출한 .acquire()는 즉시 리턴되어 A는 다음 코드를 진행하게 되고 여기서 counter를 사용합니다. 반면 B의 .acquire() 호출은 락을 획득할 때까지 대기하기 때문에 B의 진행 흐름은 여기서 멈추게 되고, A가 자원을 쓰는 동안 . . . 으로 묘사됩니다.

Worker A                   B
       |.acquire()         |.acquire()
        \                  . 
         |- use counter    .
         |.release()       .
        /                  \
       |.acquire()          |-use counter
       .                    |.release() 
       .                   /
       \                   |.acquire()

따라서 Lock을 올바르게 사용하기 위해서는 스레드 관점이 아닌 자원 관점에서 생각하는 것이 맞습니다. 자원 경쟁을 피하고 스레드 안전하게 처리해야 할 자원을 액세스하는 시점에 모든 스레드는 다음과 같이 코드를 작성합니다.

  1. 자원을 액세스하기 직전에 lock.acquire()를 호출합니다.
  2. 해당 자원을 사용합니다.
  3. 처리를 마치면 lock.release()를 호출하여 다른 스레드가 사용할 수 있도록 합니다.

획득-해제의 매커니즘이 두 개의 메소드 호출을 통해서 시작하고 끝나기 때문에 파이썬에서는 락 구간을 시각적으로 구분하기 힘듭니다. 이 때 lock.acquire()는 실질적으로는 락 획득 여부를 리턴하기 때문에 if 문으로 블럭을 구분해주는 것이 좋습니다. 물론 블럭의 끝에서 lock.release()를 호출하는 것을 잊으면 곤란하겠죠.

락을 쓰는 구간 앞뒤로 빠짐없이 넣어야 하는 코드 때문에 획득-해제 구간이 많으면 많을수록 코드 작성이 번거롭습니다. 하지만 threading 모듈이 제공하는 획득-해제식 동기화 수단들은 모두 with 문을 사용할 수 있습니다.

from threading import Thread, Lock, Barrier
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(threadName)s %(message)s')

res = {'counter': 0}
lock = Lock()
bar = Barrier(11)


def worker():
    for _ in range(10):
        with lock:
            r = res['counter']
            res['counter'] += 1
            logging.debug(f"counter: {r} -> {res['counter']}")
    bar.wait()


def main():
    ts = [Thread(name=f'WORKER{i+1}', target=worker)
             for i in range(10)]
    for t in ts:
        t.start()
    bar.wait()


if __name__ == '__main__':
    main()

몇가지 궁금증

항상 락을 사용해야 하나?

일반적으로 외부 세계에 대한 핸들(파일 등)이 아닌 메모리 내 값이나 객체에 대해서 그것이 불변이라면 여러 스레드에서 동시에 참조해도 안전하다고 간주할 수 있습니다. 왜냐하면 변하지 않을 것이 약속되어 있다면 언제 어디서 참조하든 똑같은 값일 것을 기대할 수 있기 때문입니다. 따라서 상수를 참조하는 경우는 락을 생각하지 않아도 좋습니다.

‘쓰기’에서만 락을 적용하면 되나?

여러 스레드에서 공유하는 객체가 mutable 하다면, 쓰기 뿐만 아니라 이 객체를 읽는 동작까지 Lock을 수반해야 합니다. 최악의 경우 읽기와 쓰기가 동시에 진행될 수 있기 때문입니다. 참고로 특정한 스레드끼리만 락을 사용해서도 안됩니다. 현재 스레드가 락을 획득했다 하더라도 다른 스레드에서 해당 락을 얻는 과정을 생략해버린다면 해당 리소스에 대해 스레드 안전한 접근이 보장되지 않습니다.

blocking 과 timeout 옵션

락을 획득하기 위해 acquire() 를 호출할 때 두 가지 옵션이 있습니다. 하나는 blocking=True 이고 다른 하나는 timeout=-1 입니다. 타임아웃은 주어진 시간(초)까지만 락을 얻기 위해 대기하다가 락을 얻거나 얻지 못하고 타임아웃이 지났을 때 리턴하게 됩니다. 결국 acquire()는 락 획득 여부를 True/False로 리턴해주게 됩니다. 타임아웃의 기본 값은 -1이며, 이 경우 acquire()는 락을 획득할 때까지 무기한 기다리게 됩니다.

타임 아웃 대신 blocking=False 옵션을 사용하는 방법이 있습니다. 이 옵션을 사용하면 락을 획득하려 시도하고 즉시 결과를 리턴합니다. 논블럭 락을 사용하거나 타임아웃을 적용하는 경우, 항상 if 문을 사용하여 락을 획득하였을 때에만 선점된 리소스에 접근하도록 해야 합니다.

locked()

락 객체에 대해서 locked() 메소드는 “현재 스레드”가 해당 락을 획득하였는지 여부를 확인할 수 있습니다. 락 획득-해제 구간의 코드가 아닌 다른 함수에서 이미 락을 획득했는지 여부를 알고 싶을 때 사용할 수 있습니다.