[Python] negative index를 사용하자

일반적으로 random access 가능한 자료 구조에서 뒤에서 k 번째 원소를 가져오는 방법은 다음과 같다. 자료구조의 크기를 가져온다. 크기를 기반으로 접근할 인덱스를 구한다. 원하는 자료를 가져온다. 이를 코드로 표현하면 다음과 같다. 파이썬에서는 이를 보다 간결하게 표현하기 위해 negative indexing을 지원한다. negative indexing은 말 그대로 음수를 이용해서 뒤에서부터 자료에 접근하는 것이다. negative indexing을 이용하면 위의 코드는 아래와 같이 표현된다. 파이썬에서는 뒤에서부터 접근할 때 negative indexing을 사용하는 것이 권장되지만 다른 언어로 코딩을 처음 배운 사람들은 파이썬에서도 길이를 가져와 인덱스를 계산하는 방식을 사용하는 경우가 종종 있다. 이런 사람들은 negative indexing이 단순한 syntactic sugar 라고 생각하는 것 같다. 하지만 negative indexing은 syntactic sugar가 아니다. negative indexing이 syntactic sugar라면 negative indexing을 쓴 것과 같은 바이트 코드가 생성돼야 한다. 하지만 둘은 서로 다른 바이트 코드가 생성된다. 위의 각 코드가 실제로 생성하는 바이트 코드는 다음과 같다. 첫 번째 코드는 global memory에서 len 함수를 찾아 호출하여 그 결과에 k 를 뺀 값을 인덱스로 사용하는 코드를 생성하고, 두 번째 코드는 k 를 음수로 만들어 바로 인덱스로 사용한다. 인덱싱을 통해 값을 가지고 오는 바이트 코드는 BINARY_SUBSCR 인데, 물론 인터프리터가 BINARY_SUBSCR 를 실행할 때 주어진 인덱스가 음수이면 내부적으로 리스트의 길이를 가져와 인덱스를 다시 계산 하는 과정을 거친다. 하지만 Global Interpreter Lock(a.k.a. GIL) 때문에 이 둘의 동작은 같지 않다. 파이썬의 GIL은 바이트 코드 단위의 atomi...

Python은 어떻게 swap하는가

지난번 글 에서 Lua의 multiple assignment를 이용한 swap이 내부적으로 어떻게 돌아가는지 설명했었다. 그렇다면 파이선은 어떨까? 자신의 버추얼 머신의 구현까지 제공하는 루아와 다르게 파이선은 공식적으로 어떤 버추얼 머신을 사용해야 하는지 제공하지 않는다. dis 라이브러리를 통해서 어떤 바이트 코드가 나오는지 알 수 있지만, 구체적인 버추얼 머신의 스펙을 정의하거나 바이트 코드의 구현을 정의하지 않는다. 덕분에 PyPy , IronPython , Jython 등 다양한 구현체가 존재한다. 이번에는 우선 그중에서 사실상 표준 구현체라고 할 수 있는 CPython의 구현에 대해서 살펴보겠다. CPython의 버추얼 머신이 어떻게 구현돼야 하는가는 명확히 기술되지 않았지만, CPython의 바이트 코드를 보면, CPython은 Lua가 레지스터 머신을 사용하는 것과 다르게 스택 머신을 사용한다는 것을 쉽게 알 수 있다. CPython의 버추얼 머신은 스택과 글로벌/로컬 메모리를 가지고 있다. 글로벌 메모리와 로컬 메모리는 객체의 레퍼런스를 저장하는 데 사용되고 계산을 하기 위해서는 스택으로 레퍼런스를 복사해온 뒤 사용해야 한다. 이제 swap이 CPython에서 어떤 바이트 코드로 컴파일되는지 살펴보자. 위와 같은 코드는 아래와 같은 바이트 코드로 컴파일된다. 여기서 ROT_TWO 는 스택의 가장 위의 두 아이템의 위치를 바꿔주는 바이트 코드다. 즉, 위의 바이트 코드는 스택에 a 를 올리고 b 를 올린 뒤, 스택의 가장 위의 두 아이템의 위치를 바꾸고, 스택 가장 위의 아이템을 b 에 다음 아이템을 a 에 저장하는 것이다. 그렇다면 ROT_TWO 는 어떻게 구현됐을까? 현재 CPython에서 ROT_TWO 는 임시 변수 2개를 사용하여 스택의 두 값의 위치를 바꾸는 것으로 돼 있다. 즉, CPython에서는 변수 2개를 스왑하기 위해서 스택의 두 자리, ROT_TWO 에서 사용하는 임시 변수 두 자리까지 총 6개의 의...

