Devin.KR
로그인

파이썬 모듈과 패키지 - import 동작과 __name__ __main__ (파이썬 초급 8단원)

개발자 조회 1

이 단원에서 배우는 것

7단원까지 만든 것을 모아 보면 재고를 읽는 함수, 발주 대상을 고르는 함수, 영수증을 찍는 함수, 그리고 그것들을 부르는 실행 코드가 한 파일에 들어 있다. 지금은 200줄 남짓이지만 여기에 주문·회원·정산이 붙으면 아무도 못 읽는 파일이 된다. 초급 과정의 마지막인 이번 단원은 그 파일을 나눈다. 그리고 파이썬 파일 끝에서 늘 보이는 if __name__ == "__main__":장식이 아니라 필수인 이유를 밝힌다.

  • import가 실제로 무슨 일을 하는지, 모듈을 어디서 찾는지 안다.
  • 여러 .py 파일을 패키지로 묶고 함수를 나눠 담는다.
  • if __name__ == "__main__":의 동작을 설명하고 올바른 자리에 쓴다.

왜 필요한가

파일을 나누는 이유를 "정리 정돈"으로만 이해하면 이 단원의 핵심을 놓친다. 진짜 이유는 import가 코드를 실행하기 때문이다.

파이썬에서 import bookstore.inventory는 "그 파일의 함수 목록을 참조하라"는 뜻이 아니다. 그 파일을 위에서 아래까지 한 번 실행하라는 뜻이다. def 줄을 실행해서 함수를 만들고, 파일 맨 아래에 print()가 있으면 그 print()도 실행한다. 그러니 재고 파일에 테스트용 실행 코드를 그냥 두면, 다른 파일에서 함수 하나를 쓰려고 import하는 순간 그 실행 코드가 같이 돈다. 판매 마감 배치를 import했다가 마감이 한 번 더 돌아 버리는 사고가 이래서 생긴다.

그래서 파이썬에는 "지금 이 파일이 직접 실행된 것인가, 남에게 불려 온 것인가"를 구분하는 장치가 필요했다. 그게 __name__이다.

문법과 예제

모듈은 파일 하나, 패키지는 폴더

파이썬 파일 하나가 곧 모듈이다. 별도의 선언이 필요 없고, 파일 이름에서 .py를 뗀 것이 모듈 이름이다. 그런 파일들을 담은 폴더가 패키지다. 7단원까지의 코드를 이렇게 나눈다.

proj/
├── main.py                  ← 실행하는 파일
├── inventory.csv
└── bookstore/               ← 패키지 (폴더)
    ├── __init__.py          ← 이 폴더가 패키지임을 알리는 빈 파일
    ├── inventory.py         ← 재고 관련 함수
    └── receipt.py           ← 영수증 관련 함수

__init__.py는 파이썬 3.3부터 없어도 대개 동작하지만, 있으면 의도가 분명해지고 도구들이 헷갈리지 않는다. 빈 파일로라도 만들어 둔다가 실무 관행이다.

bookstore/inventory.py다. 7단원에서 만든 함수를 그대로 옮겼다.

"""재고 관련 기능."""
import csv


def load_inventory(path):
    """CSV 파일을 읽어 {제목: 재고} 딕셔너리를 돌려준다."""
    inventory = {}
    with open(path, encoding="utf-8", newline="") as f:
        for row in csv.DictReader(f):
            inventory[row["title"]] = int(row["stock"])
    return inventory


def pick_reorder(inventory, threshold=2):
    """재고가 기준 이하인 책 제목 리스트를 돌려준다."""
    reorder = []
    for title, stock in inventory.items():
        if stock <= threshold:
            reorder.append(title)
    return reorder


if __name__ == "__main__":
    # 이 파일을 직접 실행했을 때만 도는 간이 시험 코드
    print(pick_reorder({"파이썬 개론": 3, "리팩터링": 0}))

bookstore/receipt.py는 6단원의 영수증을 함수로 바꾼 것이다. 화면에 찍는 대신 문자열을 돌려준다. 5단원에서 말한 "출력과 계산을 나눈다"의 실천이다.

"""영수증 출력."""


def format_receipt(items):
    """items 는 (제목, 정가, 수량) 튜플의 리스트다."""
    lines = ["=" * 34]
    total = 0
    for title, price, qty in items:
        amount = price * qty
        total += amount
        lines.append(f"{title:<16}{qty:>3}권{amount:>10,}원")
    lines.append("-" * 34)
    lines.append(f"{'합계':<16}{'':>4}{total:>10,}원")
    return "\n".join(lines)

마지막으로 main.py다. 이 파일은 부르기만 하고 스스로 계산하지 않는다.

from bookstore.inventory import load_inventory, pick_reorder
from bookstore.receipt import format_receipt


