TIL (2020.05.29)

Awesome·2020년 5월 29일
0

TIL

목록 보기
5/46
post-custom-banner

Python

Import Packages and modules

지난 번 다뤘던 import statement에 연장으로 이번에는 직접 패키지와 모듈, 함수를 만들고 import 해보겠다.

패키지 구조는 다음과 같다.

각 모듈은 다음과 같은 코드를 포함하고 있다.

  • multiplication.py
def multiply(a, b):
    return(a*b)

  • add_and_multiply.py
from .multiplication import multiply
# from calculator.multiplcation import multiply


def add_and_multiply(a, b):
    return multiply(a, b)+(a+b)

  • main.py
# 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 에 대해서 먼저 알아볼 필요가 있다.
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 를 통해 감추고, 함수의 기능만 제공하는 방식으로 활용할 수 있다.

profile
keep calm and carry on
post-custom-banner

0개의 댓글