Python 함수

src/content/documents/python/python-functions-parameters-and-first-class-objects.json

결론: 좋은 함수는 입력 계약과 반환 결과가 선명하다

함수는 반복 코드를 감추는 상자만이 아니다. 어떤 값을 어떤 방식으로 받을지, 잘못된 입력을 어떻게 알릴지, 무엇을 돌려줄지를 이름과 signature로 표현하는 작은 계약이다. 데이터 분석에서는 읽기, 정제, 요약, 출력 단계를 함수로 나누면 각 단계를 따로 검사하고 재사용하기 쉬워진다.

def 문을 실행하면 함수 객체가 이름에 연결되고, 본문은 함수를 호출할 때 실행된다. 이 동작과 기본값 규칙은 Python 언어 레퍼런스의 Function definitions에서 확인할 수 있다 (2026-08-03 확인).

def, 호출, return의 가장 작은 예

def normalize_label(label):
    return label.strip().title()

def log_count(count):
    print(f"rows={count}")

result = normalize_label("  data science  ")
returned = log_count(3)
print(result)
print(returned is None)
rows=3
Data Science
True

return은 값을 호출 위치로 돌려주고 즉시 함수 실행을 끝낸다. return 문이 없거나 return만 쓰면 None을 반환한다. 출력한 값과 반환값은 다르므로, 분석 함수는 print만 하기보다 결과를 return하고 출력은 바깥에서 담당하게 하는 편이 테스트하기 쉽다.

다섯 가지 매개변수를 한 줄에서 읽기

parameter는 함수 정의에 적은 이름이고 argument는 호출할 때 전달하는 값이다. Python은 위치 전용, 위치 또는 키워드, 가변 위치, 키워드 전용, 가변 키워드 매개변수를 구분한다. / 왼쪽은 위치로만, * 오른쪽은 키워드로만 전달한다.

from inspect import signature

def load_column(source, /, column, *transforms, missing=None, **reader_options):
    return {
        "source": source,
        "column": column,
        "transforms": transforms,
        "missing": missing,
        "reader_options": reader_options,
    }

print(signature(load_column))
result = load_column(
    "sales.csv",
    "amount",
    str.strip,
    int,
    missing=0,
    encoding="utf-8",
)
print(result["transforms"][0].__name__)
print(result["reader_options"])
(source, /, column, *transforms, missing=None, **reader_options)
strip
{'encoding': 'utf-8'}
  • source는 / 앞의 positional-only다. source=처럼 호출할 수 없다.

  • column은 positional-or-keyword라서 두 방식 모두 가능하다.

  • *transforms는 남는 위치 argument를 tuple로 받는 variadic positional이다.

  • missing은 * 뒤의 keyword-only라서 이름을 써야 한다.

  • **reader_options는 정의되지 않은 나머지 키워드 argument를 dict로 받는 variadic keyword다.

정의 순서는 크게 일반 매개변수, *args, keyword-only, **kwargs 흐름이다. 기본값이 있는 일반 매개변수 뒤에는 * 전까지 기본값 없는 일반 매개변수를 둘 수 없다. 매개변수 종류와 /·* 규칙은 Python 공식 튜토리얼의 Special parameters에 예제와 함께 정리되어 있다 (2026-08-03 확인).

*args와 **kwargs는 확장 지점이 정말 필요할 때 유용하지만 이름과 허용값을 숨긴다. 애플리케이션 함수에서는 명시적 매개변수를 우선하고, 전달할 옵션 집합이 열려 있거나 wrapper를 만드는 경우에 제한적으로 사용한다.

호출 쪽의 *와 **는 값을 펼친다

정의의 *args·**kwargs와 호출의 *·**는 역할이 다르다. 호출에서는 iterable을 위치 argument로, mapping을 keyword argument로 unpack한다.

def scale(value, factor=1, *, digits=0):
    return round(value * factor, digits)

positional = (12.345, 2)
keywords = {"digits": 1}
print(scale(*positional, **keywords))
24.7

같은 매개변수에 위치 값과 키워드 값을 중복 전달하거나, 함수가 받지 않는 키워드를 넘기면 TypeError가 난다. 외부 dict를 그대로 **로 펼칠 때는 허용 key를 검증해야 오타나 예상 밖 옵션을 조기에 발견할 수 있다.

가변 기본 인자는 정의 시 한 번만 만들어진다

def collect_bad(value, bucket=[]):
    bucket.append(value)
    return bucket

