지난 번 다뤘던 import statement에 연장으로 이번에는 직접 패키지와 모듈, 함수를 만들고 import 해보겠다.
패키지 구조는 다음과 같다.
각 모듈은 다음과 같은 코드를 포함하고 있다.
def multiply(a, b):
return(a*b)
from .multiplication import multiply
# from calculator.multiplcation import multiply
def add_and_multiply(a, b):
return multiply(a, b)+(a+b)
# absolute path
# from calculator.add_and_multiply import add_and_multiply
# relative path
from .calculator.add_and_multiply import add_and_multiply
if __name__ == "__main__":
print(add_and_multiply(1, 2))
마지막으로 __init__.py
는 아무것도 기록되어 있지 않다.
__init.py
는 패키지의 초기 설정을 할 수 있는 파일이며, 패키지가 import 될 때, 자동으로 실행된다.
패키지 내부에서 특정 함수나 모듈, 변수 등을 제한하여 import 하도록 설정하고 싶은 경우 __all__
변수에 정의함으로써 가능하다.
코드를 작성한 뒤, main.py를 실행하면 에러가 발생한다.
에러가 발생한 이유는 모듈을 import 할 때 지정한 상대경로에서 상위 패키지를 찾지 못했기 때문으로 나온다.
경로는 문제가 없다. 상대경로를 사용했다는 것 자체가 문제다.
그럼 왜 문제인가?
일단 namespace 에 대해서 먼저 알아볼 필요가 있다.
namespace는 말 그대로 이름 공간이다. 변수나 함수, 모듈, 패키지 등을 정의할 때 사용된 이름이 딕셔너리 형태로 저장되어 있다.
def multiply(a, b):
return(a*b)
x = 20
dict_ = {"name": "아이유"}
print(locals()) # namespace 출력
결과를 보면 코드 내에서 정의한 변수 x, dict_ 등의 변수명을 키(key)로 하고, 그 값을 value로 하는 딕셔너리가 출력된 것을 볼 수 있다. 이처럼 해당 모듈에서 정의된 이름이 저장되는 공간이 바로 namespace 이다.
Namespace 에서 조금 더 주의 깊게 볼 변수가 __name__
변수이다.
__name__
은 해당 모듈의 이름을 나타낸다. 만약 모듈을 해당 모듈이 작성된 스크립트 안에서 실행한다면 항상 __name__ == "__main__"
이다. 그러나 다른 모듈에서 import 할 경우에는 "__main__"
이 아닌 모듈명이 출력된다.
# multiplication.py
def multiply():
print("multiplication 모듈 시작")
print("multiplication.py __name__:", __name__)
print("multiplication 모듈 종료")
# main.py
from calculator.multiplication import multiply
multiply()
print("main.py __name__:", __name__)
기존에 있던 multiplication.py 와 main.py 를 위와 같이 잠시 수정하였다. 그리고 결과를 보니 앞선 multiplication.py 의 __name__
변수에는 "__main__"
이 출력되었으나 main.py 에서 import하여 실행한 결과, 모듈명으로 바뀌어 나오는 것을 확인할 수 있다. 그리고 스크립트를 실행한 main.py의 __name__
변수에 "__main__"
값이 할당된 것을 확인할 수 있다.
이렇듯, 모듈의 외부에서 해당 모듈을 import 하는 경우 __name__
값이 변화한다.
상대경로는 실행 스크립트를 기준으로 경로를 설정하는 방식이다. 이 때, 파이썬은 모듈의 이름을 찾게 되는데 main module 에서 상대경로를 지정하면 현재 스크립트 명이 기준이 아닌 "__main__"
이라는 모듈명을 기준으로 경로를 찾는다.
그러나 사용자가 정의한 모듈명은 "__main__"
이 아니므로 경로에서 오류가 발생하게 된다. 따라서 main module 에서는 반드시 상대경로가 아닌 절대경로를 사용하여 모듈이나 패키지를 import 해야한다.
최초 main.py 코드를 다음과 같이 수정할 수 있다.
# absolute path
from calculator.add_and_multiply import add_and_multiply
if __name__ == "__main__":
print(add_and_multiply(1, 2))
> 5
이번엔 main.py 가 아닌 add_and_multiply.py 를 보겠다.
앞서 확인한 것과 마찬가지로 main module인 add_and_multiply.py 에서 multiplication 모듈의 함수를 import 하기 위해 절대경로로 지정하였다.
# from .multiplication import multiply -- 상대경로
from calculator.multiplcation import multiply #절대경로
def add_and_multiply(a, b):
return multiply(a, b)+(a+b)
> ModuleNotFoundError: No module named 'calculator'
절대경로로 지정했음에도 불구하고, 상위 패키지명인 calculator를 찾지 못하는 에러가 발생했다. 이번엔 무슨 문제일까?
절대경로와 관련해서 정리한 어제자 글을 다시 확인해보자.
"상대경로가 절대경로보다 짧고 간단하게 표현된다. 최상위 my_app 프로젝트 디렉토리와 하위 package2는 현재 스크립트를 실행하는 module4.py 의 current directory 로 sys.path 에 default로 포함되므로 굳이 지정해줄 필요가 없다. 절대경로는 바로 이 current directory에서 시작한다."
라고 내가 어제 썼다. 주목할 점은 "절대경로는 바로 이 current directory에서 시작한다." 인 것 같다. 절대경로라고 해서 무조건 최상위 디렉토리에서 시작하지는 않는다. 해당 스크립트가 속한 경로 중에서 default로 지정되는 current directory 를 파악해보면 될 것 같다.
# add_and_multiply.py
import os
print(os.path.dirname(__file__)) # 현재 스크립트가 속한 디렉토리까지의 경로
print(os.listdir(os.path.curdir)) # current directory list
print(os.listdir(os.path.pardir)) # parent directory list
> /Users/Desktop/python/practice/calculator
> ['.DS_Store', 'calculator', 'main.py']
> ['.DS_Store', 'practice']
결과를 보면 add_and_multiply.py 가 속한 디렉토리는 calculator 이며, 상위 디렉토리는 practice 이다. 즉, 절대경로의 시작점은 calculator 이므로, 경로에서 제외해야 한다. 따라서 다음과 같이 수정할 수 있다.
from multiplcation import multiply # 절대경로
def add_and_multiply(a, b):
return multiply(a, b)+(a+b)
> 5
__init__.py
마지막으로 __init__.py
를 직접 수정해보고 그 역할에 대해서 확인하겠다.
__init__.py
는 패키지의 초기 설정을 돕는다. 특히 __all__
변수를 통해서 import 가능한 모듈/함수/변수 등을 지정할 수 있다.
# multiplication.py
def multiply(a, b):
return a*b
def minus(a,b):
return b-a
# init.py
from .multiplication import multiply
__all__ = ["multiply"] # import 가능한 함수를 multiply 로 한정함
# main.py
from calculator import *
print(multiply(1,2))
print(minus(1,2))
> 2
> NameError: name 'minus' is not defined
다음과 같이 패키지 calculator 를 통해서 모듈이나 함수에 접근하는 경우, __init__.py
로 인하여 함수 minus는 사용이 불가능해진다. 그러나 해당 모듈 및 함수에 직접 접근하는 경우에는 사용 가능하다.
# main.py
from multiplication import minus
print(minus(1,2))
> 1
그럼에도 노출을 원치 않는 변수에 대해서 __init__.py
를 통해 감추고, 함수의 기능만 제공하는 방식으로 활용할 수 있다.