반응형

파이썬에서 import 문을 쓸 때, 다양한 형태의 import 문을 마주치게 된다.

경험상, import 문의 동작 방식을 제대로 이해하지 못하면 예상치 못한 문제들을 마주할 수 있다.

 

먼저, import 대상이 점(.)으로 시작하는 경우와 그렇지 않은 경우가 있는데, 언제 점(.)을 쓰고 안 써야 하는지 정리해보려고 한다.

다음으로 import문의 충돌 문제에 대해서도 다뤄보겠다.

 

1. 절대 경로 import (점 없는 import)

점(.) 없는 import 문이 모듈을 찾는 경로는 Python의 sys.path에 등록된 모든 경로이다.

sys.path란 Python이 모듈을 검색할 때 참조하는 경로 목록으로 Python Path라고 부르며, 다음 경로들을 포함한다:

  1. 현재 실행중인 스크립트가 있는 디렉토리(sys.path[0])
  2. PYTHONPATH 환경변수에 등록된 디렉토리들
  3. Python이 설치될 때 기본으로 등록된 디렉토리들 (표준 라이브러리가 위치한 lib 디렉토리와 외부 라이브러리들이 설치되는 site-packages 디렉토리)

이런 방식의 import는 크게 세 가지 경우에서 볼 수 있다:

1.1 파이썬 기본 모듈

파이썬 설치 경로 밑의 (표준 라이브러리가 위치한 lib 디렉토리)에서 찾는다.

from datetime import datetime
import os
import sys

1.2 외부 라이브러리

lib 디렉토리 밑의 site-packages 폴더에서 찾는다.

import pandas
import numpy as np
from sklearn.model_selection import train_test_split

1.3 Python Path에 등록된 로컬 패키지

크게 3가지 경우이다:

  1. 현재 (스크립트의) 실행 위치(sys.path[0])
  2. PYTHONPATH 환경변수에 등록된 경로들
  3. 프레임워크나 도구가 자동으로 sys.path에 추가하는 경로들.

로컬 패키지가 Python Path에 등록되는 것은, Django 같은 웹 프레임워크에서 자주 볼 수 있는 케이스이다.

Django의 경우 프로젝트의 루트 디렉토리가 Python Path에 자동으로 추가되어, 앱 단위의 모듈을 절대 경로로 import할 수 있다.

# Django 프로젝트의 urls.py
from main.views import index  # main은 로컬 앱 이름
from blog.models import Post  # blog는 로컬 앱 이름

이것이 가능한 이유는, 파이썬의 기본동작과 manage.py 스크립트 실행 방식 때문에 프로젝트 루트 디렉토리가 Python Path에 추가되기 때문이다.

  • 파이썬은, 파이썬으로 스크립트를 직접 실행할 때(ex: python manage.py 실행) 스크립트가 위치한 디렉토리를 sys.path의 첫번째 항목(sys.path[0])에 자동 추가한다.
    • 참고로, 스크립트가 타 프로그램에 의해 간접 실행되는 경우, sys.path[0]에는 그 프로그램이 실행된 디렉토리(CWD)가 설정된다. 이는 아래의 "상대 경로 import(점 있는 import)" 절에서 더 다룬다.
  • manage.py는 Django 프로젝트의 루트 디렉토리에 위치하므로, python manage.py를 실행하면 루트 디렉토리가 sys.path[0]에 자동으로 추가된다.

아래처럼 manage.py를 수정해보면, manage.py 파일이 위치한 디렉토리(프로젝트 루트 디렉토리)가 출력되는 것을 확인 가능하다.

import sys
import os

# ... 코드 생략 ...

if __name__ == '__main__':
    print(sys.path[0])  # 현재 디렉토리인 프로젝트 최상위 디렉토리가 출력됨
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
    execute_from_command_line(sys.argv)

이렇게 해서 프로젝트 내의 모든 앱을 최상위 네임스페이스에서 접근 가능하게 된다(달리 말하면, sys.path에 프로젝트 루트 디렉토리가 포함되므로, 코드 작성시 프로젝트 내의 앱 디렉토리를 최상위 네임스페이스처럼 사용할 수 있게 된다).

 

