자주 하는 실수

../_images/34435688380_b5a740762b_k_d.jpg

대부분의 경우에서 파이썬의 목표는 깔끔하고 일관적이라 깜짝 놀랄 일이 일어나지 않는 언어를 목표로 합니다. 하지만 초보자에게 혼란을 일으킬 수 있는 몇 가지 케이스가 있습니다.

어떤 것들은 의도된 바입니다. 하지만 놀랄 수도 있습니다. 어떤 것들은 필시 언어의 암적인 부분입니다. 다음은 언뜻 보기에는 이상해 보일 수 있지만 그 깜짝 놀랄만한 원인을 알고 나면 합리적이라 생각할 수도 있는 기묘한 동작들의 모음입니다.

가변 디폴트 전달인자(Mutable Default Arguments)

파이썬 초보자가 보기에 가장 놀랄만한 것은 파이썬의 함수 선언에서의 가변 디폴트 전달인자(Arguments)입니다.

이렇게 작성하면

def append_to(element, to=[]):
    to.append(element)
    return to

요렇게 동작하기를 기대할텐데

my_list = append_to(12)
print(my_list)

my_other_list = append_to(42)
print(my_other_list)

두 번째 전달인자가 없다면 함수가 호출될 때마다 매번 새 리스트를 만들테니 그 결과는 이럴거라 생각하지만:

[12]
[42]

실제로는

[12]
[12, 42]

새 리스트는 함수를 정의할 때 한번만 만들어지고 함수가 호출될 때마다 같은 리스트를 계속 사용합니다.

파이썬의 디폴트 전달인자는 함수가 정의될 때 한번만 평가됩니다. (루비처럼) 함수가 실행될 때마다 평가되는 게 아닙니다.이 말인즉 함수에서 디폴트 전달인자를 변경하면 그 함수가 실행될 때마다 앞으로 그 객체를 계속 수정한다는 것입니다.

대신 이렇게 하세요

디폴트 전달인자가 없다는 표지를 만들어 함수가 호출될 때마다 매번 새 객체를 생성하도로 하세요.(None 이 좋을겁니다).

def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to

리스트 객체를 두번째 전달인자로 넣는 걸 잊지 마세요.

실수가 실수가 아닌 때

가끔은 함수를 호출할 때마다 그 상태를 유지하기 위하여 특별히 위 동작을 "이용" (의도된 것입니다, 라 읽어요) 할 수도 있습니다.

늦은 바인딩 클로져(Late Binding Closure)

또다른 혼란의 근원은 파이썬이 클로져(또는 클로져를 둘러싸는 글로벌 스코프)에서 그 변수를 바인딩하는 방법입니다.

이렇게 작성하면

def create_multipliers():
    return [lambda x : i * x for i in range(5)]

요렇게 동작하기를 기대할텐데

for multiplier in create_multipliers():
    print(multiplier(2))

리스트는 각각 i 변수를 독립적으로 가지는 5개의 함수를 구성요소로 가지며 인자를 곱해서 이런 결과를 낼거라 생각하지만

0
2
4
6
8

실제로는

8
8
8
8
8

5개의 함수가 만들어졌습니다. 그리고 모든 함수는 그저 x 를 4로 곱합니다.

파이썬의 클로저는 늦은 바인딩 을 합니다. 그 말인즉 클로저 안에서 사용되는 변수값은 안쪽 함수가 호출될 때 비로소 정해진다는 것입니다.

여기서 리턴된 함수는 무엇이든 호출될 때마다 그 호출 시점에서 바깥 스코프의 i 값을 조회합니다. 그리하여 반복문이 끝나고 i 는 반복문에서의 마지막 값인 4가 되는 것입니다.

이 문제에 대해 특히 끔찍한 일은 이것이 파이썬의 lambdas <python:lambda>`와 관련이 있다는 잘못된 정보가 널리 퍼져있다는 것입니다.``람다` 표현식으로 만들어진 함수라고 특별한 게 아닙니다. 사실 아래와 같은 일반적인 def 와 똑같이 동작합니다:

