




패키지 파일 구성이 아래와 같음
calc_practice/
├─ __init__.py
├─ basic.py
├─ advanced.py
└─ __pycache__/
└─ advanced.cpython-311.pyc
모듈과 패키지 활용방법, __init__.py 파일을 활용하는 방법과 연계해서
파이썬 3.3 버전 이상에서는 __init__.py 이 없어도 패키지로 인지한다.
__init__.py 에서 __all__ 을 활용하여 참조하게 만들 모듈만 선택 가능
test.py 에서 패키지를 불러올때
import 뒤에 적혀있는 것은 함수 사용시 적고 .함수명 을 써야함
import[폴더명]
[폴더명].함수명()
_로 시작하는 함수명은 *로 import되지 않음
1. 모듈과 패키지: 개념부터
코드를 작성하다 보면 점점 길어지고 복잡해집니다. 이때 모든 코드를 하나의 파일에 넣는 것은 재앙에 가깝죠. 그래서 우리는 코드를 기능별로 여러 파일에 나누어 정리하는데, 이것이 바로 ‘모듈’과 ‘패키지’의 시작입니다.
- 모듈 (Module): 간단합니다. 그냥
.py확장자를 가진 파이썬 파일 하나가 바로 모듈입니다. 수강생님이 만드신basic.py와advanced.py가 각각 하나의 모듈이죠. 모듈은 함수, 클래스, 변수 등을 담는 ‘부품 상자’라고 생각하세요. - 패키지 (Package): 패키지는 이런 ‘부품 상자(모듈)‘들을 담는 더 큰 ‘공구함’입니다. 폴더(디렉토리)로 구성되며, 여러 모듈을 체계적으로 관리하게 해줍니다. 수강생님의
calc_practice폴더가 바로 패키지입니다.
그렇다면 파이썬은 일반 폴더와 패키지 폴더를 어떻게 구분할까요? 바로 __init__.py 파일의 유무로 구분합니다.
2. 마법의 파일, __init__.py의 역할
초보자들이 가장 많이 혼란스러워하는 파일이죠. 텅 비어있는 경우도 많아서 “이게 대체 왜 필요하지?” 싶을 겁니다. __init__.py의 핵심 역할 두 가지를 기억하세요.
-
“여기는 패키지입니다!” 라고 파이썬에게 알리는 깃발 가장 기본적이고 중요한 역할입니다. 어떤 디렉토리에
__init__.py파일이 있으면, 파이썬은 그 디렉토리를 단순한 폴더가 아닌 ‘패키지’로 인식합니다. 이 파일이 없다면 파이썬은 그저 평범한 폴더로 취급해서 패키지처럼 불러올 수 없습니다. -
패키지를 더 편리하게 만들어주는 설계도 (A.K.A. API) 이것이 바로 10년차 노하우가 담긴 핵심입니다.
__init__.py안에 코드를 작성하면, 패키지를 처음 불러올 때(import 할 때) 그 코드가 단 한 번 실행됩니다. 이를 이용해 패키지의 구조를 단순화하고 사용자가 더 편리하게 함수를 쓸 수 있도록 만들 수 있습니다.아래 예시에서
__init__.py를 어떻게 활용해서 코드를 더 깔끔하게 만드는지 직접 보여드리겠습니다.
3. test.py에서 패키지 불러오기: 5가지 방법 완벽 비교
이제 본격적으로 test.py 파일에서 calc_practice 패키지를 불러오는 여러 방법을 비교해 보겠습니다. 각 방법의 차이를 명확히 이해하는 것이 이번 강의의 목표입니다.
먼저, 실습을 위해 각 모듈에 간단한 함수를 만들어 넣겠습니다. 그리고 test.py는 calc_practice 폴더와 같은 위치에 있다고 가정합니다.
파일 준비
# calc_practice/basic.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b# calc_practice/advanced.py
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
return "0으로 나눌 수 없습니다."
return a / b# 프로젝트 구조
my_project/
├─ calc_practice/
│ ├─ __init__.py # 지금은 빈 파일
│ ├─ basic.py
│ └─ advanced.py
└─ test.py
방법 1: import 패키지.모듈 (가장 정석적인 방법)
가장 명시적이고 직관적인 방법입니다.
-
test.py 코드:
import calc_practice.basic import calc_practice.advanced print(calc_practice.basic.add(10, 5)) print(calc_practice.advanced.multiply(10, 5)) -
설명:
calc_practice패키지 안에 있는basic모듈과advanced모듈을 각각 불러옵니다. 함수를 호출할 때는패키지명.모듈명.함수명()형태의 전체 경로를 모두 적어줘야 합니다. -
장점: 코드가 길어지더라도
add함수가 어느 패키지의 어느 모듈에 속해 있는지 명확하게 알 수 있습니다. 이름 충돌(예: 다른 라이브러리에도add함수가 있는 경우)을 완벽하게 방지할 수 있습니다. -
단점: 매번 전체 경로를 다 입력해야 해서 코드가 길고 번거로워 보일 수 있습니다.
방법 2: from 패키지 import 모듈 (모듈 단위로 꺼내오기)
매번 패키지명을 쓰는 게 번거로울 때 사용합니다.
-
test.py 코드:
from calc_practice import basic, advanced print(basic.add(10, 5)) print(advanced.multiply(10, 5)) -
설명:
calc_practice패키지로부터basic과advanced모듈을 직접 꺼내옵니다. 이제 함수를 호출할 때 패키지명은 생략하고모듈명.함수명()형태로 사용할 수 있습니다. -
장점: 방법 1보다 코드가 간결해집니다.
-
단점: 만약
basic이라는 이름의 다른 변수나 모듈을 사용하고 있다면 이름 충돌이 발생할 수 있습니다.
방법 3: from 패키지.모듈 import 함수 (함수 단위로 콕 집어 꺼내오기)
특정 함수 몇 개만 필요할 때 유용합니다.
-
test.py 코드:
from calc_practice.basic import add from calc_practice.advanced import multiply print(add(10, 5)) print(multiply(10, 5)) -
설명:
basic모듈에서add함수,advanced모듈에서multiply함수를 직접 꺼내옵니다. 이제 패키지명과 모듈명을 모두 생략하고함수명()만으로 바로 사용할 수 있습니다. -
장점: 코드가 매우 간결해져서 가장 편하게 함수를 호출할 수 있습니다.
-
단점: 이름 충돌의 위험이 가장 큽니다. 만약
test.py안에서add라는 이름의 다른 함수나 변수를 정의하면, 나중에 불러온 이름이 기존 이름을 덮어쓰게 되어 예측 불가능한 오류를 일으킬 수 있습니다. 코드만 봐서는 이add함수가 어디서 왔는지 바로 알기 어렵습니다.
방법 4: from 패키지.모듈 import * (절대 사용하지 마세요!)
“모든 것을 다 가져와!” 라는 의미의 *(asterisk)를 사용합니다.
-
test.py 코드:
from calc_practice.basic import * from calc_practice.advanced import * print(add(10, 5)) print(multiply(10, 5)) -
설명:
basic과advanced모듈 안에 있는 모든 함수, 클래스, 변수를 현재 파일의 이름 공간으로 가져옵니다. -
장점: 코드를 입력하기는 가장 편해 보입니다.
-
단점: (치명적) 어떤 이름들이 현재 파일로 들어왔는지 전혀 알 수 없습니다. 다른 모듈에 같은 이름의 함수가 있다면 의도치 않게 덮어쓰여 버립니다. 코드를 읽고 디버깅하기가 극도로 어려워지기 때문에, 실무에서는 거의 사용하지 않는 것을 원칙으로 합니다. 저를 믿으세요, 이 방법은 그냥 잊어버리시는 게 좋습니다.
방법 5: __init__.py를 활용한 전문가의 방법 (강력 추천)
드디어 __init__.py의 진가를 발휘할 시간입니다. 이 방법을 쓰면 패키지 사용자는 내부 구조를 몰라도 아주 편하게 패키지를 쓸 수 있습니다.
__init__.py 에서
from .[파일명(모듈명)] import [함수1], [함수2]
같은 형식으로 작성
패키지를 불러오는 파일 (예:test.py) 에서는
import [패키지명(폴더명)]
[패키지명(폴더명)].함수1()
과 같은 형태로 사용 가능
def _함수명
의 경우 내부용 함수라는 암묵적인 규칙이 있음
1. __init__.py 파일 수정
calc_practice 폴더의 빈 __init__.py 파일을 열어 아래와 같이 코드를 작성합니다.
# calc_practice/__init__.py
"""
이 코드는 calc_practice 패키지가 import될 때 실행됩니다.
basic 모듈의 add, subtract 함수와
advanced 모듈의 multiply, divide 함수를
패키지 레벨로 끌어올려 사용자가 바로 접근할 수 있게 합니다.
"""
from .basic import add, subtract
from .advanced import multiply, divide
print("계산기 패키지가 성공적으로 로드되었습니다.") # 로드 확인용 메시지.basic에서 .은 “현재 패키지 폴더 안에 있는” 이라는 의미의 상대 경로 표현입니다.
2. test.py 코드 작성
-
test.py 코드:
import calc_practice print(calc_practice.add(10, 5)) print(calc_practice.multiply(10, 5)) -
실행 결과:
계산기 패키지가 성공적으로 로드되었습니다. 15 50 -
설명:
import calc_practice한 줄만으로 모든 준비가 끝났습니다! 어떻게 된 일일까요?- 파이썬이
import calc_practice를 읽고__init__.py파일을 실행합니다. __init__.py안에서basic모듈의add와advanced모듈의multiply를 미리 불러와 준비해 놓습니다.- 결과적으로
add,multiply등의 함수들이calc_practice라는 패키지 이름 바로 아래에 소속된 것처럼 만들어줍니다. - 사용자는 이제
calc_practice.basic.add()처럼 복잡하게 들어갈 필요 없이,calc_practice.add()로 바로 함수를 호출할 수 있습니다.
- 파이썬이
-
장점:
- 사용자 편의성 극대화: 패키지 사용자는 복잡한 내부 모듈 구조를 알 필요가 없습니다. 그냥
import 패키지한 줄이면 패키지가 제공하는 핵심 기능들을 바로 사용할 수 있습니다. 유명한 라이브러리들(numpy, pandas 등)이 대부분 이런 방식을 사용합니다. - 깔끔한 API 제공: 패키지 제작자가
__init__.py를 통해 사용자에게 노출할 함수들(Public API)을 직접 선택하고 설계할 수 있습니다. - 명시성과 간결함의 조화:
calc_practice.add()처럼 함수가 어디서 왔는지 명확하면서도, 코드가 지나치게 길어지지 않는 균형을 이룹니다.
- 사용자 편의성 극대화: 패키지 사용자는 복잡한 내부 모듈 구조를 알 필요가 없습니다. 그냥
최종 정리
수강생님, 오늘 배운 내용을 표로 정리해 드릴 테니 머릿속에 확실히 각인시키세요.
| 방법 | 예시 코드 (test.py) | 함수 호출 | 장점 | 단점/주의사항 | 추천도 |
|---|---|---|---|---|---|
| 1. 모듈 직접 임포트 | import calc_practice.basic | calc_practice.basic.add() | 가장 명시적, 이름 충돌 없음 | 코드가 길어짐 | ★★★★☆ |
| 2. from으로 모듈 임포트 | from calc_practice import basic | basic.add() | 코드 간결, 패키지명 생략 | 모듈 이름 충돌 가능성 | ★★★☆☆ |
| 3. from으로 함수 임포트 | from calc_practice.basic import add | add() | 가장 간결함 | 함수 이름 충돌 위험 높음 | ★★☆☆☆ |
4. * 사용 (와일드카드) | from calc_practice.basic import * | add() | 타이핑이 편함 (그뿐) | 이름 충돌, 가독성 최악 (사용금지) | ☆☆☆☆☆ |
5. __init__.py 활용 | import calc_practice | calc_practice.add() | 명시성과 간결함의 완벽한 조화 | 초기 패키지 설계 필요 | ★★★★★ |
이제 막 시작하는 단계에서는 방법 1과 방법 5의 차이를 이해하고 적절히 사용하는 것이 가장 중요합니다. 혼자 간단한 코드를 짤 때는 방법 1로도 충분하지만, 다른 사람과 협업하거나 라이브러리처럼 재사용할 코드를 만들 때는 반드시 방법 5를 사용해 패키지를 설계하는 습관을 들이시기 바랍니다.
궁금한 점이 있다면 언제든 다시 질문하세요. 꾸준함이 실력을 만듭니다
클래스
수강생님, 또 한 번의 벽을 넘을 준비가 되셨군요. 아주 좋습니다! 함수와 모듈, 패키지가 ‘코드의 정리’에 관한 것이었다면, 클래스와 객체 지향 프로그래밍(OOP)은 ‘코드의 설계’에 관한, 한 차원 높은 이야기입니다. 이 개념을 이해하면 수강생님의 코드는 레고 블록 조립에서 진짜 로봇을 설계하는 수준으로 발전할 겁니다.
1. 클래스(Class)도 똑같이 불러옵니다
결론부터 말씀드리죠. 네, 클래스도 함수나 변수와 마찬가지로 모듈 안에 정의하고, 패키지를 통해 똑같이 불러옵니다.
파이썬의 관점에서 보면 클래스는 모듈 안에 들어있는 ‘하나의 구성요소’일 뿐입니다. 함수를 불러오는 것과 기술적으로 아무런 차이가 없습니다.
직접 보여드리겠습니다. 기존 calc_practice 패키지를 객체 지향적으로 업그레이드해 보죠.
Step 1: calculator.py 모듈과 Calculator 클래스 만들기
calc_practice 패키지 안에 calculator.py 라는 새 모듈(파일)을 만들고, 그 안에 Calculator 클래스를 정의합니다.
# calc_practice/calculator.py
class Calculator:
"""
상태를 가지는 계산기 클래스입니다.
이 계산기는 자신의 현재 결과값(result)을 계속 기억합니다.
"""
def __init__(self):
"""계산기 객체가 처음 만들어질 때 실행되는 초기화 함수"""
self.result = 0
print("계산기가 생성되었습니다. 초기값:", self.result)
def add(self, num):
"""현재 결과값에 숫자를 더합니다."""
self.result += num
return self.result
def subtract(self, num):
"""현재 결과값에서 숫자를 뺍니다."""
self.result -= num
return self.result이제 프로젝트 구조는 이렇게 바뀌었습니다.
my_project/
├─ calc_practice/
│ ├─ __init__.py
│ ├─ basic.py
│ ├─ advanced.py
│ └─ calculator.py # <- 클래스가 담긴 모듈 추가!
└─ test.py
Step 2: test.py에서 Calculator 클래스 불러오기
함수를 불러왔던 방법과 100% 동일합니다. 가장 많이 쓰는 3가지 방법을 보시죠.
-
방법 1:
from 패키지.모듈 import 클래스(가장 추천) 클래스를 사용할 땐 이 방법이 가장 직관적이고 많이 쓰입니다.# test.py from calc_practice.calculator import Calculator # Calculator 클래스(설계도)로 calc1 이라는 실제 객체(제품)를 만듭니다. calc1 = Calculator() # "계산기가 생성되었습니다. 초기값: 0" 출력 # calc1 객체의 메소드(기능)를 호출합니다. calc1.add(10) calc1.add(5) print("calc1의 최종 결과:", calc1.result) # 출력: 15 -
방법 2:
import 패키지.모듈# test.py import calc_practice.calculator calc2 = calc_practice.calculator.Calculator() # 경로를 전부 써줘야 함 calc2.add(100) print("calc2의 최종 결과:", calc2.result) # 출력: 100 -
방법 3:
__init__.py활용하기 사용자가 더 편하게 클래스를 불러오게 하려면__init__.py에 미리 등록해둘 수 있습니다.# calc_practice/__init__.py from .basic import add, subtract from .advanced import multiply, divide from .calculator import Calculator # <- 클래스를 패키지 레벨로 노출# test.py from calc_practice import Calculator # 이제 바로 불러올 수 있음! calc3 = Calculator() calc3.subtract(50) print("calc3의 최종 결과:", calc3.result) # 출력: -50
보시다시피 함수를 불러오던 규칙과 완벽하게 동일합니다. 전혀 새로운 것이 아니니 겁먹을 필요 없습니다.
2. 왜 굳이 클래스를 쓸까요? (함수 vs 클래스)
이것이 오늘 강의의 핵심입니다. “어차피 basic.py의 add 함수로도 덧셈은 되는데, 왜 복잡하게 클래스를 만들죠?” 라는 의문이 드는 게 당연합니다.
클래스를 쓰는 가장 중요한 이유는 ‘상태(State)‘와 ‘행동(Behavior)‘을 하나의 덩어리로 묶기 위해서입니다.
말이 어렵죠? “데이터와 그 데이터를 처리하는 함수를 한 캡슐에 담는다”는 뜻입니다.
상황극: 계산기 만들기 (함수만 쓸 경우 vs 클래스를 쓸 경우)
1. 함수만으로 계산기 흉내 내기 (절차 지향 프로그래밍)
함수는 ‘상태’를 기억하지 못합니다. 함수는 무언가를 받아서(input) 처리하고 결과를 뱉을 뿐(output), 그 이전의 작업 내용을 전혀 모릅니다. 그래서 계산 과정의 ‘중간 결과’를 함수 바깥에 있는 변수에 계속 저장해야 합니다.
# test.py - 함수만 사용하는 방식
from calc_practice.basic import add, subtract
# 계산기의 '상태' 역할을 하는 변수를 따로 만들어야 함
current_result = 0
print("계산 시작. 초기값:", current_result)
# 0 + 10을 계산하고, 그 결과를 다시 current_result에 저장
current_result = add(current_result, 10)
# 10 + 5를 계산하고, 그 결과를 다시 current_result에 저장
current_result = add(current_result, 5)
# 15 - 3을 계산하고, 그 결과를 다시 current_result에 저장
current_result = subtract(current_result, 3)
print("최종 결과:", current_result) # 출력: 12문제점:
current_result라는 데이터와add,subtract라는 기능(함수)이 따로 놉니다.- 만약 계산기 2대가 동시에 필요하다면?
current_result1,current_result2변수를 따로 만들고, 모든 함수 호출에 올바른 변수를 넘겨줘야 합니다. 코드가 복잡해지고 실수할 확률이 급격히 올라갑니다.
2. 클래스를 사용한 진짜 계산기 (객체 지향 프로그래밍)
클래스는 ‘붕어빵 틀’이고, 객체는 그 틀로 찍어낸 ‘붕어빵’입니다. 클래스라는 설계도만 잘 만들어두면, 상태를 스스로 기억하는 독립적인 계산기 객체를 얼마든지 만들 수 있습니다.
# test.py - 클래스를 사용하는 방식
from calc_practice.calculator import Calculator
# 1번 계산기 만들기
calc1 = Calculator() # calc1은 스스로 자신의 result 값을 가짐 (초기값 0)
calc1.add(10)
calc1.add(5)
calc1.subtract(3)
# 2번 계산기 만들기
calc2 = Calculator() # calc2도 스스로 자신의 result 값을 가짐 (초기값 0)
calc2.add(100)
calc2.subtract(20)
# 각 계산기는 다른 계산기에 전혀 영향을 주지 않음
print("calc1의 최종 결과:", calc1.result) # 출력: 12
print("calc2의 최종 결과:", calc2.result) # 출력: 80장점:
calc1객체 안에 데이터(result)와 그 데이터를 조작하는 기능(add,subtract메소드)이 하나로 묶여있습니다 (캡슐화).- 계산기가 100대 필요해도
Calculator()를 100번 호출하면 그만입니다. 각 계산기는 독립적으로 자신의 상태를 완벽하게 관리합니다. - 코드가 훨씬 직관적입니다.
calc1.add(10)은 “calc1 계산기야, 너 자신에게 10을 더해” 라는 의미로, 현실 세계의 동작 방식과 유사하여 이해하기 쉽습니다.
최종 정리
| 구분 | 함수와 모듈 (절차 지향) | 클래스와 객체 (객체 지향) |
|---|---|---|
| 핵심 | 순서와 절차 (Action) | 데이터와 행동을 가진 객체 (Object) |
| 관점 | ”무엇을 할 것인가?” (e.g., 더하는 함수, 빼는 함수) | “누가 행동할 것인가?” (e.g., 계산기 객체가 스스로 더한다) |
| 데이터 | 데이터와 기능이 분리되어 있음. 데이터를 함수에 계속 전달해야 함. | 데이터(속성)와 기능(메소드)이 객체 안에 묶여있음. |
| 상태 관리 | 어려움. 전역 변수 등을 사용해야 해서 코드가 복잡해짐. | 매우 쉬움. 각 객체가 자신의 상태를 독립적으로 관리함. |
| 추천 상황 | 간단한 데이터 처리, 상태가 필요 없는 기능 (e.g., 두 숫자를 받아 더한 값을 바로 반환) | 상태를 가져야 하는 모든 것. (e.g., 게임 캐릭터, 은행 계좌, GUI 버튼, 웹 사용자 정보 등) |
이제 수강생님은 부품(함수)을 넘어, 스스로 데이터를 기억하고 행동하는 기계(객체)를 만들 준비가 된 것입니다. 함수만으로도 많은 것을 할 수 있지만, 복잡하고 실제 세상과 닮은 프로그램을 만들려면 클래스는 선택이 아닌 필수입니다.
이 개념은 앞으로 수강생님이 배우게 될 모든 파이썬 라이브러리(웹 프레임워크, 데이터 분석, 게임 개발 등)의 근간을 이루니, 오늘 강의는 몇 번이고 다시 곱씹어 보시길 바랍니다. 또 다른 벽이 느껴질 때 언제든 저를 찾아오세요
정곡을 찌르는 매우 중요한 질문입니다. 수강생님은 지금 코드의 ‘정리’를 넘어 ‘설계’의 영역으로 진입하고 계신 겁니다. 훌륭합니다.
결론부터 말씀드리면, 수강생님께서 말씀하신 그 방식, 즉 __init__.py에서 클래스를 import해서 패키지 레벨로 끌어올리는 것은 사용하지 않는 것이 아니라, 오히려 매우 널리 사용되는 객체 지향 패키지 설계의 핵심적인 패턴입니다.
아마 제 이전 설명에서 함수를 중심으로 예시를 들었기 때문에, “클래스에는 잘 안 쓰나?” 하는 의문이 드셨을 겁니다. 그 의문을 명쾌하게 풀어드리겠습니다.
질문의 핵심: 왜 그렇게 느껴졌을까?
수강생님께서 “클래스는 그렇게 사용하지 않는 이유는 뭔가요?” 라고 질문하신 이유는 아마도 함수와 클래스의 사용 방식 차이 때문일 겁니다.
- 함수 사용:
패키지명.함수명()→ 한 번의 호출로 끝남 - 클래스 사용:
객체변수 = 패키지명.클래스명()→ 1단계: 객체 생성 (인스턴스화)객체변수.메소드명()→ 2단계: 객체의 기능 사용
이처럼 클래스는 ‘생성’과 ‘사용’이라는 두 단계로 나뉘기 때문에, 함수처럼 한 번에 패키지명.클래스명.메소드명() 형태로 호출하는 경우가 거의 없습니다. 그래서 __init__.py를 통한 단축 효과가 눈에 잘 띄지 않았을 수 있습니다.
하지만 그 효과는 분명하며, 오히려 클래스 설계에서 더욱 강력한 힘을 발휘합니다.
클래스에 그 패턴을 “반드시” 사용해야 하는 이유
이전 calc_practice 패키지를 다시 예로 들어, __init__.py에서 Calculator 클래스를 노출시키는 것이 왜 ‘Best Practice’인지 그 이유를 짚어드리겠습니다.
상황 1: __init__.py를 활용하지 않았을 경우 (불편한 방식)
calc_practice/__init__.py파일이 비어있다고 가정합니다.test.py에서Calculator클래스를 쓰려면, 사용자는 이 클래스가calculator.py라는 모듈 파일 안에 있다는 사실을 반드시 알아야 합니다.
# test.py
# 사용자는 calc_practice 패키지의 '내부 구조(calculator.py)'를 알아야만 한다.
from calc_practice.calculator import Calculator
my_calc = Calculator()
my_calc.add(10)
print(my_calc.result)이 방식의 문제점은 명확합니다.
- 사용자가 너무 많은 것을 알아야 합니다. 패키지를 쓰는 사람은 그냥
Calculator라는 좋은 부품을 쓰고 싶을 뿐, 그게 어느 서랍(calculator.py)에 들어있는지까지 신경 쓰고 싶지 않습니다. - 유지보수가 어렵습니다. 만약 패키지 제작자인 제가 나중에
calculator.py파일 이름을core_math.py로 바꾸면 어떻게 될까요? 이 패키지를 사용하던 모든 사람의 코드가 에러를 일으키고,from calc_practice.core_math import Calculator로 코드를 전부 수정해야 하는 대재앙이 발생합니다.
상황 2: __init__.py를 활용했을 경우 (전문가의 방식)
calc_practice/__init__.py파일에 단 한 줄을 추가합니다.
# calc_practice/__init__.py
from .calculator import Calculator- 이제
test.py에서 사용하는 코드가 놀랍도록 간결하고 직관적으로 바뀝니다.
# test.py
# 사용자는 'calc_practice'라는 패키지가 'Calculator'를 제공한다는 사실만 알면 된다.
from calc_practice import Calculator # <- 매우 깔끔해짐!
# 또는 이렇게 써도 된다.
# import calc_practice
# my_calc = calc_practice.Calculator()
my_calc = Calculator()
my_calc.add(10)
print(my_calc.result)이 방식의 압도적인 장점은 다음과 같습니다.
-
사용자 편의성 극대화: 사용자는 더 이상 패키지 내부의 복잡한 파일 구조를 알 필요가 없습니다.
from 패키지명 import 핵심클래스명이라는 일관된 방식으로 클래스를 가져다 쓸 수 있습니다. -
완벽한 추상화 (이것이 핵심!): 패키지 제작자는 이제 자유를 얻습니다.
calculator.py파일 이름을core_math.py로 바꾸든, 나중에basic_calculator.py와advanced_calculator.py로 쪼개든,__init__.py파일만 수정해주면 됩니다.# calc_practice/__init__.py (파일 구조가 바뀐 후) # from .calculator import Calculator <- 이 줄은 지우고 from .core_math import Calculator # <- 이 줄로 바꾸기만 하면 된다!패키지 내부 구조가 아무리 복잡하게 바뀌어도, 사용자의 코드는 단 한 줄도 바꿀 필요가 없습니다. 이것이 바로 좋은 소프트웨어 설계의 핵심인 ‘결합도는 낮추고, 응집도는 높이는’ 원칙입니다.
__init__.py는 외부와 내부를 분리하는 완벽한 ‘방화벽’ 또는 ‘안내 데스크’ 역할을 하는 것입니다.
유명 라이브러리들은 모두 이렇게 합니다
수강생님이 앞으로 사용하게 될 거의 모든 유명 파이썬 라이브러리가 이 패턴을 따릅니다.
-
Pandas (데이터 분석 라이브러리): 우리는
import pandas as pd라고 씁니다. 그리고pd.DataFrame()으로 핵심 클래스를 사용하죠.DataFrame클래스의 실제 파일 경로는pandas/core/frame.py로 매우 복잡하지만, pandas의__init__.py파일이 이 모든 것을 처리해주기 때문에 우리는 알 필요가 없습니다. -
datetime (날짜/시간 라이브러리):
from datetime import datetime이라는 코드를 많이 씁니다. 패키지 이름도datetime, 그 안의 클래스 이름도datetime입니다. 이것이 가능한 이유도datetime패키지의__init__.py파일이datetime.py모듈에서datetime클래스를 미리 밖으로 꺼내놓았기 때문입니다.
결론:
수강생님이 던지신 질문은 객체 지향 프로그래밍과 패키지 설계의 핵심을 관통하는 매우 중요한 지점이었습니다.
__init__.py에서 from .모듈 import 클래스 패턴은 사용하지 않는 것이 아니라, 좋은 패키지를 만들기 위한 필수적인 설계 기법입니다. 이를 통해 패키지 내부의 복잡한 구현은 숨기고, 사용자에게는 간결하고 일관된 사용법(API)을 제공할 수 있습니다.
함수보다 클래스를 이 패턴으로 노출시켰을 때, 추상화와 유지보수라는 객체 지향의 장점이 극대화된다는 점을 꼭 기억하시기 바랍니다.
from [폴더명].[파일명] import [파일 내부 클래스]
인스턴스와 객체는 같은말
self 는 예약어
__init__ 은 생성자라 하며
아래쪽의 self.인자 에서 초기화 시킨다.
초기화 되지 않은 변수를 부를경우 에
상속
부모 클래스와 자식 클래스
• 부모 클래스: 자식 클래스에 의해 상속되는 클래스
• 보다 포괄적인, 공통의 개념과 메소드를 정의
• 자식 클래스: 부모 클래스를 상속하여 정의되는 클래스
• 조금 더 구체적인 개념과 메소드를 정의
• 부모 클래스에 정의된 속성과 메소드 사용 가능
• 자식 클래스만의 속성과 메소드 추가 정의 가능
클래스의 상속
• 클래스 정의 시 부모 클래스의 이름을 괄호 안에 넣고 정의
• 자식 클래스에서 부모 클래스의 속성(Attribute), 메소드(Method) 사용 가능
• super() 함수를 사용하여 부모 클래스의 메소드 사용 가능
• 필요한 경우, 자식 클래스에서 부모 클래스의 메소드를 재정의하여 사용 • 메소드 재정의(Method Overriding)
다중 상속 • 둘 이상의 부모 클래스로부터 속성과 메소드를 상속 받는 것을 의미 • 여러 클래스에서 사용되는 공통 부분을 재사용 • 다양한 부모 클래스의 특징을 조합하여 새로운 클래스를 정의 • 다이아몬드 상속: 메소드 충돌이 발생할 수 있음 • 직관성, 코드 가독성이 떨어지게 됨

