코드로 우주평화

파이썬에서 직접 만든 패키지를 불러오자. (feat. 절대 경로와 상대 경로) 본문

나는 이렇게 학습한다/Language

파이썬에서 직접 만든 패키지를 불러오자. (feat. 절대 경로와 상대 경로)

daco2020 2021. 11. 6. 12:04

파이썬에 내장된 패키지나 모듈은 build-in modules에, pip 등으로 다운로드한 패키지나 모듈은 sys.path에 저장되기 때문에 쉽게 이름만으로 불러올 수 있지만, 직접 만든 로컬 패키지의 경우 '절대 경로', '상대 경로'에 따라 다음과 같은 import 에러 메시지를 만나게 될 수 있다.

from .package import module2

>>>

ImportError: attempted relative import with no known parent package
#ImportError: 알려진 상위 패키지가 없는 상대 가져오기 시도

이 문제를 해결하기 위해 절대 경로와 상대 경로에 대해 알아보고 어떤 식으로 import 해야 하는 살펴보자.

 

우선 아래 처럼 project1 안에 주로 실행할 main.py을 만들고, package라는 패키지를 만들었다.

패키지 안에는 해당 폴더가 패키지임을 알려주는 init 파일과 2개 의 모듈이 있다.

우리가 주로 실행할 파일은 main.py 이기 때문에 해당 파일로 패키지에 있는 모듈을 불러와 보겠다.

# main.py
# 상대 경로의 경우

from .package import module2 # . 기호는 현재 디렉토리, .. 기호는 부모 디렉토리를 의미함.
print(module2.multiply(1,2)) # 모듈 안에 있는 함수를 실행 및 출력.

>>>

ImportError: attempted relative import with no known parent package
#ImportError: 알려진 상위 패키지가 없는 상대 가져오기 시도

main.py 에서 package 안에 있는 module2를 불러오려고 상대 경로를 사용했다.

. 을 이용해 main.py와 동일 디렉토리에 있는 package, 그 안에 있는 module2를 불러오도록 하였다.

하지만 "알려진 상위 패키지가 없는 상대 가져오기 시도"라는 에러 메시지가 뜬다. 왜일까? 코드를 수정하여 절대 경로도 살펴보자.

# main.py
# 절대 경로의 경우

from package import module2 
print(module2.multiply(1,2)) # 모듈 안에 있는 함수를 실행 및 출력.

>>>

2 # 정상적으로 모듈이 실행되어 출력되었다.

절대 경로로 수정하니 정상적으로 import 되어 코드가 실행되는 것을 볼 수 있다. 

이 이유에 대해 파이썬 공식 문서에서는 다음과 같이 설명하고 있다.

 

 

상대 임포트가 현재 모듈의 이름에 기반을 둔다는 것에 주의하세요. 메인 모듈의 이름은 항상 "__main__" 이기 때문에, 파이썬 응용 프로그램의 메인 모듈로 사용될 목적의 모듈들은 반드시 절대 임포트를 사용해야 합니다.

 

 

 

직접 실행하는 메인 모듈의 이름은 "__main__"으로 바뀐다고 한다. 상대 경로는 "__name__"이라는 자체 변수명에 의해 위치를 파악하여 경로를 계산하게 되는데, 메인 모듈(직접 실행하는 파일)의 "__name__" 이 "__main__"가 되면 실행 파일의 실제 위치를 '상대 경로'가 인식할 수 없게 된다. (상대 경로는 반드시 기준 위치를 알아야 한다. 기준 위치로부터 다른 파일의 위치를 추적하기 때문이다.)

 

결국 main.py는 시스템상 package와 동일 선상에 있지만, 막상 package를 import를 하려고 하면 동일 선상이 아니라고 하는 것이다. 그리고 이는 main.py 뿐만 아니라 다른 모듈도 마찬가지이기 때문에 어떤 모듈이든 직접 실행하면 상대 경로는 사용할 수 없게 된다. 

 

 

한마디로 정리해보자면 다음과 같다.

"모듈을 직접 실행하면 해당 파일의 이름이 달라지므로 [상대 경로]가 기준 위치를 알 수 없게 되어 경로를 찾지 못한다."

 

 

 

이런 이유 때문에 파이썬에서 메인 모듈(직접 실행하는 파일)은 반드시 절대 경로를 사용해야 한다.

 


 