1.1~1.3의 예시들을 정리하자면, 

  1. Python 기본 모듈 (예: datetime)은 Python 설치 경로의 기본 라이브러리 lib에서 찾는다.
  2. 외부 라이브러리 (예: pandas)는 Python 설치 경로의 site-packages에서 찾는다.
  3. Django의 앱 같은 로컬 패키지는 프로젝트 루트 디렉토리(Djangomanage.py 실행시 sys.path에 추가됨)에서 찾는다.

 

 

2. 상대 경로 import (점 있는 import)

현재 파일의 위치를 기준으로 다른 모듈의 위치를 지정하는 방식이다.

from .utils import helper          # 현재 디렉토리의 utils
from ..models import User          # 상위 디렉토리의 models
from ...core import settings       # 두 단계 상위 디렉토리의 core
 
프로젝트 실행 위치에 따라 import 문의 동작이 달라질 수 있다.
아래 구조의 FastAPI 프로젝트를 예로 들어보자:
my_fastapi_project/
    ├── src/
    │   ├── main.py
    │   ├── api/
    │   │   └── v1/
    │   │       └── endpoints.py
    │   └── utils/
    │       └── helpers.py

src 디렉토리에서 uvicorn으로 FastAPI 서버를 실행할 경우:

  1. 해당 명령어를 실행한 디렉토리 srcsys.path[0]에 추가된다.
  2. 이는  src에서 python main.py를 실행한 것과 동일한 효과를 가진다.
    • main.py가 위치한 src를 직접 sys.path[0]으로 넣는 효과를 의미한다.
# src/ 디렉토리에서
uvicorn main:app --reload
# src/main.py
from api.v1.endpoints import router  # ✅ 동작함
from utils.helpers import format_response  # ✅ 동작함

 

반면, 프로젝트 루트(my_fastapi_project)에서 uvicorn으로 FastAPI 서버를 실행할 경우:

  1. my_fastapi_projectsys.path[0]이 된다.
  2. uvicornmain.py를 직접 실행한 게 아니라, 자신의 CWD(현재 작업 디렉토리)에서 스크립트를 실행한 것이기 때문이다.
    • 스크립트를 간접 실행하는 프로그램의 경우, sys.path[0]에는 그 프로그램이 실행된 디렉토리(CWD)가 설정된다.
    • 이는 파이썬의 기본 동작이므로, uvicorn, pytest, 또는 다른 파이썬 기반 도구에서도 동일한 원칙이 적용된다.
  3. 따라서 위 예시의 import 문들은 동작하지 않는다.
# my_fastapi_project/ 디렉토리에서
uvicorn src.main:app --reload
# src/main.py
from api.v1.endpoints import router  # ❌ 실패
from src.api.v1.endpoints import router  # ✅ 동작함

# 또는 상대 경로 사용
from .api.v1.endpoints import router  # ✅ 동작함
 

 

3. 실제 프로젝트 예시

실제 프로젝트에서 import문을 어떻게 사용하는지 살펴보기 위해, 다음과 같은 구조의 프로젝트가 있다고 가정해보자:

myproject/
    └── src/
        ├── utils/
        │   ├── __init__.py
        │   └── helper.py
        └── models/
            ├── __init__.py
            ├── user.py
            └── services/
                └── auth.py

auth.py 파일에서 다른 모듈을 import할 때, 절대 경로 방식과 상대 경로 방식이 어떻게 사용되는지 비교해보자:

(myproject 폴더가 sys.path[0]에 등록된 상태임을 가정)

# 절대 경로 방식
from src.utils.helper import some_function
from src.models.user import User

# 상대 경로 방식
from ...utils.helper import some_function
from ..user import User

 

 

4. 언제 어떤 방식을 사용해야 할까?

상대 경로 import가 좋은 경우

  • 같은 프로젝트 내의 모듈을 import 할 때
  • 프로젝트 구조가 자주 변경될 수 있을 때
  • 코드의 재사용성을 높이고 싶을 때

절대 경로 import가 좋은 경우

  • 외부 라이브러리를 사용할 때
  • 프로젝트 구조가 안정적일 때
  • 코드의 명확성이 더 중요할 때

 

 

5. import 충돌 문제

동일한 import 문이 여러 모듈을 가리킬 수 있어 충돌이 발생할 수 있다.

주요 시나리오들을 살펴보자:

5.1 sys.path 충돌

