개발환경을 정리하는 툴 direnv

direnv는 개발환경을 쉽게 정리하는 툴입니다.
헤로쿠의 등장을 기점으로 각종 프로그램의 설정은 환경변수를 이용하는 게 보편화 되었고 그 환경변수는 .env 파일에 저장하는 게 일반적입니다.
헤로쿠의 경우 foreman을 이용해 로컬에서 개발서버를 돌려 볼 때 자동적으로 .env를 읽어서 환경변수로 지정한 뒤에 실행하게 되고 파이썬 같은 경우 honcho라는 클론 버전이 있습니다.

하지만 개발서버를 돌리는 일 외에 DB 마이그레이션(eg: 장고의 ./manage.py migrate)등을 해야 할 때도 그 환경변수를 지정해야 할 일이 생겼고 그 때마다 honcho run ./manage.py migrate 등의 일을 하기란 굉장히 불편합니다.
그래서 해당 디렉터리에 들어갈 때 자동적으로 .env를 로드해 주는 autoenv라는 툴이 나왔습니다만, 이 툴은 해당 디렉터리를 빠져나갈 때 unload를 시켜주지 않는 문제가 있습니다.

그런 배경을 가지고 direnv가 나오게 된 것인데 이 툴은 기본적으로 디렉터리를 빠져나갈 때 unload를 해 주는 것은 물론이고 현대 개발언어 환경에 유용한 기능들을 많이 가지고 있습니다.
Python 개발의 경우 대부분 venv를 어떤 식으로든 사용을 하게 되는데 해당 디렉터리에 들어 갈 때마다 이를 활성화 해 주는 일도 여간 귀찮은 게 아닙니다. pyenv를 사용해서 .python-version을 이용하는 방법도 있지만 개인적으로 pyenv-virtualenv는 가상환경을 ~/.virtualenvs 밑에 만드는데 프로젝트를 삭제할 때 이 venv가 같이 삭제가 안 되는 게 좀 껄끄럽습니다. 일종의 강박증이죠.
direnv는 .envrc 파일에 layout python을 적어 넣으면 자동적으로 숨겨진 디렉터리(./.direnv) 안에 venv를 만들어 주고 해당 디렉터리에 들어가 있을 때만 활성화를 시켜 줍니다.
짜잔! 드디어 파이썬도 npm이나 bundle처럼 디렉터리마다 자동적으로 따로 관리 되는 개발환경을 가질 수 있게 되었군요!
direnv example
python 이외에 다른 언어들도 지원합니다. 자세한 내용은 해당 프로젝트의 wiki 페이지를 참고하시면 됩니다.

direnv의 문제

layout python 등의 명령어가 있으므로 .env를 사용하지 않고 .envrc를 사용하는 건 이해가 가지만 .envrcdotenv를 적어주지 않으면 .env에 적힌 환경변수를 로딩하지 않는 게 기본 설정입니다. 왜 이렇게 동작하는지 모르겠지만 전역 설정(~/.config/direnv/direnvrc or ~/.direnvrc)에 dotenv를 적어봤는데 내가 있는 디렉터리를 기준으로 .env를 읽어 오는 게 아니라 이 설정파일의 위치를 기준으로 읽어오기 때문에 사용할 수 없습니다.

중복된 이미지를 삭제해주는 프로그램

난 트위터를 하다가 재밌거나 한 짤(그림)이 보이면 쭉쭉 저장하곤 하는데 이게 쌓인 양이 늘어나다보니 찾기가 힘들어서 폴더별로 분리를 하기 시작했다.
폴더를 분리해도 각 폴더 안에 사진이 1000장 이상 들어가기 시작하면 그래도 찾기가 어려워지는데 그러다가 중복된 짤이 있는 것 같은 느낌이 들었다.

원래는 같은 파일명을 가진 사진이라면 덮어쓰기가 되어 중복해서 들어오지 않겠지만 누군가가 저장했던 사진을 다시 재업로드 하면 그 때는 파일명이 달라지니 어쩔 수가 없었다. 게다가 웬만한 서버에서는 그림파일을 올리면 exif 정보를 날리는 등의 변조를 하므로 파일의 해시값도 달라지게 된다. 그래서 sha1 해시 등을 이용해서 중복된 그림을 찾아 지우는 건 거의 불가능에 가까웠다.

언젠가 파이썬으로 비슷한 이미지를 찾는 글을 찾았는데 이걸 거의 그냥 그대로 이용해서 중복된 이미지를 찾아 지워주는 CLI 프로그램을 하나 작성했다. 비슷한 이미지가 있으면 동일한 것으로 간주하고 가장 해상도가 높은 한 장만 남긴채 나머지를 모두 지워버리게 했다.

