파이썬 모듈과 패키지 - import 동작과 __name__ __main__ (파이썬 초급 8단원)
이 단원에서 배우는 것
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만 해도 이 줄이 실행된다. 결과는 셋 중 하나다. 쓸데없는 출력이 섞이거나, 실행 폴더가 달라 FileNotFoundError로 import 자체가 실패하거나, 마감 처리 같은 것이었다면 한 번 더 실행된다. 규칙은 단순하다. 모듈 맨 위 레벨에는 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.py가 d.py를 부르고 d.py가 다시 c.py를 부르면 이 오류가 난다. c.py를 실행하는 도중에 d.py로 넘어갔고, d.py가 요구하는 fc는 아직 만들어지기 전이기 때문이다. 메시지의 partially initialized가 그 뜻이다.
이건 import 문제가 아니라 설계 문제라는 점이 중요하다. 두 모듈이 서로를 필요로 한다면 경계가 잘못 그어진 것이다. 공통으로 쓰는 부분을 세 번째 모듈로 빼내면 대개 풀린다. 서점 예제에서 receipt.py가 inventory.py를 부르지 않도록, 필요한 값을 인자로 받게 만든 것이 같은 원리다.
5. from module import * 를 쓴다
from bookstore.inventory import *
from bookstore.receipt import * # 두 모듈에 같은 이름의 함수가 있으면?
나중에 읽은 쪽이 앞의 것을 소리 없이 덮어쓴다. 게다가 코드에 나오는 함수가 어느 파일에서 왔는지 알 수 없어져 에디터의 "정의로 이동"도 못 쓴다. 예제 코드나 대화형 셸에서는 편하지만 파일에 저장하는 코드에는 쓰지 않는다. 필요한 이름을 하나씩 적는 편이 길어도 낫다.
스스로 확인하기
bookstore/receipt.py맨 아래에if없이print(format_receipt([("테스트", 1000, 1)]))를 넣었다.main.py를 실행하면 무슨 일이 생기는가?python3 main.py로 실행할 때와python3 -m bookstore.inventory로 실행할 때,bookstore/inventory.py의__name__은 각각 무엇인가?- 같은 폴더에
json.py라는 연습 파일을 만들어 두었더니, 전혀 상관없어 보이는main.py가AttributeError: module 'json' has no attribute 'dump'로 죽는다. 원인과 해결책은?
정답
main.py가from bookstore.receipt import format_receipt를 실행하는 순간receipt.py가 통째로 한 번 실행되므로, 프로그램의 진짜 출력보다 먼저 테스트 영수증이 찍힌다. import 는 이름을 빌려 오는 게 아니라 파일을 실행하는 일이기 때문이다. 시험 코드는if __name__ == "__main__":안으로 넣는다.- 앞의 경우는
"bookstore.inventory"다.main.py가 import 해서 불려 왔으므로 모듈 경로가 들어간다. 뒤의 경우는"__main__"이다.-m으로 직접 실행 대상이 되었기 때문이다. 그래서 뒤의 경우에만if __name__ == "__main__":블록이 돈다. sys.path의 맨 앞이 현재 스크립트 폴더라서,import json이 표준 라이브러리 대신 내가 만든json.py를 집어 온다. 내 파일에는dump함수가 없으니AttributeError가 난다. 해결은 파일 이름을 바꾸는 것이고, 이미 실행한 적이 있다면 같이 생긴__pycache__폴더도 지운다.
여기까지가 파이썬 초급 커리큘럼이다. 1단원의 print 한 줄에서 시작해, 지금은 CSV에서 재고를 읽고 발주 대상을 골라 영수증을 찍는 프로그램이 패키지로 정리되어 있다. 중급 과정에서는 이 코드에 예외 처리와 클래스를 도입하고, 지금은 if로 대충 막아 둔 오류 상황들을 제대로 다룬다.