# 프로젝트 구조:
my_project/
    utils/
        helper.py
    venv/
        lib/
            site-packages/
                utils/
                    helper.py  # pip로 설치된 외부 라이브러리

# my_project/main.py
from utils.helper import something  # 어떤 helper.py를 import 할까?

이 경우, 파이썬은 sys.path의 첫 번째로 매칭되는 모듈을 import한다.

5.2 이름 충돌

# 여러 utils.py가 있는 경우
from auth.utils import helper
from billing.utils import helper  # 이전 helper를 덮어씀

5.3 순환 import

# a.py
from .b import b_function

def a_function():
    print("This is a_function")
    b_function()

# b.py
from .a import a_function

def b_function():
    print("This is b_function")
    a_function()

이런 순환 import 문제는 특히 Django나 FastAPI 같은 웹 프레임워크에서 Model과 Serializer, 또는 Model과 View 사이에서 자주 발생한다. 가장 좋은 해결책은 애초에 순환 참조가 발생하지 않도록 코드를 구조화하는 것이지만, 불가피한 경우 위와 같이 지역 import를 사용하여 해결할 수 있다.

5.5 import 충돌 방지를 위한 Best Practices

1. 명확한 alias 사용하기

❌ alis 미사용:

# 여러 utils.py가 있는 경우
from auth.utils import helper
from billing.utils import helper  # 이전 helper를 덮어씀

✅ 명확한 alias 사용:

from auth.utils import helper as auth_helper
from billing.utils import helper as billing_helper

2. 패키지 이름을 구체적으로 짓기

❌ 안 좋은 예:

my_project/
    utils/
        date.py
        string.py    # Python 기본 모듈인 string과 이름이 같음
        json.py      # Python 기본 모듈인 json과 이름이 같음

✅ 좋은 예:

my_project/
    custom_utils/               # 더 구체적인 이름
        date_formatter.py      # 기능을 더 명확히 표현
        string_validator.py    # 기능을 더 명확히 표현
        json_handler.py       # 기능을 더 명확히 표현

3. 가능하면 상대경로보다 절대경로를 사용하기

❌ 복잡한 상대 경로:

# my_project/apps/users/views/auth/login.py
from ....utils.validators import validate_email  # 점이 너무 많아 혼란스러움
from ...models.user import User                 # 경로 파악이 어려움

✅ 명확한 절대 경로:

# my_project/apps/users/views/auth/login.py
from my_project.utils.validators import validate_email  # 명확한 경로
from my_project.apps.users.models import User          # 어디서 import하는지 바로 알 수 있음

4. 로컬 모듈과 외부 라이브러리 이름이 겹치지 않도록 주의하기

❌ 이름 충돌 가능성 있는 구조:

my_project/
    requests/            # requests는 유명한 HTTP 라이브러리 이름
        api.py
    json/               # json은 Python 기본 모듈 이름
        parser.py

✅ 충돌을 피한 구조:

my_project/
    http_client/        # requests 대신 더 구체적인 이름
        api.py
    json_processing/    # json 대신 더 구체적인 이름
        parser.py

이렇게 구조화하면, import문의 의미가 명확해진다.

# 혼란스러울 수 있는 import
import requests  # 외부 라이브러리? 로컬 모듈?
from json import loads  # Python 기본 모듈? 로컬 모듈?

# 명확한 import
import requests  # 확실히 외부 라이브러리
from http_client import api  # 확실히 로컬 모듈
from json_processing import parser  # 확실히 로컬 모듈

 

 

마무리

import 문은 단순해 보이나, 실제로는 복잡한 규칙과 주의사항들이 있다. 특히 프로젝트가 커질수록 import 충돌 문제는 디버깅이 어려운 버그의 원인이 될 수 있다. import에 대한 규칙들을 잘 이해하면, 더 견고한 파이썬 프로젝트를 만들 수 있다.

반응형

'Python > 문법' 카테고리의 다른 글

[Python] dataclass  (0) 2025.01.27
[Python] Module과 Package  (0) 2025.01.18
[Python] Module import로 이해하는 Namespace와 Scope  (1) 2025.01.09
[Python] Unpacking의 다양한 예시  (1) 2024.12.27
[Python] Iterable과 Sequence 자료형  (0) 2024.12.27

+ Recent posts