함수와 모듈: 코드를 나누고 다시 쓰기

함수를 정의하고 값을 돌려받는 법, 매개변수와 기본값, 변수의 유효 범위, 좋은 함수의 조건을 익히고, 모듈과 표준 라이브러리, pip와 가상 환경까지 코드를 부품으로 나누는 법을 배워요.

초급강의 4 / 6약 9분

프로그램이 길어지면 같은 코드를 복사해 붙이는 일이 생겨요. 그러다 한 곳을 고치면 나머지도 다 찾아 고쳐야 하고, 하나라도 빠뜨리면 버그가 돼요. 함수는 이런 코드를 이름 붙은 부품으로 만드는 도구이고, 모듈은 그 부품들을 파일 단위로 묶어 다시 쓰는 방법이에요.

함수의 기본

정의와 호출

def greet(name):
    print(f"안녕하세요, {name}님!")

greet("민지")
greet("준혁")
  • def로 함수를 정의해요. 이름, 괄호 안의 매개변수, 콜론, 그리고 들여쓴 본문으로 이뤄져요.
  • 정의만으로는 아무 일도 일어나지 않아요. greet("민지")처럼 호출해야 본문이 실행돼요.
  • 호출할 때 넘기는 실제 값("민지")을 인자라고 불러요.

return: 결과 돌려주기

def area_of_circle(r):
    return 3.14159 * r * r

a = area_of_circle(3)
print(f"넓이: {a:.2f}")

print는 화면에 보여 주기만 하고, return은 호출한 곳으로 값을 돌려줘요. 돌려받은 값은 변수에 담거나 다른 계산에 쓸 수 있어요. 함수를 처음 배울 때 가장 많이 헷갈리는 부분이에요.

def add_print(a, b):
    print(a + b)

def add_return(a, b):
    return a + b

x = add_print(2, 3)    # 화면에 5가 찍히지만
print(x)               # None  (돌려준 값이 없어요)

y = add_return(2, 3)
print(y * 10)          # 50

return을 만나면 함수는 그 자리에서 끝나요. 값을 여러 개 돌려줄 수도 있어요.

def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([4, 9, 1, 7])
print(lo, hi)   # 1 9

매개변수 다루기

기본값

def make_coffee(size="톨", shot=1):
    return f"{size} 사이즈, 샷 {shot}개"

print(make_coffee())                 # 톨 사이즈, 샷 1개
print(make_coffee("그란데"))           # 그란데 사이즈, 샷 1개
print(make_coffee(shot=2))           # 톨 사이즈, 샷 2개

기본값이 있는 매개변수는 생략할 수 있어요. 마지막 예처럼 이름=값 형태로 넘기는 것을 키워드 인자라고 하는데, 순서를 신경 쓰지 않아도 되고 읽기에도 분명해요.

주의 기본값으로 리스트나 딕셔너리 같은 바뀌는 값을 쓰면 안 돼요. def add(item, bag=[])처럼 쓰면 호출할 때마다 같은 리스트가 계속 쌓여요. 대신 bag=None으로 두고 함수 안에서 if bag is None: bag = []로 만드세요.

개수가 정해지지 않은 인자

def total(*numbers):
    return sum(numbers)

print(total(1, 2, 3))        # 6
print(total(10, 20, 30, 40)) # 100

*numbers는 넘어온 인자들을 하나의 묶음(튜플)으로 받아요. 키워드 인자를 묶어 받는 **options도 있지만, 처음에는 이런 게 있다는 것만 알아 두면 충분해요.

변수의 유효 범위

함수 안에서 만든 변수는 함수 밖에서 보이지 않아요.

def calc():
    result = 42
    return result

calc()
print(result)   # NameError: 함수 안의 변수는 밖에서 못 써요

함수 안의 변수는 지역 변수, 파일 맨 바깥의 변수는 전역 변수예요. 함수 안에서 전역 변수를 읽을 수는 있지만, 바꾸는 것은 피하는 게 좋아요. 함수가 바깥 상태를 몰래 바꾸면 어디서 값이 바뀌었는지 추적하기 어려워져요.