Lua를 쓰면 3번째 의자가 필요하지 않을까 - lua는 어떻게 swap할까

프로그래머들 사이에서 자주 하는 농담 중에 프로그래머 두 명이 자리를 바꾸려면 의자가 3개 필요하다는 농담이 있다. 둘이 동시에 자리에서 일어나 자리를 옮길 수 있는 사람과 달리 컴퓨터가 다루는 값은 언제나 어딘가에는 할당돼 있어야 해서 나온 말이다. c++로 작성하면 아래와 같은 코드가 되는데, 여기서 local 변수인 temp 가 3번째 의자가 된다. 그렇다면 multiple assignment 가 가능한 lua같은 언어는 어떨까? lua로 swap 하는 함수를 짜면 위와 같은 코드가 된다. 일단 겉보기에는 3번째 의자가 필요 없어 보인다. 하지만 정말로 3번째 의자가 필요 없을까? 어떤 트릭을 쓰기에 그런 것이 가능한 것일까? 일단 결론부터 말하면 그런 마법은 없다. 위의 코드가 루아 바이트 코드로 컴파일되면 다음과 같은 바이트 코드가 나온다. 이 코드를 이해하기 위해 우선 루아 버츄얼 머신을 이해해야 한다. 루아 버츄얼 머신은 stack-based가 아니라 register-based 머신이다. 루아 버츄얼 머신은 최대 256개의 레지스터를 사용할 수 있으며, 함수의 인자는 순서대로 0번 레지스터부터 할당된다. 물론 이는 어디까지나 버츄얼 머신이 이렇다는 것이기 때문에 실제로 256개의 레지스터가 필요하지는 않고, 적절히 번역돼서 실행된다. 이제 위의 바이트 코드를 읽어보자. b, a = a, b 는 MOVE 2 0; MOVE 0 1; MOVE 1 2 로 컴파일됐다. 이를 이해하기 위해서는 MOVE x y 는 x번째 레지스터에 y번째 레지스터의 값을 복사해 넣는 바이트 코드라는 것과 인자로 넘어온 a 와 b 는 각각 0번 레지스터와 1번 레지스터에 저장돼 있다는 것을 알아야 한다. 그러면 위의 바이트 코드는 3번째 의자인 2번 레지스터를 이용해서 두 변수를 스왑한다는 것을 알 수 있다.

[Web] SpeechSynthesis - TTS API

SpeechSynthesis 는 Web Speech API의 하나로 주어진 텍스트를 소리로 바꿔주는 TTS API이다. SpeechSynthesis 이전에도 TTS 서비스가 있었지만, 이들은 유료이거나 웹에서 사용하기 불편한 경우가 대부분이었는데 SpeechSynthesis 의 경우 브라우저에 내장되는 API이므로 무료로 쉽게 사용할 수 있다는 장점이 있다. 물론 Web Speech API 자체가 아직 draft 에 해당하기 때문에 브라우저가 지원해야만 사용 가능하다는 문제는 있지만, 2016년 8월 15일 현재 데스크탑에서는 크롬과 사파리, 안드로이드 기본 브라우저인 크롬, 아이폰의 기본 브라우저인 사파리에 지원하고, 파이어폭스는 9월에 지원할 예정이므로 대부분의 모던 브라우저에서는 사용할 수 있다. speechSynthesis 는 5개의 함수를 가지고 있다. 그중 4개는 speak , cancel , pause , resume 이다. 이를 이용해서 재생할 음성을 추가하거나 취소하거나 일시 정지할 수 있다. 이 중 speak 함수는 SpeechSynthesisUtterance 를 인자로 받는다. speak 함수를 호출했을 때, 이미 재생 중인 utterance 가 없고 speechSynthesis 가 pause 되어 있지 않으면, 요청된 utterance 는 즉시 재생된다. 하지만 이미 재생 중인 utterance 가 있거나 speechSynthesis 가 pause 되어 있다면, utterance 는 바로 재생되지 않고 queue에 저장되었다가 후에 재생된다. 따라서 실제로 언제 재생되는지는 utterance 의 콜백을 통해서만 알 수 있다. 이벤트의 종류에 따라서 onstart , onend , onerror , onpause , onresume 등의 콜백을 등록할 수 있다. 또한, SpeechSynthesisUtterance 는 6개의 속성을 가지고 있다. 첫 번째 속성은 text 로 읽을 텍스트를 지정한다. 객체를 생성할 ...