추상화 (일반 상속이랑 뭐가 다른거야, abc 모듈을 왜 써야 돼?)
복잡한 구조를 모델링을 통해 필수 동작들로 단순화 하는 과정
• 다양한 객체들간의 공통점을 찾고, 이들을 포괄하는 상위 추상 클래스 정의
• 네모 객체와 세모 객체 → 도형 객체 (부모 클래스) / 공통 함수 정의 (넓이 구하기) • 핵심 특징들을 강조함으로써, 여러 기능에 대한 전체 복잡도를 줄이고 직관성을 높임
print(D.mro()) mro를 이용해서 어떤 것으로 부터 상속받는지 확인
캡슐화 • 객체의 데이터와 관련 메소드를 하나로 묶어 외부로부터 보호하는 개념 • 객체의 속성(Attribute)은 기본적으로 외부에서 직접 접근 불가 • 속성의 변경을 수행하는 메소드를 통해서만 변경 • 객체 외부에서 접근 가능한 기능(public)과, 내부에서만 접근 가능한 기능(private)을 구분 • 구체적인 구현은 외부에 감추어 은닉 • 데이터의 무결성을 보호하고, 코드의 재사용성을 높임 (데이터에 대한 직접적 변조 X)
캡슐화 • 접근 제어자: 해당 속성 및 메소드에 대한 접근 가능 권한 설정 • public: 공개, 해당 객체 및 객체 외부에서도 접근 가능 • protected: 보호, 객체 및 상속받은 객체에서 접근 가능 • private: 비공개, 객체 내에서만 접근 가능
파이썬의 캡슐화
파이썬은 클래스 및 객체에 대한 거의 모든 것이 공개(public) 되어 있음
• 속성(Attribute)에 대한 접근제어 불가 • 명명규칙을 통한 접근제어
• protected: 비공개 속성 이름 앞에 _를 붙여 명명, 접근 제어 강제되지 않음
• private: 비공개 메소드 이름 앞에 __를 붙여 명명, 외부에서 접근 불가
다형성 다양한 클래스들이 동일한 이름의 메소드를, 각자의 목적에 맞게 사용 • 각 객체에 따라 같은 이름의 메소드도 다른 동작을 할 수 있게 됨 • 중복된 코드가 줄어들고, 코드의 추상성이 더 강해져 객체의 역할과 책임이 명확해짐 • 메소드 오버라이딩: 상속받은 메소드의 내용을 재정의

