[Python] Module

Falcon·2024년 12월 4일

python

목록 보기
1/2
post-thumbnail

예제 미리보기

src
├── calc
│   ├── __init__.py
│   └── calculator.py
├── client
│   └── client.py
│   └── sys_path_test.py

calc/calculator.py

def add(a: int, b: int)-> int:
    return a + b

client/client.py

# 절대 경로 참조
from calc.calculator import add

print(__name__)
def calc_sum():
    a = 1
    b = 2
    print(add(a,b))

calc_sum()

실행 결과

$ python client/client.py
ModuleNotFoundError: No module named 'calc'

모듈 calc 를 찾지 못한다.
왜 그럴까?

Python 실행 Context

파이썬은 실행 대상 파일의 패키지를 Current Working Directory(CWD) 이자 top-level script 로인식한다.

python client/client.py 실행시
client 를 CWD , top-level package로 인식한다.

-> from calc.calculator 구문은 프로젝트 src/calc 대신 client/calc 를 참조 하려한다.

-> 참조 실패: ModuleNotFoundError

Q. 이상한데요? 그럼 엉뚱한 곳에 위치해서 실행시키면요?
A. 무적권 실행 인자 (대상 파일)가 위치한 디렉토리가 곧 sys.path 의 첫번째 값이 된다.

sys.path 는 뭐야?

import 할 때 모듈을 찾을 경로를 저장해둔 리스트.
OS에 환경변수 PATH가 있다면
Python 에는 모듈 디렉토리를 모아둔 sys.path 가 있다.

sys_path_test.py

import sys

print(sys.path)

위 코드를 실행시켜보자.

$ pwd
%USER_HOME%/my_project
$ python src/sys_path_test.py

실행 결과

[
'C:\\Users\\m_falocn\\IdeaProjects\\python-practice\\src\\module\\client', 
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313\\python313.zip',
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313\\DLLs',
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313\\Lib',
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313'
]

파이썬은 sys.path 리스트의 0번째 원소부터 차례대로 해당 모듈을 찾는다.

즉, 위 결과에 따르면
1. python-practice\\src\\module\\client
2. AppData\\Local\\Programs\\Python\\Python313\\python313.zip
3. AppData\\Local\\Programs\\Python\\Python313\\DLLs,
4. AppData\\Local\\Programs\\Python\\Python313\\Lib
5. AppData\\Local\\Programs\\Python\\Python313

위 순서대로 모듈을 찾고 없으면 ModuleNotFoundError 를 뱉는다.

sys.path 의 첫 번째 값은 Python 스크립트를 실행한 CWD 가 아닌, 그 스크립트가 위치한 디렉터리가 된다.

위 코드를 실행시켜보자.

$ pwd
%USER_HOME%/my_project
$ python src/sys_path_test.py

$ pwd
%USER_HOME/src
$ python sys_path_test.py

둘다 같은 결과를 갖는다.

top-level package

Root 가 되는 package다.

sound/               Top-Level Package
    main.py
    formats/
        a.py
	effects/
		b.py
	filters/
		c.py

안타깝게도 아래와 같이 실행하면
formats/a.py 에서 effects/b.py 참조가 불가능하다.

a.py

from ..effects.b import b_method

b_method()

모듈은 절대 top-level package 를 넘어서서 참조할 수 없다.

⚠️ ImportError: attempted relative import beyond top-level package

왜 에러가나지?

sound 가 Top-Level Package 로 인식되지 않기 때문이다.
파이썬 인터프리터는 sound 패키지가 존재한다는 사실조차 모른다.

왜 인식을 못하지?
이를 이해하기 위해선 top-level script, __main__ 이 무엇인지 알아야한다.

top-level code - __main__

# main.py 는 top-level code 다.
$ python main.py

python 실행 대상 스크립트를 Top-level code 라고 한다.
top-level code 외에 top-level script, EntryPoint 라고도 부른다.

top-level code 는 항상 __name__ 값을 __main__ 으로 갖는다.

print(__name__) # '__main__'

참조: python - main docs, Python - package reference docs

__name__ 은 패키지명 + 모듈명이다.

예제를 통해 알아보자.

