Python

-m 실행 옵션과 __name__

둔진 2020. 5. 28. 02:43

  Python 관련 글들을 보다 보면 스크립트를 실행시킬 때 어떤 때는 python3 script.py처럼 실행시키고, 어떤 때는 python3 -m script처럼 -m 옵션을 사용해서 실행시킬 때가 있습니다. 차이는 무엇일까요? 전자는 script.py를 실행시키는 것이고, 후자는 script라는 이름의 모듈을 sys.path에서 찾아서 실행시키는 것입니다. sys.path에 대한 설명은 알쏭달쏭 Python import - sys.path을 참고하세요.

 

  오늘도 간략한 예시를 들어서 알아보겠습니다.

1. 간단한 함수부터

  아래와 같은 Python 코드를 만들고 있다고 가정해보겠습니다. 주어진 문자열 주위를 아름답고 화려한 *으로 감싸주는 코드입니다.

import sys

def print_with_border(s):
    print('*' * (len(s) + 4))
    print('* ' + s + ' *')
    print('*' * (len(s) + 4))
    
print_with_border(sys.argv[1])
➜   python3 add_border.py 'Beautiful world!'
********************
* Beautiful world! *
********************

  잘 되는군요. 불현듯 print_with_border를 여기에서만 쓰기는 아깝다는 생각이 듭니다. 그래서 여러곳에서 사용하기로 합니다. 

2. 문제의 시작

  add_border.print_with_border 함수를 사용하는 main.py 를 아래와 같이 만듭니다.

import add_border

add_border.print_with_border('Wonderful day!')
➜   python3 main.py
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    import add_border
  File "/Users/ceongjeein/Workspace/pydev/add_border.py", line 8, in <module>
    print_with_border(sys.argv[1])
IndexError: list index out of range

안 되네

  에러가 발생합니다. 그것도 심지어 첫번째 줄인 import add_border에서 에러가 발생합니다. 조금 더 들여다보니 add_border.py에서 에러가 발생했군요. 원인은 import 문이 작동하는 방식에 있습니다. import 문은 느낌상 코드를 가져온다는 느낌적인 느낌이 있는데, 실제로는 이 과정에서 해당 코드 전체를 실행합니다. 즉, add_border.py의 8번째 줄인 print_with_border(sys.argv[1])를 실행하려는데, sys.argv[1]이 없기 때문에 에러가 발생한 거죠.

3. 모듈화의 시작

  언젠가 소프트웨어의 모듈화는 좋은 것이라는 이야기를 들었던 것이 불현듯 떠오릅니다. 그래서 코드를 아래와 같이 나눕니다.

 

  * add_border.py

import sys

def print_with_border(s):
    print('*' * (len(s) + 4))
    print('* ' + s + ' *')
    print('*' * (len(s) + 4))
    

  * add_border_exec.py

import add_border
import sys

add_border.print_with_border(sys.argv[1])

  * main.py

import add_border

add_border.print_with_border('Wonderful day!')

  그리고 아래와 같이 실행해보니 기대대로 아주 잘 작동합니다.

➜  python3 add_border_exec.py 'Beautiful world!'
********************
* Beautiful world! *
********************

➜   python3 main.py
******************
* Wonderful day! *
******************

4. 파편화?

  하지만 어딘가에서 같은 기능은 같이 있어야 한다는 이야기를 들은 것이 떠오릅니다. add_border_exec.py의 내용을 분리하는 것도 나쁘지 않지만, 앞으로 유지보수를 위해서 print_with_border 함수와 같이 있는 것이 좋을 것 같은 생각이 듭니다.

  이때 __name__이 등장합니다. __name__에 대한 자세한 설명은 알쏭달쏭 Python import - sys.path에 있습니다. 요약하자면 현재 파일이 직접 실행됐다면 __name__의 값은 __main__이 되고, 다른 파일에 의해서 import 됐다면 모듈의 이름이 됩니다. 예를 들어 add_border.py의 경우,

  • python3 add_border.py라고 실행됐다면 __name__ == '__main__'이 되고,
  • 어딘가에서 import add_border.py를 만나서 실행됐다면, __name__ == 'add_border'가 됩니다.

  바로 이 __name__을 이용해서 현재 파일이 직접 실행된 것인지, import 된 것인지를 판단해서 동작을 나눌 수 있습니다.

  앞에 코드 중 add_border.py를 아래와 같이 수정합니다.

import sys

def print_with_border(s):
    print('*' * (len(s) + 4))
    print('* ' + s + ' *')
    print('*' * (len(s) + 4))

if __name__ == '__main__':    
    print_with_border(sys.argv[1])
➜   python3 add_border.py 'Beautiful world!'
********************
* Beautiful world! *
********************

➜   python3 main.py
******************
* Wonderful day! *
******************

  이제 파일 하나가 스크립트처럼도 작동하고 모듈처럼도 작동합니다.

5. 드디어 -m 옵션

  -m 옵션이 하는 일은 처음에 설명한 대로, 실행할 Python 스크립트를 찾을 때 sys.path에서 찾는다는 점이 다릅니다. 나머지 동작은 앞에서 설명한 것과 같습니다. sys.path에서 찾는다는 것은 import 때의 규칙을 그대로 따른다는 말이고요. 알쏭달쏭 Python import - sys.path서 설명한대로 스크립트가 위치한 디렉터리가 sys.path에 추가되기 때문에 아래와 같이 실행할 수도 있습니다.

➜   python3 -m add_border 'Beautiful world!'
********************
* Beautiful world! *
********************

  자세히 보시면 -m 옵션을 주고 add_border.py가 아닌 그냥 add_border를 사용했습니다. -m이 원하는 값은 파일명이 아니고 모듈 이름이기 때문입니다.

 

  갑자기 의문이 듭니다. python3 add_border.py 'Beautiful world!'라고 그냥 하며 되는데 왜 굳이 헷갈리게 이렇게 할까요.

  -m 옵션은 모듈이 배포되었을 때 힘을 발휘합니다. 예를 들어, add_border가 인기가 엄청 많아져서 배포를 하기로 했고 많은 사람들이 pip를 통해서 설치했다고 가정해보겠습니다. 그때마다 사용자들이 add_border.py의 위치를 찾아서 python3 /usr/local/somewhere/anywhere/add_border.py 'Beautiful world!' 라고 실행할 수는 없습니다. 사용하는 환경마다 실제로 모듈이 설치되는 위치가 다르니까요. 대신 python3 -m add_border 'Beautiful world!'를 사용하면 Python이 add_border 모듈을 찾는 일을 해주기 때문에 문제를 해결할 수 있습니다.

 

  실제로 많은 Python 모듈들이 __name__ == '__main__' 을 사용해서 모듈에 실행 가능한 기능을 추가해서 배포하고 있습니다. venv, site 등이 그렇습니다.