Go의 장단점

지난 4개월 동안 Go로 2개의 프로젝트를 진행하게 되었다. 한 프로젝트는 비동기적인 웹 서버를 작성하는데, 실행파일이 네이티브 바이너리로 나올 필요가 있었다. C나 C++은 웹서버를 만드는 것도, 비동기적인 작업을 하는 것도 간단하게 되지 않기에 Rust와 Go 중에서 고민하다가 Go를 사용하기로 했다. 다른 한 프로젝트는 큰 파일을 다운받아서 라인 파싱하고, rest API로 데이터를 요청하고, 그래프를 만들어 자료를 분석하여야 했다. 워낙 큰 파일을 파싱해야 해서 JavaScript는 사용하기 싫었고, 며칠을 밤낮으로 돌아가야 하는 코드인데 별도의 검증을 할 수 없는 Python으로는 실행 중 버그가 발생했을 때 손실되는 시간이 크기 때문에 사용하기는 싫어서 Java, Scala, Go 중에서 고민하다가 Go를 쓰기로 했다. 이번 글에서는 Go를 사용하면서 느꼈던 장단점에 대해서 간단히 정리해보도록 하겠다. 장점 학습 속도가 빠르다. Go는 공식 튜토리얼 만 읽으면 다른 사람이 쓴 코드를 읽고 쓰는데 아무런 문제가 없다. 물론 Go의 튜토리얼 문서가 잘 돼 있기는 하지만, 다른 언어에 비해 크게 뛰어난 것은 아니다. 그냥 Go에는 프로그램을 작성하는데 필수적인 최소 기능들만 들어가 있기 때문이다. Channel 기반 언어 Go는 대표적인 채널 기반 언어이다. 스레드라는 것을 명시적으로 주지 않고 goroutine을 생성하면 알아서 스레드를 생성해주고 적절한 스레드에 goroutine을 할당한다. goroutine 사이의 커뮤니케이션을 전부 채널을 통해서 한다면, 귀찮은 동기화 문제를 신경 쓰지 않아도 된다. 네이티브 바이너리가 나온다. 결과를 배포하는 입장에서 네이티브 바이너리가 나온다는 것은 매우 큰 장점이다. 요즘은 대부분 서버에 파이썬이나 JVM이 설치되어 있지만, 역시 배포는 네이티브 바이너리로 하는 것이 가장 편하다. 컨벤션이 통일되어 있다. Go는 컨벤션에 대한 논쟁이 전혀 없다. go에는 컨벤션에 맞춰서 코드를 수정해주...

Normalized Google Distance - 구글을 통해서 유사도 비교하기