def create_multipliers():
    multipliers = []

    for i in range(5):
        def multiplier(x):
            return i * x
        multipliers.append(multiplier)

    return multipliers

대신 이렇게 하세요

가장 많이 쓰이는 해결책은 필시 약간의 해킹입니다. 디폴트 전달인자가 함수에서 어떻게 평가되는지에 대하여 위에서 소개한 파이썬의 동작(see 가변 디폴트 전달인자(Mutable Default Arguments))을 응용하면 됩니다. 다음과 같이 디폴트 전달인자를 사용해서 전달인자에 즉시 바인딩 되는 클로져를 만들 수 있습니다.

def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]

이 방법 대신 functools.partial 함수를 사용해도 됩니다.

from functools import partial
from operator import mul

def create_multipliers():
    return [partial(mul, i) for i in range(5)]

실수가 실수가 아닌 때

가끔은 일부러 클로져를 이런 방식으로 사용하고 싶을 때도 있습니다. 늦은 바인딩이 도움되는 경우가 많습니다. 반복문을 돌려서 유니크한 함수를 만드는 것은 불운한 딸꾹질을 일으킬 수 있습니다.

바이트코드 (.pyc) 파일로 가득찼어!

기보적으로 파일에서 파이썬 코드를 실행하면 파이썬 인터프리터는 자동적으로 파일의 바이트코드 버전을 만들어서 디스크에 쓸 겁니다(예를 들면 module.pyc).

.pyc 파일이 소스코드 저장소에 들어가면 안됩니다.

이론상으로 이 동작은 성능상의 이유 때문에 늘 켜져 있습니다. 이러한 바이트코드 파일이 없다면 파이썬은 그 파일을 실행할 때마다 바이트코드를 다시 생성할 것입니다.

바이트코드 (.pyc) 파일을 만들지 않는 설정하기

다행히 바이트코드를 생성하는 프로세스는 아주 빠릅니다. 그러니 개발하는 동안 바이트코드 생성하는 시간이 오래걸릴까봐 걱정할 필요가 없습니다.

이 귀찮은 파일들을 다 지워버립시다!

$ export PYTHONDONTWRITEBYTECODE=1

환경변수에 $PYTHONDONTWRITEBYTECODE 이라 지정해놓으면 파이썬은 더이상 이 파일들을 디스크에 쓰지 않습니다. 그리고 개발환경은 깨끗해집니다.

이 환경변수를 ~/.profile 에 세팅하기를 추천드립니다.

Bytecode (.pyc) 파일 삭제하기

이미 이 파일들이 존재한다면 한꺼번에 몽땅 삭제하는 멋진 방법이 있습니다.

$ find . -type f -name "*.py[co]" -delete -or -type d -name "__pycache__" -delete

프로젝트의 최상위 디렉토리에서 위 명령어를 실행하면 모든 .pyc 파일이 사라집니다. 한결 낫구만.

버전 컨트롤에서 무시하기

그래도 성능 상의 이유 때문에 .pyc 파일이 필요하다면 버전 컨트롤 저장소의 ignore 파일에 추가해도 됩니다. 대부분의 버전 컨트롤 시스템이 ignore 파일에서 와일드카드를 사용하는 특수 규칙을 적용할 수 있습니다.

ignore 파일이 이 규칙과 맞아떨어지는 파일들을 저장소에 들어가지 않도록 해줍니다. Git.gitignore 을 사용하고, Mercurial.hgignore 을 사용합니다.

ignore 파일은 최소 이런 모양일 겁니다.

syntax:glob   # This line is not needed for .gitignore files.
*.py[cod]     # Will match .pyc, .pyo and .pyd files.
__pycache__/  # Exclude the whole folder

필요에 따라 더 많은 파일과 디렉토리를 포함 할 수 있습니다. 다음에 저장소에 커밋 할 때 이러한 파일은 포함되지 않습니다.