콘텐츠로 건너뛰기
Home » 파이썬 소스코드가 실행되는 방식과 import의 동작 원리

파이썬 소스코드가 실행되는 방식과 import의 동작 원리

<updated> 원래 이 글은 파이썬 실행하는 방법에 대한 글이었는데, 이와 관련하여 별도로 내용을 더 자세히 정리한 글이 있어서 해당 글의 링크로 대신하고, 여기서는 파이썬 소스코드가 실행되는 방식과 내가 작성한 파이썬 파일을 import 하는 방법에 대해서 알아보기로 한다.

파이썬은 기본적으로 IDLE이라는 GUI 쉘과 편집기가 결합된 도구를 제공해주고 있다. 특히 코드 에디터를 이용해서 파이썬 코드를 파일로 저장하는 것은 같은 코드를 다른 프로그램에서 다시 작성할 필요 없이 쉽게 재사용할 수 있다. 특히 이런 코드 재사용을 위해서는 소스 코드를 조리법 식으로 작성하는 것이 아니라 함수 형태로 작성한 후, 다른 소스에서 import 구문을 사용하여 반입하는 방식으로 쉽게 재사용이 가능하다.

모듈로서의 파이썬 파일

파이썬으로 작성된 파일은 기본적으로 그 자체가 모듈이 될 수 있다. 모듈은 다른 코드에서 가져다 사용하기 적합한 형태의 파이썬 파일로, 특정한 상수나, 함수, 클래스를 정의한 내용이 될 수 있다.

예를 들어 두 수의 최대공약수를 구하는 파이썬 코드를 작성했다고 가정하자. 아래는 온전한 파이썬 프로그램이기도 하다.

# 두 수를 입력받아 최대공약수를 구한다.
def gcd(a, b):
    if a < b:
        a, b = b, a
    if a % b == 0:
        return b
    return gcd(b, a % b)
x, y = [int(n) for n in input("두 수를 입력하세요:").split()[:2]]
g = gcd(x, y)
print("%d와 %d의 최대 공약수는 %d 입니다." % (x, y, g))

이 스크립트에는 두 수를 입력받아 그 둘의 최대 공약수를 구하는 gcd()라는 함수가 정의되어 있다.

그리고 이번에는 두 수를 받아서 그 최소 공배수를 구하는 프로그램을 작성한다고 생각해보자. 두 수의 최소 공배수를 구할 때에는 두 수의 최대 공약수 g를 먼저 구해서, 두 수를 나누고, 그 각각의 몫과 g를 곱하면 된다. 따라서 두 수의 최소 공배수를 구하는 프로그램에는 반드시 두 수의 최대 공약수를 구하는 함수가 포함되어야 한다.

우리는 이미 gcd.py 파일에 gcd() 함수를 정의해 놓았으니, 이를 그대로 반입하면 되지 않을까?

import 구문 : 다른 코드를 반입한다

파이썬의 import 구문은 다른 파이썬 소스나 패키지 혹은 파이썬 라이브러리의 기능 전체 혹은 일부를 현재 프로그램 코드 문맥을 가져오는 일을 한다. import gcd라고 하면 gcd.py 파일에 있는 내용 전체를 gcd라는 모듈로 현재 프로그램으로 가져온다는 뜻이다.

이 때 주의해야 할 점은 새로 작성하는 lcm.py와 위에서 작성한 gcd.py는 같은 디렉토리 내에 있어야 한다는 점이다. import하고자 하는 파일간의 위치 관계에 대해서는 아래에서 조금 더 자세히 다루도록 하겠다.

우선 우리의 lcm.py 파일은 아래와 같이 작성할 수 있다.

from gcd
def lcd(a, b):
    g = gcd.gcd(a, b)
    x, y = a // g, b // g
    return x * y * g
x, y = [int(n) for n in input("두 수를 입력하세요:").split()[:2]]
l = lcd(x, y)
print("%d와 %d의 최소 공배수는 %d 입니다." % (x, y, l))

이 파일을 lcm.py 라고 저장하고 실행해보자. 좀 이상하게 동작할 것이다. 파일을 실행하면 먼저 최대공약수를 구하는 부분이 먼저 실행되고, 그 다음에 다시 두 수를 입력받아 최소 공배수를 구하는 부분이 실행된다. 왜 이렇게 동작할까? 그리고 어떻게 이 문제를 고칠 수 있을까?

import의 동작방식

import로 다른 모듈을 반입하게 되면 파이썬 해석기는 모듈이나 패키지 이름에 기반하여 해당 모듈의 이름으로 되어 있는 디록토리나 파일을 찾기 시작한다. 해당 모듈의 파일을 찾았다면, 우선 해당 파일을 한 번 컴파일 하여 바이트코드 모듈로 만든다. 이 과정은 해당 모듈의 파일을 처음부터 끝까지 읽어서 실행하는 것과 동일하다. 따라서 gcd.py 파일은 import gcd를 수행하는 시점에 완전하게 로드되어 평가(실행)된다. 그래서 실제 우리가 원하는 동작을 하기 전에 해당 소스코드에 들어있는 내용이 전부 실행되는 것이다.

해결방법 : 모듈로 동작할때와 주 실행 프로그램으로 동작할 때를 구분하기

인터넷 상에서 참고할 수 있는 많은 소스 코드들은 대부분 위쪽에 클래스나 함수에 대한 정의가 들어오고 맨 마지막 부분에 다음과 같은 식으로 되어 있는 것을 발견할 수 있다.

if __name__ == '__main__':
    n = int(input())