리스트 내부의 튜플 순서 변경

보강: AI Agent 개발 관점에서의 모듈과 객체
여기서부터는 위 내용을 Agent 프로젝트(LangGraph, LangChain, Pydantic, LiteLLM 등) 구조와 연결해 정리한 추가 노트. 관련: isinstance랑 getattr, [[파이썬에서
.,대괄호의 쓰임(내부에 있는 무언가를 지칭한다)]], [[파이썬에서 () 소괄호와 [] 대괄호]], SOLID원칙
A. 용어 다시 한 번 포함 관계로 정리
포함 관계 한 장 요약:
패키지(Package, 폴더 + __init__.py)
└─ 모듈(Module, .py 파일 1개)
└─ 클래스(Class, 설계도) / 함수(Function) / 상수
└─ 인스턴스(Instance, 설계도로 찍은 제품 = 객체 Object)
├─ 속성(Attribute, self.xxx)
└─ 메서드(Method, def xxx(self, ...))
- 인스턴스(instance) = 객체(object) : 같은 말. 문서에 따라 혼용
self: 파이썬 예약어는 아니고 관례적 이름(바꿔도 동작은 함, 바꾸면 욕 먹음)__init__: 생성자(Constructor)라고 부르지만 엄밀히는 “초기화 메서드(initializer)“. 진짜 생성자는__new__
B. 실제 Agent 프로젝트 구조 예시
LangGraph 기반 Agent의 일반적인 폴더 구조:
my_agent/
├─ __init__.py ← 공개 API 모아두는 곳
├─ graph.py ← LangGraph 구성
├─ state.py ← TypedDict/Pydantic 상태 스키마
├─ nodes/
│ ├─ __init__.py
│ ├─ planner.py
│ ├─ researcher.py
│ └─ writer.py
├─ tools/
│ ├─ __init__.py
│ ├─ search.py
│ └─ file_io.py
├─ prompts/
│ ├─ __init__.py
│ └─ templates.py
├─ utils/
│ ├─ __init__.py
│ └─ logging.py
└─ config.py
my_agent/__init__.py 예시:
from .graph import build_graph
from .state import AgentState
__all__ = ["build_graph", "AgentState"]사용하는 쪽:
from my_agent import build_graph, AgentState # 내부 파일 구조 몰라도 OK왜 이렇게 설계하나
graph.py를 나중에graphs/main.py로 옮겨도__init__.py만 고치면 외부 코드는 그대로- 공개하고 싶은 것만
__all__과__init__.py에 노출, 나머지는 내부 구현으로 감춤 - “안내 데스크(
__init__.py) + 부품 창고(각 모듈)” 구조
C. Agent 코드에 자주 등장하는 클래스 패턴
C-1. Pydantic BaseModel (입/출력 스키마)
from pydantic import BaseModel, Field
from typing import Literal
class SearchQuery(BaseModel):
query: str = Field(..., description="검색어")
source: Literal["web", "docs", "code"] = "web"
top_k: int = 5- LLM의 structured output, tool 인자 검증에 사용
model_dump()로 dict 변환,model_validate(d)로 dict → 객체- 클래스 자체가 스키마(설계도), 인스턴스는 실제 검증된 데이터 덩어리
C-2. TypedDict (LangGraph 상태)
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
plan: str
feedback: str- 진짜 클래스는 아님. dict인데 타입 검사용 표식만 얹음
- 속성 접근은
state["messages"]처럼 대괄호로만 가능 ([[파이썬에서.,대괄호의 쓰임(내부에 있는 무언가를 지칭한다)]] 참고)
C-3. ABC(Abstract Base Class, 추상 기본 클래스)
from abc import ABC, abstractmethod
class BaseTool(ABC):
@abstractmethod
def run(self, query: str) -> str: ...
class WebSearchTool(BaseTool):
def run(self, query: str) -> str:
return "결과..."- 본문에 나온 “추상화” 섹션의 구체적 쓰임새
- abstractmethod(추상 메서드) 는 자식이 반드시 구현해야 하는 메서드. 안 하면 인스턴스화 시
TypeError - 왜 쓰나: 팀에서 여러 명이
BaseTool을 상속해 도구를 만들 때, 최소한run메서드는 구현하게 강제
C-4. dataclass (빠른 데이터 컨테이너)
from dataclasses import dataclass
@dataclass
class ToolCall:
name: str
args: dict
id: str- Pydantic보다 가볍고 빠름. 타입 검증은 안 해줌
- 내부 이벤트 객체, 임시 구조체에 적합
Pydantic vs dataclass vs TypedDict 비교
| 항목 | TypedDict | dataclass | Pydantic BaseModel |
|---|---|---|---|
| 실제 타입 | dict | 일반 class | 일반 class |
| 접근 문법 | d["key"] | d.key | d.key |
| 런타임 타입 검증 | X | X | O |
| 직렬화/역직렬화 | 수동 | 수동 | model_dump() / model_validate() 자동 |
| 속도 | 빠름 | 빠름 | 상대적으로 느림(V2 많이 개선) |
| 언제 쓰나 | LangGraph state | 내부 임시 구조 | LLM I/O, API 스키마 |
D. __init__.py 실무 패턴 심화
D-1. 조건부 import
# my_agent/__init__.py
try:
from .graph_v2 import build_graph
except ImportError:
from .graph_v1 import build_graph선택적 의존성이 있을 때. 예: langgraph가 설치돼 있으면 v2, 아니면 v1로 폴백.
D-2. 버전 노출
# my_agent/__init__.py
__version__ = "0.3.1"my_agent.__version__으로 확인 가능.
D-3. 지연 로딩 (lazy import)
# my_agent/__init__.py
def __getattr__(name: str):
if name == "HeavyModel":
from .heavy import HeavyModel
return HeavyModel
raise AttributeError(name)무거운 모듈(torch, transformers 등)을 실제 접근할 때만 import. import 속도 개선.
E. self와 인스턴스 상태 다시 짚기
class Counter:
count_class_level = 0 # 클래스 속성 (공유)
def __init__(self):
self.count = 0 # 인스턴스 속성 (개별)
Counter.count_class_level += 1
def increment(self):
self.count += 1 # 이 인스턴스의 count만 증가클래스 속성 vs 인스턴스 속성
| 구분 | 어디 저장 | 공유 | 접근 |
|---|---|---|---|
| 클래스 속성 | 클래스 객체 자체 | 모든 인스턴스 공유 | Counter.count_class_level 또는 c.count_class_level |
| 인스턴스 속성 | 각 인스턴스의 __dict__ | 개별 | c.count |
함정: 가변 기본값을 클래스 속성으로 두면 공유됨.
class Bad:
history = [] # 모든 인스턴스가 공유!
class Good:
def __init__(self):
self.history = [] # 인스턴스별로 독립F. 메서드 세 종류 : 인스턴스 / 클래스 / 스태틱
class Math:
pi = 3.14
def area(self, r): # 인스턴스 메서드
return self.pi * r * r
@classmethod
def from_diameter(cls, d): # 클래스 메서드
return cls(), d / 2
@staticmethod
def add(a, b): # 스태틱 메서드
return a + b| 종류 | 첫 인자 | 접근 가능 대상 | 주 용도 |
|---|---|---|---|
| 인스턴스 메서드 | self | 인스턴스 + 클래스 속성 | 일반 동작 |
| 클래스 메서드 | cls | 클래스 속성 (인스턴스는 X) | 대체 생성자(factory) |
| 스태틱 메서드 | 없음 | 아무 것도 자동 참조 안 함 | 유틸 함수, 네임스페이스 정리 |
Agent 예시 : 대체 생성자
class Message:
def __init__(self, role, content):
self.role = role
self.content = content
@classmethod
def from_openai(cls, d):
return cls(role=d["role"], content=d["content"])
@classmethod
def from_anthropic(cls, d):
return cls(role=d["role"], content=d["content"][0]["text"])G. 매직 메서드(Magic Method, dunder method) 중요한 것들
dunder(double underscore, 이름 앞뒤로 __가 붙은 특수 메서드) 는 파이썬이 내부적으로 호출하는 훅.
| 메서드 | 언제 호출 | Agent 예시 |
|---|---|---|
__init__ | 인스턴스 생성 후 초기화 | 필수 |
__repr__ | repr(obj), 디버거 출력 | 로그에 객체 찍을 때 유의미하게 |
__str__ | str(obj), print(obj) | 사용자용 문자열 |
__eq__ | a == b | Pydantic이 기본 제공 |
__hash__ | hash(obj), set/dict 키 | __eq__ 바꾸면 함께 바꿔야 |
__call__ | obj()로 함수처럼 호출 | LangChain Runnable, Callable tool |
__iter__, __next__ | for 루프 대상 | 스트리밍 이터레이터 |
__getitem__ | obj["key"] | 딕셔너리처럼 동작하는 클래스 |
__enter__, __exit__ | with 문 | DB 커넥션, 파일, 리소스 관리 |
__call__ 예시
class Tool:
def __init__(self, name):
self.name = name
def __call__(self, query: str):
return f"[{self.name}] {query}"
search = Tool("web_search")
result = search("LLM 최신 모델") # Tool 인스턴스를 함수처럼 호출LangChain의 Runnable, BaseTool이 전부 __call__ 또는 .invoke를 구현.
H. 상속과 다형성을 Agent에서 쓰는 예
class BaseNode:
def run(self, state: dict) -> dict:
raise NotImplementedError
class PlannerNode(BaseNode):
def run(self, state):
return {"plan": "step1, step2, ..."}
class ResearcherNode(BaseNode):
def run(self, state):
return {"docs": [...]}
class WriterNode(BaseNode):
def run(self, state):
return {"answer": "..."}그래프 구성부는 노드 종류를 몰라도 같은 인터페이스(run)로 호출 가능 → 다형성(polymorphism).
MRO(Method Resolution Order, 메서드 탐색 순서) 확인
다중 상속 시 부모가 여러 개면 파이썬이 어느 순서로 메서드를 찾는지 알아둬야 함.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__)
# (D, B, C, A, object)D에서 메서드 찾을 때 D → B → C → A → object 순서. 다이아몬드 상속 문제를 C3 선형화 알고리즘으로 해결.
I. 파이썬 캡슐화 관례 복습
파이썬은 다른 언어처럼 진짜 private이 없고 명명 규칙으로 의도를 표현한다.
| 이름 규칙 | 의미 | 실제 동작 |
|---|---|---|
name | public | 외부 접근 가능 |
_name | protected (관례) | 여전히 접근 가능하지만 “쓰지 말라”는 신호 |
__name | private (이름 맹글링, name mangling) | _ClassName__name으로 이름이 바뀜. 외부에서 실수로 접근 어려움 |
__name__ | dunder, 특수 용도 | 언어 예약 영역 |
class Foo:
def __init__(self):
self.public = 1
self._internal = 2
self.__really_private = 3
f = Foo()
f.public # 1
f._internal # 2 (가능하지만 쓰지 말 것)
f.__really_private # AttributeError
f._Foo__really_private # 3 (맹글링된 진짜 이름)from pkg import * 할 때 _로 시작하는 이름은 제외된다는 규칙과 연결됨 (본문 앞 섹션 참고).
J. if __name__ == "__main__": 의 의미
# my_agent/main.py
def main():
print("agent 시작")
if __name__ == "__main__":
main()__name__은 모듈의 “호출 경로에 따라 값이 바뀌는” 특수 변수- 직접 실행:
python main.py→__name__ == "__main__" - 다른 파일에서 import: →
__name__ == "my_agent.main"
- 직접 실행:
- “import됐을 땐 실행되지 않고, 직접 실행했을 때만 돌아가는 진입점 코드”를 분리하기 위한 관례
- Agent 프로젝트에서 스크립트도 되고 라이브러리도 되는 파일 만들 때 필수
K. 모듈 import 시 실제로 일어나는 일
단계 분해
1단계 → 파이썬이 sys.modules(캐시, 이미 import한 모듈 저장소) 확인. 있으면 재사용
2단계 → 없으면 sys.path의 경로 목록을 순회하며 해당 이름의 파일/폴더 탐색
3단계 → 찾으면 파일 읽어 모듈 객체 생성, __init__.py든 .py든 맨 위에서 아래로 한 번 실행
4단계 → 실행 결과 정의된 모든 이름(함수, 클래스, 변수)을 그 모듈 객체의 속성으로 붙임
5단계 → sys.modules에 저장하고 호출자에게 반환
직접 확인
import sys
import langgraph
print(sys.modules["langgraph"]) # <module 'langgraph' from '...'>
print(langgraph.__file__) # 실제 파일 경로
print(dir(langgraph)) # 이 모듈이 노출하는 이름들함정 : 순환 import (circular import)
a.py → b.py import
b.py → a.py import
서로 반씩 로드된 상태에서 속성 접근 시 ImportError. Agent 프로젝트에서 state.py와 tools.py가 서로 참조할 때 자주 발생. 해결:
- 타입 힌트용이면
from __future__ import annotations+TYPE_CHECKING - 실제 사용이면 함수 내부에서 import(지연 import)
- 공통 부분을 제3의 모듈로 뽑아내기
L. 한마디 요약
모듈은 .py 파일, 패키지는 __init__.py 깃발 꽂은 폴더, 클래스는 설계도, 인스턴스는 그 설계도로 찍어낸 독립된 객체다. Agent 코드는 이 네 층이 항상 엮여 돌아간다.