Python을 조금만 공부하다 보면 같은 코드를 여러 번 작성하게 된다.
처음에는 복사해서 붙여넣어도 되지만 코드가 커질수록 수정해야 할 장소가 늘어나고, 로직의 의도도 파악하기 어려워진다.
이 문제를 해결하는 핵심 도구가 함수, 모듈, 패키지다.
이 글에서는 작은 코드 조각을 함수로 묶는 것부터 여러 파일로 프로그램을 분리하는 방법까지 정리한다.
예를 들어 여러 주문의 할인 금액을 계산한다고 해보자.
price = 50000
discount = price * 0.1
final_price = price - discount
비슷한 계산을 여러 곳에서 반복한다면 함수로 분리하는 편이 좋다.
def calculate_discount_price(price, rate):
discount = price * rate
return price - discount
이제 필요한 곳에서 호출할 수 있다.
result = calculate_discount_price(50000, 0.1)
print(result)
함수의 핵심은 반복되는 로직에 이름을 붙여 재사용하는 것이다.
함수는 def로 정의한다.
def greet():
print("안녕하세요.")
호출:
greet()
def greet(name):
print(f"안녕하세요, {name}님.")
여기서 name은 매개변수(parameter)다.
greet("kim")
호출할 때 전달한 "kim"은 인자(argument)다.
returndef add(a, b):
return a + b
result = add(10, 20)
print(result)
return을 만나는 순간 함수 실행도 종료된다.
def validate_age(age):
if age < 0:
return False
return True
명시적인 return이 없는 함수는 None을 반환한다.
def print_message():
print("hello")
result = print_message()
print(result)
Python에서는 여러 값을 쉼표로 나열해서 반환할 수 있다.
def get_order_summary():
total_price = 82000
order_count = 4
return total_price, order_count
받을 때는 언패킹할 수 있다.
total, count = get_order_summary()
실제로 반환되는 값은 튜플이다.
함수 안에서 만든 변수는 기본적으로 함수 내부에서만 사용할 수 있다.
def process():
message = "processing"
print(message)
process()
아래 코드는 오류가 난다.
print(message)
message는 함수의 지역 변수이기 때문이다.
tax_rate = 0.1
def calculate_tax(price):
return price * tax_rate
함수 내부에서 외부 값을 읽는 것은 가능하다.
하지만 외부 상태에 지나치게 의존하면 함수의 동작을 이해하기 어려워진다.
가능하면 필요한 값은 인자로 전달하는 편이 명확하다.
def calculate_tax(price, tax_rate):
return price * tax_rate
global은 신중하게 사용하자request_count = 0
def increase_count():
global request_count
request_count += 1
동작은 하지만 프로그램 규모가 커질수록 상태 변경 위치를 추적하기 어려워질 수 있다.
가능하면 값을 반환하고 외부에서 갱신하는 구조가 더 단순하다.
def increase_count(count):
return count + 1
request_count = 0
request_count = increase_count(request_count)
def request_api(timeout=3):
print(f"timeout: {timeout}초")
request_api()
request_api(10)
다음 코드는 예상치 못한 결과를 만들 수 있다.
def add_item(item, items=[]):
items.append(item)
return items
기본값 객체는 함수가 호출될 때마다 새로 만들어지는 것이 아니라 함수 정의 시 한 번 만들어진다.
안전한 패턴은 None을 사용하는 것이다.
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
def create_user(name, age=20, active=True):
return {
"name": name,
"age": age,
"active": active,
}
user = create_user(
name="kim",
active=False,
)
매개변수 이름을 직접 지정하면 호출 코드를 읽었을 때 각 값의 의미가 분명해진다.
*args정해지지 않은 개수의 위치 인자를 받을 때 사용한다.
def total(*numbers):
return sum(numbers)
print(total(10, 20, 30))
함수 내부에서 numbers는 튜플이다.
**kwargs정해지지 않은 개수의 키워드 인자를 받을 때 사용한다.
def print_options(**options):
for key, value in options.items():
print(key, value)
print_options(
timeout=3,
retries=5,
debug=True,
)
options는 딕셔너리다.
def add(a, b):
return a + b
numbers = [10, 20]
print(add(*numbers))
딕셔너리는 **를 사용한다.
def create_user(name, age):
return {
"name": name,
"age": age,
}
data = {
"name": "lee",
"age": 28,
}
user = create_user(**data)
def calculate_total(prices):
"""주어진 가격 목록의 총합을 반환한다."""
return sum(prices)
__doc__로 확인할 수 있다.
print(calculate_total.__doc__)
help()도 docstring을 활용한다.
help(calculate_total)
좋은 함수는 보통 하나의 분명한 역할을 가진다.
다음 함수는 너무 많은 일을 한다.
def process_order(order):
# 검증
# 할인 계산
# 결제
# 데이터베이스 저장
# 이메일 전송
...
역할별로 분리하면 테스트와 수정이 쉬워진다.
def validate_order(order):
...
def calculate_order_total(order):
...
def save_order(order):
...
def send_order_email(order):
...
함수는 단순히 코드 줄 수를 줄이는 도구가 아니라 프로그램의 책임을 나누는 단위다.
Python에서는 .py 파일 하나를 모듈로 사용할 수 있다.
price_utils.py
def calculate_discount(price, rate):
return price * (1 - rate)
다른 파일에서 불러올 수 있다.
import price_utils
price = price_utils.calculate_discount(50000, 0.1)
print(price)
함수 하나보다 더 큰 단위의 재사용이 필요할 때 모듈을 사용한다.
import가 하는 일import math
print(math.sqrt(16))
모듈 내부의 이름은 마침표로 접근한다.
math.sqrt
math.pi
이 방식은 이름의 출처가 명확하다는 장점이 있다.
from ... import ...from math import sqrt
print(sqrt(16))
간단하지만 같은 이름을 가진 함수가 많아질 경우 출처를 파악하기 어려울 수 있다.
대규모 코드에서는 다음처럼 명시적인 형태가 더 읽기 쉬운 경우가 많다.
import math
math.sqrt(16)
import *를 피하는 이유from some_module import *
어떤 이름이 현재 네임스페이스에 들어왔는지 코드만 보고 판단하기 어려워진다.
이름 충돌 가능성도 커진다.
__name__ == "__main__"Python 파일은 직접 실행할 수도 있고 다른 파일에서 import할 수도 있다.
두 상황을 구분할 때 다음 패턴을 자주 사용한다.
def main():
print("프로그램 실행")
if __name__ == "__main__":
main()
직접 실행하면 main()이 호출된다.
python app.py
하지만 다른 파일에서 import하면 자동 실행되지 않는다.
import app
sys.pathimport sys
for path in sys.path:
print(path)
ModuleNotFoundError가 발생했을 때 중요한 확인 포인트다.
dir()와 help()import math
print(dir(math))
help(math.sqrt)
인터넷 검색 없이도 실행 환경 안에서 객체의 기본 정보를 확인할 수 있다.
프로젝트가 커지면 모듈도 많아진다.
이때 여러 모듈을 디렉터리 단위로 묶어 관리하는 구조가 패키지다.
my_app/
main.py
services/
__init__.py
user_service.py
order_service.py
utils/
__init__.py
date_utils.py
price_utils.py
main.py에서는 다음처럼 사용할 수 있다.
from services.user_service import create_user
from utils.price_utils import calculate_discount
__init__.py는 항상 필수일까?과거에는 패키지 디렉터리에 __init__.py가 반드시 필요했다.
현재 Python에는 namespace package도 있기 때문에 모든 상황에서 필수는 아니다.
하지만 일반 애플리케이션 프로젝트에서는 패키지 경계를 명확하게 보여주고 초기화 코드를 둘 수 있다는 이유로 여전히 흔하게 사용된다.
__pycache__와 .pyc모듈을 실행하거나 import하면 디렉터리에 다음과 같은 폴더가 생길 수 있다.
__pycache__/
그 안에는 바이트코드 캐시인 .pyc 파일이 생성될 수 있다.
직접 수정할 파일은 아니다.
Git 저장소에서도 일반적으로 제외한다.
__pycache__/
*.pyc
별도의 import 없이 사용 가능하다.
len()
print()
sum()
range()
Python과 함께 제공되지만 import가 필요하다.
import json
import pathlib
import datetime
별도 설치가 필요하다.
python -m pip install requests
import requests
order_app/
main.py
services/
__init__.py
order_service.py
utils/
__init__.py
price.py
utils/price.py
def calculate_total(prices):
return sum(prices)
services/order_service.py
from utils.price import calculate_total
def create_order(prices):
total = calculate_total(prices)
return {
"items": len(prices),
"total": total,
}
main.py
from services.order_service import create_order
def main():
order = create_order([12000, 8000, 30000])
print(order)
if __name__ == "__main__":
main()
이 질문 중 여러 개에 해당한다면 함수나 모듈 분리를 고려해볼 수 있다.
*args, **kwargs는 가변 개수의 인자를 다룰 때 사용한다..py 파일 하나는 하나의 모듈이 될 수 있으며 import로 재사용할 수 있다.__name__ == "__main__" 패턴을 이용하면 직접 실행과 import 상황을 구분할 수 있다.dir(), help(), sys.path를 알면 Python 객체와 import 문제를 스스로 탐색하기 쉬워진다.