필요한 값은 인자로 받고, 결과는 return으로 돌려주세요. 이렇게 입력과 출력만으로 동작하는 함수를 순수 함수라고 해요. 순수 함수는 테스트하기 쉽고, 어디서 불러도 똑같이 동작해서 믿을 수 있어요.

좋은 함수의 조건

  1. 한 가지 일만 해요. 이름을 지을 때 "그리고"가 들어가면 둘로 나눌 신호예요. load_and_clean_and_save_data는 세 함수가 돼야 해요.
  2. 이름이 동사로 시작해요. get_price, calculate_tax, is_valid_email처럼 무엇을 하는지 드러나게요. 참거짓을 돌려주는 함수는 is_, has_로 시작하면 읽기 좋아요.
  3. 짧아요. 한 화면(대략 30줄)을 넘어가면 쪼갤 곳이 있는지 보세요.
  4. 설명이 있어요. 함수 바로 아래 따옴표 세 개로 쓰는 독스트링에 무엇을 받고 무엇을 돌려주는지 적어요.
def calculate_discount(price: int, rate: float) -> int:
    """할인된 가격을 원 단위 정수로 돌려준다.

    price: 원래 가격(원)
    rate: 할인율 (0.2는 20%)
    """
    return int(price * (1 - rate))

price: int, -> int 같은 표시는 타입 힌트예요. 실행에는 영향을 주지 않지만, 에디터가 실수를 미리 잡아 주고 읽는 사람이 이해하기 쉬워져요.

모듈: 파일 단위로 나누기

파이썬 파일 하나가 곧 모듈이에요. 다른 파일의 함수를 import로 가져다 쓸 수 있어요.

표준 라이브러리

파이썬에는 설치 없이 바로 쓸 수 있는 모듈이 수백 개 들어 있어요. 자주 쓰는 것만 맛보기로 볼게요.

import math
print(math.sqrt(16))         # 4.0
print(math.ceil(3.2))        # 4  올림

import random
print(random.choice(["가위", "바위", "보"]))

from datetime import date, timedelta
today = date.today()
print(today + timedelta(days=100))   # 100일 뒤 날짜

from pathlib import Path
print(Path.home())           # 내 홈 폴더 경로

import statistics
print(statistics.mean([70, 85, 90]))   # 평균

import math는 모듈 전체를 가져와 math.sqrt처럼 쓰고, from datetime import date는 필요한 것만 꺼내 date로 바로 써요. 어디서 온 이름인지 분명해야 할 때는 앞의 방식이 좋아요.

내 모듈 만들기

같은 폴더에 money.py를 만들어요.

# money.py
def won(amount):
    """숫자를 '1,234원' 형식으로 바꾼다."""
    return f"{amount:,}원"

def vat(amount, rate=0.1):
    """부가세를 원 단위로 계산한다."""
    return round(amount * rate)

그리고 main.py에서 가져다 써요.

# main.py
from money import won, vat

price = 35000
print(won(price))            # 35,000원
print(won(vat(price)))       # 3,500원

if __name__ == "__main__"

모듈 파일에 시험용 코드를 넣고 싶을 때가 있어요. 그런데 그냥 넣으면 import할 때마다 시험 코드도 실행돼요. 이걸 막는 관용구가 있어요.

# money.py 맨 아래
if __name__ == "__main__":
    print(won(1234567))   # 이 파일을 직접 실행할 때만 동작해요

파일을 직접 실행하면 __name__이 "__main__"이 되고, import되면 모듈 이름("money")이 돼요. 그래서 이 블록은 직접 실행할 때만 돌아요.

남이 만든 모듈: pip와 가상 환경

파이썬의 진짜 힘은 전 세계 개발자들이 만든 수십만 개의 패키지에 있어요. 웹 요청을 보내는 requests, 데이터 분석의 pandas, 머신러닝의 scikit-learn 같은 것들이요.

가상 환경부터 만들기

프로젝트마다 필요한 패키지와 버전이 달라요. 컴퓨터 전체에 마구 설치하면 프로젝트끼리 충돌해요. 그래서 프로젝트 폴더마다 독립된 가상 환경을 만들어요.