def main():
    inventory = load_inventory("inventory.csv")
    print("발주 대상:", pick_reorder(inventory))
    print(format_receipt([("파이썬 개론", 28000, 2)]))


if __name__ == "__main__":
    main()
$ python3 main.py
발주 대상: ['리팩터링']
==================================
파이썬 개론            2권    56,000원
----------------------------------
합계                      56,000원

import 하는 세 가지 방법

import bookstore.inventory                       # 부를 때 전체 경로를 쓴다
bookstore.inventory.pick_reorder(inv)

from bookstore.inventory import pick_reorder      # 이름을 직접 가져온다
pick_reorder(inv)

from bookstore import inventory as inv_mod        # 별명을 붙인다
inv_mod.pick_reorder(inv)

세 방식 모두 파일 전체를 실행한다는 점은 같다. from ... import 함수하나라고 해서 그 함수만 읽어 오는 게 아니다. 가져오는 이름만 다를 뿐이다.

파이썬은 import한 모듈을 한 번만 실행하고 기억해 둔다. 같은 모듈을 열 군데에서 import해도 파일은 한 번만 돈다. 그래서 모듈 맨 위에 무거운 초기화 코드를 두면 첫 import 때 딱 한 번 대가를 치른다. 처음 실행할 때 생기는 __pycache__ 폴더는 이 과정을 빠르게 하려고 만든 캐시이고, 지워도 다시 만들어지므로 .gitignore에 넣는다.

__name__ 의 정체

모든 모듈에는 __name__이라는 이름이 자동으로 들어 있고, 값은 상황에 따라 두 가지다.

상황__name__
python3 main.py 로 직접 실행"__main__"
다른 파일이 import 해서 불려옴"bookstore.inventory" (모듈 경로)

따라서 if __name__ == "__main__": 블록은 직접 실행했을 때만 돈다. 이 한 줄 덕분에 한 파일이 두 가지 역할을 겸할 수 있다. 남이 쓸 함수 창고이면서, 혼자 실행하면 자기 자신을 시험해 보는 실행 파일이다.

직접 확인해 본다. inventory.py 맨 아래에 print(__name__)을 잠깐 넣고 두 방식으로 돌려 보면 값이 다르게 찍힌다.

$ python3 bookstore/inventory.py     # __main__
$ python3 -c "import bookstore.inventory"   # bookstore.inventory

패키지 안의 모듈을 직접 실행할 때는 -m 옵션을 쓴다. 경로 대신 모듈 이름을 쓰고 .py를 붙이지 않는다.

$ python3 -m bookstore.inventory
['리팩터링']

파이썬은 모듈을 어디서 찾는가

import csv라고 쓰면 파이썬은 sys.path라는 폴더 목록을 순서대로 뒤진다. 순서가 중요하다.

import sys
print(sys.path)
# ['/Users/me/proj', ... 표준 라이브러리 폴더 ..., ... site-packages ...]

맨 앞이 실행한 스크립트가 있는 폴더다. 그다음이 표준 라이브러리, 마지막이 pip install로 설치한 것들이 들어가는 site-packages다. 1단원에서 "연습 파일 이름을 random.py로 짓지 마라"고 한 이유가 이 순서다. 내 폴더가 먼저 검색되므로 표준 random 모듈은 영영 밀린다.

실무에서 자주 틀리는 것

1. 실행 코드를 모듈 맨 위 레벨에 그냥 둔다

가장 흔하고 가장 이해가 늦는 실수다. inventory.py 맨 아래에 if 없이 이렇게 적었다고 하자.

# bookstore/inventory.py 맨 아래
print(load_inventory("inventory.csv"))     # 시험해 보려고 넣었다

이제 main.py에서 from bookstore.inventory import pick_reorder만 해도 이 줄이 실행된다. 결과는 셋 중 하나다. 쓸데없는 출력이 섞이거나, 실행 폴더가 달라 FileNotFoundErrorimport 자체가 실패하거나, 마감 처리 같은 것이었다면 한 번 더 실행된다. 규칙은 단순하다. 모듈 맨 위 레벨에는 import·def·상수만 둔다. 실제로 뭔가 하는 코드는 함수 안이나 if __name__ == "__main__": 안으로 넣는다.

main.py에서도 마찬가지다. main() 함수를 따로 만들고 if 블록에서 부르는 형태가 표준이다. 그래야 나중에 테스트 코드가 main()을 직접 부를 수 있다.

2. 파일 이름이 표준 모듈과 겹친다

1단원에서 예고한 함정을 이제 원인까지 안다. 폴더에 random.py를 만들어 두고 다른 파일에서 import random을 하면 이렇게 된다.

AttributeError: module 'random' has no attribute 'randint'
(consider renaming '/home/me/proj/random.py' since it has the same name as
the standard library module named 'random' ...)