이해를 돕기 위해 '절대 경로'와 '상대 경로'에 대해 알아보자.

Absolute path(절대 경로)

  • 현재 작업 디렉토리와 상관없이 절대적인 파일의 위치를 가리키는 경로. 
  • 파이썬의 경우 프로젝트의 최상위 디렉토리를 시작으로 아래의 패키지, 모듈, 함수나 변수, 클래스 등등을 경로로 지정할 수 있다
  • 예) 아래 코드는  project1(폴더) -> package(패키지) -> module2(모듈) -> multiply(함수)의 절대 경로이다.
# main.py

from package.module2 import multiply
# 프로젝트1 폴더 안에 패키지 안에 모듈2 안에 multiply 라는 함수를 가져와라.

Relative path(상대 경로)

  • 현재 위치한 디렉토리를 기준으로 상대적인 파일의 위치를 가리키는 경로
  • 상대 경로는 기준 경로(현재 위치한 디렉토리)를 기준으로 경로가 구성되기 때문에 기준 위치에 대한 정보가 반드시 필요하다.
  • 예) 아래 코드는 module1이 module2를 상대 경로로 import 하는 코드이다.
# module1.py

from .module2 import multiply

def add_and_multiply(a,b):
    return multiply(a,b) + (a+b)
    
>>>
ImportError: attempted relative import with no known parent package
# 직접 실행하면 __name__ 이 __main__ 으로 바뀌기 때문에 상대 경로 사용불가

참고로 해당 파일을 직접 실행하면 메인 모듈로 바뀌기 때문에 상대 경로는 불러올 수 없게 된다. 

이 상태에서 main.py로 가보자

# main.py

from package.module1 import add_and_multiply
print(add_and_multiply(1,2))

>>>

5

module1.py 는 상대 경로를 사용하고 있지만 직접 실행은 main.py에서 하고 있으므로 module2의 multiply 함수가 정상 작동하는 것을 볼 수 있다.

 

절대 경로와 상대 경로의 차이점은?

  1. 경로를 기입할 때 최상위 디렉토리를 포함되는가? 
    절대 경로는 최상위 디렉토리부터 경로를 포함하지만 상대 경로는 현재 디렉토리 기준으로 작성하기 때문에 상대적으로 경로명이 짧다
  2. 경로에 대한 비교 대상이 있는가?
    절대 경로는 비교대상이 없지만 상대 경로는 현재 디렉토리의 위치를 기준으로 잡기 때문에 기준이 달라지면 경로를 찾을 수 없다.

 


+ 추가 내용

패키지에 포함되는 __init__ 파일의 용도는 무엇일까?

  • __init__.py 는 해당 디렉토리가 패키지임을 알려주는 역할을 한다. (내용은 비워져 있어도 됨)
  • 또한 main.py 에서 package를 import 하면 자동으로 __init__.py 파일을 불러오기 때문에 만약 __init__.py 안에 패키지 내 다른 모듈들을 미리 import 해둔다면, main.py에서 다른 모듈들을 import하지 않아도 사용할 수 있게 된다. 
  • 예를 들어, package만 import 하고 따로 module2를 import 하지 않으면 아래처럼 오류가 뜬다. 
    왜냐하면, __init__.py 에는 현재 module2의 속성을 가지고 있지 않기 때문이다.
# __init__.py


# main.py

import package
print(package.module2.multiply(1,3))
>>>
AttributeError: module 'package' has no attribute 'module2'
# package 에서 module2 속성이 없습니다.

하지만 아래처럼 __init__.py 에 미리 module2를 import 해두면 main.py에서 module2를 따로 불러오지 않아도 사용이 가능하다.

# __init__.py

from . import module2


# main.py

import package
print(package.module2.multiply(1,3))
>>>
3

이 기능을 활용하면 패키지 하나만 임포트 하는 것만으로 내가 원하는 다른 모듈들을 불러와 사용할 수 있다.

 

마지막으로 __init__.py 파일에 현재 어떤 속성이 반영되어 있는지 알려면 'dir()'을 사용하면 된다.
이를 통해 불러와야 할 모듈이 잘 반영되어 있는지 확인할 수 있다.

# __init__.py

import module2
print(dir())

>>>
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'module2']
# 'module2'가 마지막에 추가된 것을 볼 수 있다.