원리는 아주 간단하다. 이미지를 흑백 이미지로 바꾼 후에(웬만한 영상처리는 흑백으로 바꾸는 것이 1단계다) 특정한 사이즈로 리사이즈를 한 후(보통 여기까지는 공통사항이다) 각 픽셀마다 오른쪽 픽셀보다 밝은지 아닌지를 Boolean 값으로 저장하면 배열이 나오게 되는데 2진법 수로 변환할 수 있으니 이걸 그냥 hash처럼 이용하면 된다. 좀 더 사이즈를 크게 할수록 미묘한 차이도 감지할 수 있겠지만 예제에 나온 8*8이 적당한 것 같아서 그냥 그대로 썼다.

별로 대단한 건 아니지만 github에 올려뒀고 pip를 통해 설치할 수 있다.

$ pip install imagecleaner;

자연수 찾기 문제

면접관이 자연수 하나를 생각할테니 그 수를 질문을 통해 맞춰보라고 합니다. 단, 질문에 대한 답은 예/아니오밖에 존재할 수 없으며 면접관은 거짓말을 하지 않습니다.
최소한의 질문으로 수를 맞추려면 어떤 전략을 짜야 할까요?

생각한 수가 X가 맞냐는 질문을 해서 맞다는 답이 나왔을 경우 최종적으로 수를 맞췄다고 인정합니다. 예를 들면

Q: 수가 1이 맞나요?

A: No

Q: 수가 2가 맞나요?

A: Yes

위와 같은 대화에선 2번만에 수를 맞춘 것입니다.

풀이 가리기

저는 이런 방법으로 풀었습니다.

  • 바이너리 서치가 가장 빠를 것 같지만 수의 범위가 제한이 없으니 1의 자리부터 거꾸로 해나가면 된다.
  • 언제 끝날지는 모르니 중간중간 계산을 해서 그 수가 맞는 지 확인을 해봐야 한다.

제가 생각한 시나리오는 다음과 같습니다.

Q: 생각한 수를 2진수로 변환했을 때 맨 오른쪽 자릿수가 1입니까?

A: Yes

Q: 그러면 생각한 수가 1입니까?

A: No

Q: 생각한 수를 2진법으로 변환했을 때 오른쪽에서 2번째 자릿수가 1입니까?

A: Yes

Q: 그러면 생각한 수가 3입니까?

A: Yes

그래서 다음과 같은 소스가 나왔습니다.

def is_number(orig, guess):
    if orig == guess:
        return True
    return False


def is_number_nth_field_one(orig, n):
    num = orig / (2 ** (n - 1))
    if num % 2:
        return True
    return False


def guess_number(num):
    count = 0
    n = 1
    guessing = 0
    correct = False
    while not correct:
        count += 1
        if is_number_nth_field_one(num, n):
            guessing += 2 ** (n - 1)
		count += 1
		if is_number(num, guessing):
			correct = True
        n += 1
    return count

if __name__ == '__main__':
    total = 0
    for i in xrange(1, 1000001):
        count = guess_number(i)
        total += count
        print "guess %d, count: %d, total: %d" % (i, count, total)

    print "Total: %d" % total

위의 방법으로 1부터 1000000까지의 수를 모두 돌려보면 총 37902890번의 질문을 합니다.
하지만 뭔가 더 질문의 수를 줄일 수 있을 것 같았습니다.
잘 살펴보면 알겠지만, 쓸모 없는 질문이 있습니다. N번째 자릿수를 물어봤을 때 0이었다면 생각한 수를 확인하는 과정에서 아까 아니라고 한 수를 또 물어봅니다. 다음과 같은 대화가 오가는거죠.

Q: 생각한 수를 2진수로 변환했을 때 맨 오른쪽 자릿수가 1입니까?

A: Yes

Q: 그러면 생각한 수가 1입니까?

A: No

Q: 생각한 수를 2진법으로 변환했을 때 오른쪽에서 2번째 자릿수가 1입니까?

A: No

Q: 그러면 생각한 수가 1입니까?

A: No

그래서 자릿수가 0일 땐 질문을 하지 않고 바로 다음 자리수로 넘어가게 했습니다.

def is_number(orig, guess):
    if orig == guess:
        return True
    return False


def is_number_nth_field_one(orig, n):
    num = orig / (2 ** (n - 1))
    if num % 2:
        return True
    return False


def guess_number(num):
    count = 0
    n = 1
    guessing = 0
    correct = False
    while not correct:
        count += 1
        if is_number_nth_field_one(num, n):
            guessing += 2 ** (n - 1)
			count += 1
			if is_number(num, guessing):
				correct = True
        n += 1
    return count

if __name__ == '__main__':
    total = 0
    for i in xrange(1, 1000001):
        count = guess_number(i)
        total += count
        print "guess %d, count: %d, total: %d" % (i, count, total)

    print "Total: %d" % total

아까랑 아주 살짝 다르죠. 이번에는 총 28836444번 질문을 했습니다. 아까보다 훨씬 줄었죠.

이것보다 더 좋은 해법이 있는지는 모르겠습니다. 더 좋은 답이 있다면 알려주세요.