최근 파이썬은 괄호 안처럼 원인까지 짚어 주지만, 버전에 따라 AttributeError 한 줄만 나와 한참 헤매게 된다. csv.py, json.py, email.py, test.py, types.py가 특히 자주 겹친다. 확실치 않으면 파이썬을 띄워 import 이름을 쳐 보고 되면 피한다.

3. 상대 import 를 쓰고 파일을 직접 실행한다

패키지 안에서 같은 패키지의 다른 모듈을 부를 때 점을 붙이는 상대 import 를 쓸 수 있다.

# bookstore/report.py
from .inventory import pick_reorder     # 점 하나 = 같은 패키지

이 파일을 python3 bookstore/report.py로 실행하면 이렇게 죽는다.

ImportError: attempted relative import with no known parent package

직접 실행된 파일은 __name__"__main__"이라 자신이 어느 패키지에 속하는지 모르고, 그러면 점이 가리킬 곳도 없기 때문이다. 해결은 두 가지다. python3 -m bookstore.report로 패키지를 알려 주며 실행하거나, 처음부터 절대 import 로 쓴다.

from bookstore.inventory import pick_reorder   # 어떻게 실행하든 같은 뜻이다

초급 단계에서는 절대 import 로 통일하는 편이 사고가 적다.

4. 두 모듈이 서로를 import 한다

ImportError: cannot import name 'fc' from partially initialized module 'c'
(most likely due to a circular import)

c.pyd.py를 부르고 d.py가 다시 c.py를 부르면 이 오류가 난다. c.py를 실행하는 도중에 d.py로 넘어갔고, d.py가 요구하는 fc는 아직 만들어지기 전이기 때문이다. 메시지의 partially initialized가 그 뜻이다.

이건 import 문제가 아니라 설계 문제라는 점이 중요하다. 두 모듈이 서로를 필요로 한다면 경계가 잘못 그어진 것이다. 공통으로 쓰는 부분을 세 번째 모듈로 빼내면 대개 풀린다. 서점 예제에서 receipt.pyinventory.py를 부르지 않도록, 필요한 값을 인자로 받게 만든 것이 같은 원리다.

5. from module import * 를 쓴다

from bookstore.inventory import *
from bookstore.receipt import *      # 두 모듈에 같은 이름의 함수가 있으면?

나중에 읽은 쪽이 앞의 것을 소리 없이 덮어쓴다. 게다가 코드에 나오는 함수가 어느 파일에서 왔는지 알 수 없어져 에디터의 "정의로 이동"도 못 쓴다. 예제 코드나 대화형 셸에서는 편하지만 파일에 저장하는 코드에는 쓰지 않는다. 필요한 이름을 하나씩 적는 편이 길어도 낫다.

스스로 확인하기

  1. bookstore/receipt.py 맨 아래에 if 없이 print(format_receipt([("테스트", 1000, 1)]))를 넣었다. main.py를 실행하면 무슨 일이 생기는가?
  2. python3 main.py로 실행할 때와 python3 -m bookstore.inventory로 실행할 때, bookstore/inventory.py__name__은 각각 무엇인가?
  3. 같은 폴더에 json.py라는 연습 파일을 만들어 두었더니, 전혀 상관없어 보이는 main.pyAttributeError: module 'json' has no attribute 'dump'로 죽는다. 원인과 해결책은?

정답

  1. main.pyfrom bookstore.receipt import format_receipt를 실행하는 순간 receipt.py가 통째로 한 번 실행되므로, 프로그램의 진짜 출력보다 먼저 테스트 영수증이 찍힌다. import 는 이름을 빌려 오는 게 아니라 파일을 실행하는 일이기 때문이다. 시험 코드는 if __name__ == "__main__": 안으로 넣는다.
  2. 앞의 경우는 "bookstore.inventory"다. main.py가 import 해서 불려 왔으므로 모듈 경로가 들어간다. 뒤의 경우는 "__main__"이다. -m으로 직접 실행 대상이 되었기 때문이다. 그래서 뒤의 경우에만 if __name__ == "__main__": 블록이 돈다.
  3. sys.path의 맨 앞이 현재 스크립트 폴더라서, import json이 표준 라이브러리 대신 내가 만든 json.py를 집어 온다. 내 파일에는 dump 함수가 없으니 AttributeError가 난다. 해결은 파일 이름을 바꾸는 것이고, 이미 실행한 적이 있다면 같이 생긴 __pycache__ 폴더도 지운다.

여기까지가 파이썬 초급 커리큘럼이다. 1단원의 print 한 줄에서 시작해, 지금은 CSV에서 재고를 읽고 발주 대상을 골라 영수증을 찍는 프로그램이 패키지로 정리되어 있다. 중급 과정에서는 이 코드에 예외 처리와 클래스를 도입하고, 지금은 if로 대충 막아 둔 오류 상황들을 제대로 다룬다.