지난번 글에서는 압축 알고리즘을 통해서 두 object의 유사도를 비교하는 방법 을 소개하였었다. 이번에 소개할 방법은 지난번 방식보다 더 재밌는 방법이다. 이번에 소개할 방법은 Normalized Google Distance(a.k.a. NGD) 라는 방법으로 이름 그대로 구글을 이용한다. 구글을 이용하기 때문에 문자적으로 의미를 가지는 키워드에 대해서만 유사도를 비교할 수 있다. 만약 두 키워드가 비슷한 의미라면, 이 두 키워드 중 하나가 나오는 문서에 다른 키워드도 등장할 확률이 높다. 이를 구글을 이용해서 측정하는 것이다. 구글에 의해 검색되는 문서 전체의 수를 N 이라고 하고, 구글에 검색해 검색결과의 수를 리턴하는 함수를 f 라고 할 때, NGD의 수식은 다음과 같다. NGD ( x , y ) = max { log f ( x ) , log f ( y ) } - log f ( x , y ) / log N - min { log f ( x ) , log f ( y ) } 비교하고 싶은 각각의 키워드를 따로따로 검색했을 때의 결과와 함께 검색했을 때의 차이를 전체 문서의 수 N 으로 정규화하는 것이다. 이때 정확한 전체 문서 수를 아는 것은 사실 불가능하다. 그래서 NGD를 이용할 때는 IDF가 매우 낮은 단어의 문서 수를 전체 문서 수로 가정한다. 보통 the 나 a 같은 단어를 사용한다. NGD는 0에서 ∞사이의 값을 가진다. 0에 가까울수록 둘은 유사한 키워드이다. 유사하지 않은 키워드는 비교하면 큰 값이 나온다. 보통 1보다 크면 둘은 유사하지 않다고 말한다. NGD는 일반적으로 사용할 수 없고, 문자적으로 의미를 가지는 키워드를 비교할 때만 사용할 수 있다는 점과 특정 토픽 내에서의 유사도를 비교한다거나 하는 식으로 확장할 수 없다는 단점이 있다. 하지만 구글링할 수 있는 키워드를 일반적으로 비교할 때 쉽게 구현할 수 있다는 장점이 있다.

Normalized Compression Distance - 압축 알고리즘을 통해서 유사도 비교하기

이번에 소개할 Normalized Compression Distance (a.k.a. NCD) 는 압축 알고리즘을 이용해서 두 object의 유사도를 측정하는 참신하고 재밌는 방법을 소개할 것이다. 이 방법은 간단하면서도 의외로 효과적이면서 기발한 방법이다. compression 알고리즘은 보통 효율적인 압축을 위해서 자주 등장하는 sequence일수록 더 짧은 길이로 압축하도록 노력한다. 따라서 같은 길이의 sequence가 더 짧은 길이로 압축됐다면, 이 sequence에는 반복되는 sub-sequence가 많이 있다는 말이 된다. NCD는 compression의 이러한 성질을 이용한다. 입력받은 sequence를 compression 한 길이를 리턴하는 함수를 Z 라고 할때 NCD의 수식은 다음과 같다. NCD ( x , y ) = [ Z ( x + y ) - min { Z ( x ) , Z ( y ) } ] / max { Z ( x ) , Z ( y ) } 즉, 비교할 두 sequence를 합친 sequence를 압축한 길이가 sequence를 따로따로 압축한 길이에 비해서 얼마나 더 짧아졌는가를 보여주는 것이다. 압축 알고리즘은 일반적으로 Z ( x + y ) 가 Z ( x ) + Z ( y ) 보다 작아서 NCD는 0에서 1 사이의 정규화된 값을 가진다. NCD가 1에 가까울수록 둘 사이에는 공통점이 없는 것이고, 0에 가까우면 둘은 매우 비슷한 패턴을 가지고 있는 것이다. NDC는 두 object를 binary sequence로 변환할 수 있는 경우에만 사용 가능하다는 문제가 있지만, 컴퓨터로 계산하는 대부분의 경우 binary sequence로 변환할 수 있으므로 큰 문제는 되지 않는다. NCD의 재밌는 점은 어떤 압축 알고리즘이든지 사용할 수 있다는 것이다. 덕분에 gzip, bzip, 7z 등 이미 존재하는 다양한 압축 알고리즘을 이용할 수 있어서 구현이 쉽지만, 비교할 object의 패턴에 따라...

이 블로그의 인기 게시물

USB 2.0 케이블의 내부 구조

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Web] SpeechSynthesis - TTS API

[Python] cache 데코레이터로 최적화하기