바로 이 영역이 마법의 코드(?)가 된다. __name__이라는 특이한 이름의 변수는 현재 모듈의 이름을 표시한다. 어디 한 번 구분을 위해서 각각의 파일의 끝에 print(__name__) 이라고 한줄 씩 추가해보자.

두 수를 입력하세요:6 8
6와 8의 최대 공약수는 2 입니다.
gcd
두 수를 입력하세요:13 28
13와 28의 최소 공배수는 364 입니다.
__main__

import gcd로 반입되었을 때, gcd.py파일에서의 __name__ 속성은 gcd로 모듈 이름이 되었다. 그리고 lcm.py 파일의 __name__ 속성은 특이하게 "__main__"으로 출력된다. 즉 __name__ 변수값이 __main__이라는 것은 해당 파일이 모듈로 반입되는 것이 아니라 주 프로그램으로 실행되었다는 것을 의미한다.

따라서 gcd.py 프로그램은 다음과 같이 수정되어야 한다.

# 두 수를 입력받아 최대공약수를 구한다.
def gcd(a, b):
    if a < b:
        a, b = b, a
    if a % b == 0:
        return b
    return gcd(b, a % b)
if __name__ == '__main__':
    x, y = [int(n) for n in input("두 수를 입력하세요:").split()[:2]]
    g = gcd(x, y)
    print("%d와 %d의 최대 공약수는 %d 입니다." % (x, y, g))

그리고 lcm.py 파일도 아래와 같이 수정할 수 있다. 왜냐하면 다른 프로그램에서 이 함수를 쓸 수 있기 때문이다.

from gcd import gcd
def lcd(a, b):
    g = gcd(a, b)
    x, y = a // g, b // g
    return x * y * g
if __name__ == '__main__':
    x, y = [int(n) for n in input("두 수를 입력하세요:").split()[:2]]
    l = lcd(x, y)
    print("%d와 %d의 최소 공배수는 %d 입니다." % (x, y, l))

도전과제

프로젝트 오일러의 5번 문제는 1, 2, 3,.. ,20 의 어떤 수로도 나누어 떨어지는 수 중에서 가장 작은 수를 구하는 문제이다.

위에서 작성한 lcm.py 모듈을 잘 활용하면 매우 쉽게 풀 수 있는 문제이며, 정답은 여기를 참고하라.

설치된 라이브러리를 어떻게 가져올 수 있을까?

import 문은 설치된 라이브러리를 어떻게 가져올 수 있을까? 기본적인 동작으로 추측해본다면,

  1. 메인 프로그램으로 실행중인 파이썬 스크립트와 같은 폴더내에서 모듈을 검색한다.
  2. 파이썬 설치 폴더 내의 라이브러리 폴더(site-packages)내에서 모듈/패키지를 검색한다.

이렇게 동작하는 것처럼 보인다.

그리고 결론은 거의 비슷하다…이다. 파이썬을 위한 PYTHONPATH라는 환경변수가 있고, 여기에 등록된 경로들과 현재 디렉토리를 기준으로 모듈을 찾는다. 하지만 윈도에서는 기본적으로 이 환경변수가 지정되어 있지 않다. 대신에 sys.path라는 변수에 import를 위해 뒤져봐야 할 경로들이 등록되어 있다.

다음은 Anaconda로 설치된 ipython에서 sys.path 값을 확인한 내용이다.

In [1]: import sys
In [2]: sys.path
Out[2]:
['',
 'e:\\Anaconda3\\Scripts',
 'e:\\Anaconda3\\python36.zip',
 'e:\\Anaconda3\\DLLs',
 'e:\\Anaconda3\\lib',
 'e:\\Anaconda3',
 'e:\\Anaconda3\\lib\\site-packages',
 'e:\\Anaconda3\\lib\\site-packages\\Sphinx-1.5.6-py3.6.egg',
 'e:\\Anaconda3\\lib\\site-packages\\win32',
 'e:\\Anaconda3\\lib\\site-packages\\win32\\lib',
 'e:\\Anaconda3\\lib\\site-packages\\Pythonwin',
 'e:\\Anaconda3\\lib\\site-packages\\setuptools-27.2.0-py3.6.egg',
 'e:\\Anaconda3\\lib\\site-packages\\IPython\\extensions']

맨 처음에 등장하는 빈 값은 현재 디렉토리를 의미하며, 그외에는 아나콘다 설치 디렉토리 내에 여러 모듈들이 설치된 디렉토리들이 표시된다.

pipeasy-install로 설치된 외부 라이브러리들은 모두 이러한 내부 패키지 폴더 내에 설치되기 때문에, 이를 호출하는 파이썬 파일이 어디에 있는지 상관없이 모두 호출가능하게 된다.

초보자가 하기 쉬운 실수

파이썬이 외부 모듈을 반입하기 위해 뒤져보는 폴더는 현재 위치를 최우선으로 sys.path에 등록된 위치들을 순서대로 찾게 된다. 그리고 그 중에서 주어진 이름과 일치하는 파일이나 패키지를 찾으면 이 내용을 읽어서 컴파일, 로드하게 된다.

많은 초보자들이 여기서 실수를 하는데, 바로 기본 라이브러리와 같은 이름의 예제 파일을 만들어 놓고, 같은 디렉토리의 다른 파일에서 그 이름을 import 하는데 사용하는 것이다.

이 경우, 모듈은 임포트되었으나 올바른 모듈이 임포트 되지 않았으므로 애트리뷰트를 찾을 수 없다는 에러를 만나게 된다.