2024년 11월 24일 일요일

파이썬 상대 경로 임포트

ImportError: attempted relative import with no known parent package
임포트를 상대 경로로 지정하면 편할 것 같아 써보니 이런 에러가 난다.
검색해보면 -m 옵션을 쓰면 된다는데, 정리할겸 테스트를 해봤다. 
다음과 같이 준비를 한다.
$ mkdir -p test/src/foo
$ cd test/src
$ touch foo/{__init__,bar}.py
$ cat << EOF > main.py
from foo import bar

def main():
    print('ok')

if __name__ == '__main__':
    main()
EOF

디렉토리 구조는 이렇게 된다. main.py 이외는 빈 파일이다.
test
└── src
    ├── foo
    │    ├── __init__.py
    │    └── bar.py
    └── main.py

1. 먼저 src/에서 실행해 본다.
$ python main.py     - ok
$ python -m main     - ok
잘 동작한다.
이 번에 하나 위에서 실행해 본다.
$ cd ..
$ python src/main.py # ok
$ python -m src.main # ModuleNotFoundError: No module named 'foo'
foo는 이 디렉토리엔 없다. 하나 아래인 src/에 있다.
앞의 3개는 main.py가 있는 곳에서 임포팅을 시도하는 것 같은데,
마지막 것은 현제 디렉토리에서 하고 있는 듯 하다.

2. 이 번엔 foo 앞에 .을 붙여 본다. (상대 경로 지정)
sed -i 's/foo/.&/' src/main.py

src/에서 실행해 보면,
$ cd src
$ python main.py     # ImportError: attempted relative import with no known parent package
$ python -m main     # ImportError: attempted relative import with no known parent package
문제의 임포트 에러가 나왔다.
하나 위에서 실행해 본다.
$ cd ..
$ python src/main.py # ImportError: attempted relative import with no known parent package
$ python -m src.main # ok
1에서 성공했던 앞의 세 명령은 실패했고, 실패했던 마지막 명령은 성공했다.

3. 하는 김에 src도 붙여 본다.
$ sed -i 's/\.foo/src&/' src/main.py
$ cd src
$ python main.py     # ModuleNotFoundError: No module named 'src'
$ python -m main     # ModuleNotFoundError: No module named 'src'
$ cd ..
$ python src/main.py # ModuleNotFoundError: No module named 'src'
$ python -m src.main # ok
앞의 두 명령은 src가 없으니 그렇다쳐도 3번째 명령도 src가 없다고 한다.

지금까지만 보면 python으로 실행하면 임포트 기준이 대상 파일이 있는 디렉토리고
python -m 으로 실행하면 명령을 실행하는 디렉토리인 것 같다.

4. 하나 더 올라가보면
$ cd ..
$ python -m test.src.main # ModuleNotFoundError: No module named 'src'
$ sed -i 's/src.foo/.foo/' test/src/main.py 
$ python -m test.src.main # ok
test/ 위로 올라오니 src가 없다고 에러가 났다.
src없이 상대 경로로 바꾸니 정상 동작을 한다.
상대 경로의 좋은점이다.

지금까지의 테스트를 보면
실행(?) 파일이 있는 디렉토리를 기준으로 임포트 경로를 잡으면 어디에서 실행시키든 문제가 없다.
상대 경로를 쓸거면 -m 옵션으로 실행하고 실행 디렉토리가 임포트의 기준(top-level)이 된다.

5. 하나를 더 보면
$ cd test
$ mkdir lib
$ touch lib/__init__.py
$ sed -i '1i from .. import lib' src/main.py
$ python -m src.main # ImportError: attempted relative import beyond top-level package
src와 같은 레벨에 있는 lib을 상대경로로 지정하면 top-level을 넘을 수 없다고 나온다.
$ cd ..
$ python -m test.src.main # ok
이 경우 src와 lib를 포함하도록 하나 더 위에서 실행하면 된다.

추가로,
1의 상태에서 다음과 같이 추가한다.
$ cat << EOF > src/foo/bar.py
from .baz import foobar
EOF
$ cat << EOF > src/foo/baz.py
foobar = 'foobar'
EOF
$ python src/main.py
bar.py는 상대 경로를 쓰고 있는데 아무런 문제가 없다.
오히려 .를 없애면 baz를 찾을 수 없다는 에러가 나온다. (foo.baz는 괜찮다)

애초부터 최초의 임포트에러는 top-level까지 올라왔을 때만 발생하는 듯 하다.