def collect(value, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(value)
    return bucket

print(collect_bad(1))
print(collect_bad(2))
print(collect(1))
print(collect(2))
[1]
[1, 2]
[1]
[2]

기본값 표현식은 함수를 정의할 때 한 번 평가되므로 collect_bad의 list가 호출 사이에 공유된다. None을 '값이 제공되지 않음'의 sentinel로 쓰고 본문에서 새 list를 만든다. None 자체가 유효한 입력이라 구분해야 한다면 _MISSING = object()처럼 전용 sentinel을 만든다. 이 함정과 기본 인자 평가 시점은 Python 공식 튜토리얼의 Default Argument Values에 명시되어 있다 (2026-08-03 확인).

실전: 검증 가능한 요약 함수 만들기

method와 digits는 키워드 전용으로 두어 summarize(values, 'mean', 2)처럼 의미가 불분명한 호출을 막는다. method별 함수를 dict에 등록하면 긴 if 연쇄 없이 함수 객체를 골라 호출할 수 있다.

from statistics import mean, median

METHODS = {
    "mean": mean,
    "median": median,
    "min": min,
    "max": max,
}

def summarize(values, *, method="mean", digits=2):
    numbers = list(values)
    if not numbers:
        raise ValueError("values must not be empty")
    if method not in METHODS:
        allowed = ", ".join(METHODS)
        raise ValueError(f"unknown method: {method}; choose one of {allowed}")
    if type(digits) is not int or digits < 0:
        raise ValueError("digits must be a non-negative int")
    result = METHODS[method](numbers)
    return round(result, digits)

values = [10, 15, 20, 35]
print("mean:", summarize(values))
print("median:", summarize(values, method="median", digits=1))
print("max:", summarize(values, method="max", digits=0))

for bad_values, bad_method in [([], "mean"), ([1, 2], "mode")]:
    try:
        summarize(bad_values, method=bad_method)
    except ValueError as error:
        print(type(error).__name__ + ":", error)
mean: 20
median: 17.5
max: 35
ValueError: values must not be empty
ValueError: unknown method: mode; choose one of mean, median, min, max

numbers = list(values)는 generator 같은 일회성 iterable도 한 번 확정해 빈 입력 검사와 집계를 같은 데이터에 수행하려는 선택이다. 입력이 매우 크다면 전체 list 변환이 메모리를 사용하므로 스트리밍 가능한 통계와 별도 설계가 필요하다. generator의 소비 규칙은 이전 글 '제너레이터로 대용량 데이터를 처리하는 원리'를 참고한다.

함수는 변수·인자·반환값이 될 수 있다

Python 함수는 일급 객체다. 변수에 할당하고, 다른 함수에 전달하고, 함수에서 반환할 수 있다. 함수를 인자로 받거나 반환하는 함수를 고차 함수라고 부른다.

def apply(values, operation):
    return [operation(value) for value in values]

def make_threshold(minimum):
    def accepted(value):
        return value >= minimum
    return accepted

double = lambda value: value * 2
at_least_10 = make_threshold(10)

print(apply([2, 4, 6], double))
print([value for value in [8, 10, 12] if at_least_10(value)])
[4, 8, 12]
[10, 12]

make_threshold가 반환한 accepted는 바깥 함수 호출이 끝난 뒤에도 minimum을 기억한다. 이것이 closure의 핵심이다. 함수 객체를 전달하고 반환할 수 있다는 공식 설명은 Python 함수 정의 레퍼런스의 programmer's note에서 확인할 수 있다 (2026-08-03 확인).

lambda와 partial은 짧은 어댑터다

lambda는 단일 표현식으로 작은 익명 함수를 만든다. 정렬 key처럼 가까운 곳에서 한 번 쓰는 단순 변환에는 편하지만, 분기·검증·설명할 이름이 필요하면 def가 낫다. lambda도 일반 함수처럼 argument를 받고 함수 객체로 전달된다. Python 공식 튜토리얼의 Lambda Expressions는 lambda가 단일 표현식으로 제한됨을 설명한다 (2026-08-03 확인).

from functools import partial

values = [10, 15, 20, 35]
median_summary = partial(summarize, method="median", digits=1)
print(median_summary(values))
print(sorted([
    {"name": "A", "score": 80},
    {"name": "B", "score": 95},
], key=lambda row: row["score"], reverse=True))
17.5
[{'name': 'B', 'score': 95}, {'name': 'A', 'score': 80}]

partial은 일부 argument를 미리 고정한 새 callable을 만든다. median_summary는 원래 함수를 감추지 않고 특정 설정을 재사용하는 어댑터다. 자세한 호출 결합 규칙은 functools.partial 공식 문서에서 확인할 수 있다 (2026-08-03 확인). partial 객체에는 __name__과 __doc__이 자동 생성되지 않는다는 차이도 있으므로 공개 API나 디버깅 도구에서는 이름·문서화에 주의한다.

호출 계약 설계 체크리스트

☐ 함수는 한 가지 책임을 갖고 결과를 return하며 출력과 계산을 분리한다.

☐ 매개변수 다섯 종류와 /·* 경계를 읽고 의도에 맞게 호출한다.

☐ 의미 있는 옵션은 명시적 keyword-only로 두고 *args·**kwargs를 습관적으로 추가하지 않는다.

☐ list·dict·set 같은 가변 객체를 기본값으로 공유하지 않고 None 또는 전용 sentinel을 사용한다.

☐ 빈 입력과 지원하지 않는 옵션을 명확한 예외로 알리고 경계값을 테스트한다.

☐ lambda는 단순한 단일 표현식에만 쓰고 설명과 검증이 필요하면 이름 있는 def를 선택한다.

다음 글: 모듈과 표준 라이브러리 사용법

함수의 입력 계약을 정했다면 다음 단계는 여러 파일에서 재사용할 수 있게 코드를 나누는 것이다. 다음 글에서는 import가 이름을 가져오는 방식, 모듈·패키지 구조, 데이터 분석에 유용한 표준 라이브러리의 선택 기준을 다룬다.

참고 자료

댓글 0

댓글을 불러오는 중…