python -m venv .venv

활성화는 운영체제마다 달라요.

# 윈도우 PowerShell
.venv\Scripts\Activate.ps1

# 맥, 리눅스
source .venv/bin/activate

터미널 앞에 (.venv)가 붙으면 성공이에요. 이 상태에서 설치한 패키지는 이 프로젝트에만 들어가요.

설치와 기록

pip install requests
pip freeze > requirements.txt

requirements.txt에 지금 설치된 패키지와 버전이 기록돼요. 다른 컴퓨터에서는 pip install -r requirements.txt 한 줄로 같은 환경을 만들 수 있어요.

팁 .venv 폴더는 Git에 올리지 마세요. 용량이 크고 컴퓨터마다 달라요. 대신 requirements.txt를 올리면 충분해요.

실습: 숫자 맞히기 게임 다듬기

지난 강의의 게임을 함수로 나눠 보세요.

import random

def ask_number(prompt):
    """숫자를 입력받을 때까지 계속 묻고, 정수로 돌려준다."""
    while True:
        text = input(prompt)
        if text.isdigit():
            return int(text)
        print("숫자만 입력해 주세요.")

def play_round(low=1, high=100):
    """한 판을 진행하고 시도 횟수를 돌려준다."""
    secret = random.randint(low, high)
    tries = 0
    while True:
        guess = ask_number(f"{low}~{high} 사이 숫자: ")
        tries += 1
        if guess < secret:
            print("더 커요.")
        elif guess > secret:
            print("더 작아요.")
        else:
            return tries

def main():
    best = None
    while True:
        tries = play_round()
        print(f"정답! {tries}번 만에 맞혔어요.")
        if best is None or tries < best:
            best = tries
            print("최고 기록이에요!")
        if input("한 판 더? (y/n): ") != "y":
            break
    print(f"최고 기록: {best}번")

if __name__ == "__main__":
    main()

같은 기능인데 읽기가 훨씬 쉬워졌죠. 각 함수는 한 가지 일만 하고, main을 읽으면 전체 흐름이 한눈에 보여요. 이게 함수로 나누는 이유예요.

자주 하는 실수

  • 괄호 없이 함수 이름만 쓰기. main은 함수 그 자체이고, main()이어야 실행돼요.
  • print를 return 대신 쓰기. 결과를 다른 곳에서 써야 한다면 return이에요.
  • 모듈 파일 이름을 표준 모듈과 같게 짓기. random.py라고 저장하면 import random이 내 파일을 가져와서 이상한 오류가 나요.
  • 가상 환경 활성화를 잊기. 패키지를 설치했는데 못 찾는다면, 활성화하지 않은 터미널에서 실행했을 가능성이 커요.

점검 문제

  1. print와 return의 차이는?
  2. def f(a, b=2): return a * b일 때 f(3)과 f(3, 4)의 값은?
  3. 함수 안에서 만든 변수를 밖에서 쓰면 어떤 오류가 날까요?
  4. if __name__ == "__main__": 블록은 언제 실행되나요?
  5. 가상 환경을 쓰는 이유는?

정답

  1. print는 화면에 보여 주기만 하고, return은 호출한 곳으로 값을 돌려줘 다른 계산에 쓸 수 있게 해요.
  2. f(3)은 6, f(3, 4)는 12예요.
  3. NameError예요. 지역 변수는 함수 밖에서 보이지 않아요.
  4. 그 파일을 직접 실행할 때만이요. 다른 파일에서 import하면 실행되지 않아요.
  5. 프로젝트마다 필요한 패키지와 버전이 달라서, 서로 충돌하지 않게 독립된 환경을 만들기 위해서예요.

다음 강의

다음 강의에서는 리스트, 딕셔너리, 집합 같은 데이터 묶음을 배워요. 실제 프로그램이 다루는 데이터는 대부분 여러 값의 묶음이에요. 묶음을 자유롭게 다룰 수 있으면 만들 수 있는 것이 확 넓어져요.