디렉토리 구조

sound/            
    main.py
    formats/
        a.py
	effects/
		b.py
	filters/
		c.py

effects/b.py

print(__name__)  # effects.b

실행 결과effects.b 에서 effects 는 패키지명 b는 모듈명이다.
이처럼 __name__<package-name>.<package-name>.<module-name> 형태로 패키지명과 모듈명을 모두 갖는다.

복습해보자. top-level script 는 특수한 __name__ 을 갖는다.

$ python main.py
print(__name__) # __main__

실행 결과가 __main__이 된다.
어? __name__ 곧 패키지명 + 모듈명이라 했다.
Q. 그럼 이녀석의 패키지명은 무엇인가?
A. 없다!

즉, 요녀석은 패키지명 없이 오로지 특수 모듈명 __main__ 을 갖는다.

top-level script , EntryPoint 는 __name__ 값이 __main__로 잡힌다.
패키지명이 없다. 오로지 특수 모듈명만 갖는다.
-> 패키지명을 파이썬 인터프리터가 해석할 수 없다.

ImportError: attempted relative import beyond top-level package

눈치 챘는가? 이 에러의 원인은
"패키지명을 파이썬 인터프리터가 해석할 수 없음" 이다.

sound/            
    main.py
    formats/
        a.py
	effects/
		b.py
	filters/
		c.py
$ python main.py

파이썬 인터프리터가 sound 라는 패키지명을 알아낼 재간이 없다.

effects/b.py 에서 .. 상대경로로 이동하고 다시 formats/a.py 로 가려면
도중에 sound 라는 패키지가 인식되어야 한다.
ex) ..effects -> sound -> sound/formats
근데 파이썬 인터프리터가 sound 의 존재를 알 수 없다.

그래서 이런 에러가 나는거다.

ImportError: attempted relative import beyond top-level package

python -m 옵션은 무슨 용도인가?

$ python -m client.client # 정상 실행

-m 옵션 추가시 파이썬이 현재 디렉토리(CWD)를 sys.path 첫 번째 원소로 추가한다.
또, 이를 top-level package 로 간주한다.

Q. 이 경우엔 top-level package 가 어디로 인식되나요?
A. 실행 위치인 src 가 된다.
따라서 src/calc 를 정상 참조한다.

-m 옵션과 함께 코드를 다시 실행시켜보자.

$ pwd
%USER_HOME%/python-practice
$ python -m src.client.sys_path_test

실행 결과

[
'C:\\Users\\m_falocn\\IdeaProjects\\python-practice\\src', 
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313\\python313.zip',
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313\\DLLs',
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313\\Lib',
'C:\\Users\\m_falocn\\AppData\\Local\\Programs\\Python\\Python313'
]

-m 옵션이 없다면 sys_path_test.py 가 위치한 \\src\\client 가 첫 경로가 된다.
-m 옵션을 실행하니 CWD 인 srcsys.path 의 첫번째 경로로 지정됐다.

반복한다.

-m 옵션 추가시 현재 디렉토리(CWD)가 곧 sys.path에 추가되어 top-level package 가 된다.


__init__ 은 무슨 용도인가?

디렉토리를 패키지로 인식할 수 있게하는 파일이다.

__name__ 은 왜, 언제 쓰나?

  • 파이썬 인터프리터가 패키지명과 모듈명을 인식하는데 쓰인다.
  • top-level script (보통 main.py) 와 일반 모듈파일을 구분하기 위해 쓰인다.

Conclusion

ImportError 를 피하고 싶다면, main.py 를 프로젝트 루트로 빼고 나머지 모듈은 상대경로 참조를 써라.

이게 나의 결론이다.
나는 절대경로를 구구절절 다 쓰는 것보단, 상대경로 참조를 선호한다.

AS-IS

sound/            
    main.py
    formats/
        a.py
	effects/
		b.py
	filters/
		c.py

main.py 를 루트로 빼자.

TO-BE

main.py
sound/            
    formats/
        a.py
	effects/
		b.py
	filters/
		c.py

이제 python main.py를 실행할 때 sound 를 찾을 수 있게되었다.

profile
I'm still hungry

0개의 댓글