Skip to content
This page has been auto-translated and may contain errors.View in English

함수

프로그램이 커지면 같은 로직을 여러 곳에서 작성하게 됩니다. 함수는 로직을 한 번만 작성하고, 이름을 붙이고, 어디서든 사용할 수 있게 해줍니다. 한 곳에서 수정하면 모든 호출에 자동으로 수정이 반영됩니다.

함수는 코드 재사용과 추상화의 주된 단위입니다. 동작을 캡슐화하고, 이름을 부여하며, 명확한 인터페이스(매개변수와 반환값)를 정의하고, 어디서든 호출할 수 있게 만듭니다. 잘 명명된 함수는 문서 역할도 합니다. validate_email()은 코드를 읽지 않아도 무엇을 하는지 알려줍니다.

Python에서 def는 함수 객체를 생성하고 현재 스코프의 이름에 바인딩합니다. 함수는 일급 객체입니다. 변수에 할당할 수 있고, 컬렉션에 저장할 수 있으며, 인수로 전달하고, 다른 함수에서 반환할 수 있습니다. 클로저는 둘러싸는 스코프의 자유 변수를 캡처합니다. 이를 이해하면 고차 프로그래밍이 자연스러워집니다.

python
def greet(name):
    return f"Hello, {name}!"

print(greet("민준"))   # "Hello, 민준!"
print(greet("서연"))   # "Hello, 서연!"

한 번 작성하고, 어디서든 사용하고, 한 곳에서 수정합니다.

함수 정의하기

def 키워드로 함수 정의를 시작하며, 그 뒤에 이름, 괄호, 콜론, 그리고 들여쓰기된 본문이 옵니다. 함수는 호출하기 전까지 아무것도 하지 않습니다. def로 정의한 다음 이름과 ()로 호출합니다.

def는 함수 객체를 만들어 현재 스코프의 주어진 이름에 바인딩하는 문장입니다. 본문은 정의 시점에 실행되지 않으며, 함수가 호출될 때만 실행됩니다. return 문이 없는 함수는 암묵적으로 None을 반환합니다.

def는 함수 본문을 코드 객체로 컴파일하고 새 함수 객체를 현재 네임스페이스의 이름에 바인딩하는 복합 문장입니다. 함수 객체는 자신의 코드 객체에 대한 참조, 기본 인수값, 그리고 둘러싸는 스코프에 대한 참조(클로저용)를 저장합니다. 본문은 호출 시에만 실행됩니다.

python
def say_hello():
    print("Hello!")

say_hello()   # 함수 호출

매개변수와 인수

매개변수는 함수가 기대하는 입력입니다. 괄호 안에 나열합니다. 함수를 호출할 때 전달하는 값들이 순서대로 매개변수와 매칭됩니다.

매개변수는 함수의 인터페이스를 정의합니다. 인수는 호출 시점에 전달되는 구체적인 값입니다. 위치 인수는 위치에 의해 매칭되고, 키워드 인수는 이름에 의해 매칭됩니다. 기본값은 매개변수를 선택적으로 만듭니다.

Python에는 네 가지 종류의 매개변수가 있습니다: 위치-또는-키워드(기본값), 키워드 전용(* 뒤), 위치 전용(/ 앞, Python 3.8+), 가변(*args, **kwargs). 호출 시 위치 인수는 왼쪽에서 오른쪽으로 바인딩되고, 키워드 인수는 이름으로 바인딩됩니다. 충돌은 TypeError를 발생시킵니다. 기본값은 함수 정의 시점에 한 번만 평가되며, 매 호출마다 평가되지 않습니다.

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("민준", "Hello")    # "Hello, 민준!"
greet("서연", "Hi")       # "Hi, 서연!"

매개변수 vs 인수

**매개변수(Parameter)**는 함수 정의에서의 이름입니다. **인수(Argument)**는 함수를 호출할 때 실제로 전달하는 값입니다. 실제로는 두 단어가 혼용됩니다. 문서를 읽을 때 차이를 알아두면 됩니다.

기본값

매개변수에 기본값을 줄 수 있습니다. 호출자가 해당 인수를 제공하지 않으면 기본값이 사용됩니다. 기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 합니다.

기본값은 매개변수를 선택적으로 만듭니다. 정의 시점에 한 번 평가되며, 매 호출마다 평가되지 않습니다. 이는 가변 기본값에서 중요합니다: def f(items=[])는 모든 호출에서 같은 리스트를 공유합니다. 해결책은 기본값으로 None을 사용하고 함수 본문 안에서 리스트를 생성하는 것입니다.

기본값은 함수 객체에 f.__defaults__(위치) 및 f.__kwdefaults__(키워드 전용)로 저장됩니다. def가 실행될 때 한 번 평가되며, 호출별로 평가되지 않습니다. 가변 기본값 함정(def f(x=[]))은 고전적인 Python 함정입니다. 리스트가 한 번 생성되고 호출 간에 제자리에서 변경됩니다. 관용적인 해결책: def f(x=None): if x is None: x = [].

python
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("민준")           # "Hello, 민준!"
greet("민준", "Hi")     # "Hi, 민준!"

기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 합니다.

키워드 인수

함수를 호출할 때 인수에 이름을 붙일 수 있습니다. 이는 매개변수가 많은 함수에서 호출을 읽기 쉽게 만들고, 어떤 순서로든 전달할 수 있게 해줍니다.

키워드 인수는 함수 호출을 자기 문서화합니다. 위치와 키워드를 섞을 수 있습니다: 위치 인수가 먼저 와야 합니다. 불리언 플래그가 있거나 비슷한 타입의 매개변수가 많은 함수의 경우, 키워드 인수는 잘못된 순서로 인수를 전달하는 조용한 실수를 막아줍니다.

키워드 인수는 위치가 아닌 이름으로 바인딩됩니다. 호출 시점에 위치 인수가 키워드 인수보다 먼저 와야 합니다. 같은 인수를 위치로도 이름으로도 전달하면 TypeError가 발생합니다. 키워드 전용 인수를 강제하려면 매개변수 목록의 단독 * 뒤에 배치하세요: def f(a, *, b)b를 키워드 전용으로 만듭니다.

python
def describe_player(name, score, level):
    print(f"{name} | Score: {score} | Level: {level}")

describe_player("민준", 87, 5)                        # 위치
describe_player(name="민준", level=5, score=87)       # 키워드, 순서 무관
describe_player("민준", level=5, score=87)            # 혼합: 위치 먼저

반환값

return은 호출자에게 값을 돌려보냅니다. return이 없으면 함수는 None을 반환합니다. return이 실행되면 함수는 즉시 종료됩니다. 해당 블록에서 그 뒤의 코드는 건너뜁니다.

return은 함수를 종료하고 호출자에게 값을 전달합니다. 명시적 return이 없는 함수는 암묵적으로 None을 반환합니다. return은 함수 본문의 어디든 나타날 수 있고 여러 번 사용될 수 있습니다. 도달한 첫 번째 return이 함수를 종료합니다. 이는 가드 절을 위한 조기 반환에 유용합니다.

return은 스택의 맨 위를 팝하여 호출자의 프레임으로 전달하는 RETURN_VALUE 바이트코드를 내보냅니다. 끝까지 도달하는 함수는 암묵적으로 None을 반환합니다. 여러 return 문은 괜찮으며 종종 복잡한 조건이 있는 단일 반환보다 더 깔끔합니다("조기 반환" 패턴). try 블록 내의 return은 여전히 관련된 finally 절을 실행합니다.

python
def add(a, b):
    return a + b

result = add(3, 4)   # result = 7
print(result)

return은 또한 함수를 즉시 종료합니다. 해당 블록에서 그 뒤의 코드는 실행되지 않습니다.

여러 값 반환하기

Python에서는 쉼표로 구분하여 여러 값을 반환할 수 있습니다. 호출자는 이를 튜플로 받으며, 한 줄에서 별도의 이름으로 언패킹할 수 있습니다.

쉼표로 여러 값을 반환하면 튜플로 패킹됩니다. 호출자는 일치하는 이름으로 언패킹합니다. 이는 자연스럽게 둘 이상의 결과를 생성하는 함수에 대한 Python의 관용적 표현입니다. 특별한 기능이 아니라 튜플 패킹과 언패킹입니다.

return a, b는 암묵적 패킹을 통해 값을 튜플로 패킹합니다. 호출자는 x, y = f()로 언패킹하며, 이는 반환된 튜플의 __iter__를 호출합니다. 타입 힌트의 명확성을 위해 반환 타입을 tuple[int, str]로 주석하거나 명명된 튜플을 사용하세요. 적은 개수에는 일반 튜플 반환이 괜찮지만, 두세 개 이상은 명명된 튜플이나 데이터클래스를 고려하세요.

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

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)   # 1 9

low, high = ... 구문은 언패킹입니다: Python이 반환된 각 값을 해당 이름에 할당합니다.

스코프

함수 안에서 생성된 변수는 해당 함수 안에서만 존재합니다. 밖에서는 볼 수 없습니다. 모든 함수 밖에서 정의된 변수는 어디서든 볼 수 있지만, 명시적 선언 없이는 함수 안에서 변경할 수 없습니다.

Python에는 지역 스코프(함수 내부), 둘러싸는 스코프(중첩 함수의 경우 외부 함수 내), 전역 스코프(모듈 수준), 내장 스코프가 있습니다. 이름 조회는 그 순서대로 LEGB 규칙을 따릅니다. 지역 변수는 같은 이름의 외부 변수를 가립니다. global은 이름이 모듈 수준 바인딩을 가리키도록 선언합니다.

Python의 LEGB 이름 해석: Local, Enclosing(클로저), Global(모듈), Built-in. 각 def는 새로운 지역 네임스페이스를 생성합니다. 전역 변수를 읽는 것은 자동으로 작동합니다. 작성하려면 지역 그림자를 만들지 않도록 global x가 필요합니다. nonlocal x는 가장 가까운 둘러싸는(비전역) 스코프에 접근하여 클로저가 캡처된 변수를 변경할 수 있게 합니다. global을 과용하면 코드를 추론하기 어려워집니다. 매개변수와 반환값을 선호하세요.

python
def calculate():
    result = 42   # 이 함수에 지역적임
    return result

calculate()
print(result)   # NameError, result는 여기 밖에 존재하지 않음
python
count = 0

def increment():
    global count    # 전역을 수정하길 원한다고 선언
    count += 1

increment()
print(count)   # 1

global 사용은 최후의 수단이어야 합니다. 코드를 추론하기 어렵게 만듭니다. 값을 전달하고 반환하는 것을 선호하세요.

*args와 **kwargs

때때로 함수가 몇 개의 인수를 받을지 알 수 없습니다. *args는 임의의 수의 위치 인수를 튜플로 수집합니다. **kwargs는 임의의 수의 키워드 인수를 딕셔너리로 수집합니다. argskwargs라는 이름은 관례이며, 중요한 것은 별표입니다.

*args는 초과 위치 인수를 튜플로 수집합니다. **kwargs는 초과 키워드 인수를 딕셔너리로 수집합니다. 둘 다 일반 매개변수와 결합할 수 있습니다. 일반 매개변수가 먼저 오고, 그 다음 *args, 그 다음 키워드 전용 매개변수, 그 다음 **kwargs입니다. 다른 함수로 인수를 전달하는 래퍼 함수에 유용합니다.

*args는 나머지 위치 인수로부터 tuple을 생성합니다. **kwargs는 나머지 키워드 인수로부터 dict를 생성합니다. 매개변수 순서: 위치-또는-키워드, *args, 키워드 전용, **kwargs. 호출 시점에서 *iterable은 위치 인수를 언패킹하고 **mapping은 키워드 인수를 언패킹합니다. 이들은 대칭적입니다: 시그니처의 *는 수집하고, 호출 시점의 *는 언패킹합니다.

python
def total(*args):
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)   # 15
python
def display(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display(name="민준", score=87, level=5)

일반 매개변수와 섞을 수 있습니다. 일반 매개변수가 먼저 옵니다:

python
def describe(title, *tags, **metadata):
    print(f"{title} | tags: {tags} | meta: {metadata}")

describe("Python intro", "beginner", "python", author="민준", year=2024)

도크스트링

도크스트링은 함수 맨 위에 있는 문자열로, 함수가 무엇을 하는지 설명합니다. Python 편집기와 도구는 함수 호출에 마우스를 올렸을 때 도움말을 보여주기 위해 이를 사용합니다. 삼중 따옴표를 사용하고, 간단한 함수에는 한 줄을 작성하세요.

도크스트링은 f.__doc__로 저장되며 help()에 의해 표시됩니다. 관례는 한 줄의 요약, 선택적으로 빈 줄과 추가 세부사항이 뒤따릅니다. 공개 함수의 경우, 도크스트링은 도구가 표면화할 수 있는 문서입니다. 여러 곳에서 호출되는 어떤 것에도 선택사항이 아닙니다.

도크스트링은 함수, 클래스, 또는 모듈 본문의 첫 번째 문장으로 배치된 문자열 리터럴입니다. 객체의 __doc__로 저장됩니다. PEP 257은 관례를 정의합니다. Sphinx, pydoc, IDE 같은 도구들은 모두 __doc__에 의존합니다. 도크스트링의 타입 정보를 위해 Google, NumPy, 또는 Sphinx 스타일을 사용하세요. 현대 코드에서는 도크스트링의 타입 주석보다 시그니처의 타입 힌트가 선호됩니다.

python
def normalise(value, min_val, max_val):
    """알려진 min과 max를 기준으로 값을 0-1 범위로 스케일링합니다."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    API 엔드포인트 URL을 빌드합니다.

    완전히 자격이 부여된 URL 문자열을 반환합니다. secure가 False이면
    URL은 https 대신 http를 사용합니다.
    """
    scheme = "https" if secure else "http"
    base   = base.replace("https://", "").replace("http://", "")
    return f"{scheme}://{base}/{version}/{resource}"

이름과 시그니처만으로 자명하지 않은 모든 함수에 대해 도크스트링을 작성하세요.

타입 힌트

타입 힌트는 함수가 어떤 타입을 기대하고 반환하는지 주석을 달 수 있게 해줍니다. Python은 런타임에 강제하지 않지만, 편집기는 이를 사용하여 무언가를 실행하기 전에 실수를 잡아냅니다. 콜론 앞의 ->는 반환 타입을 지정합니다.

타입 힌트는 도구가 검증하는 문서입니다. 편집기와 타입 체커(mypy, pyright)는 이를 사용하여 런타임 전에 타입 불일치를 잡아냅니다. 표준 Python에서는 런타임 효과가 없습니다. -> None은 반환값이 없는 함수에 대한 올바른 주석입니다. 제네릭 컨테이너의 경우 list[int], dict[str, int]를 사용하세요(Python 3.9+).

타입 힌트는 런타임에 typing.get_type_hints()에 의해 처리되지만 실행에 직접적인 영향을 미치지 않습니다. 정적 타입 체커는 런타임 전에 이를 분석합니다. PEP 484가 주석 시스템을 도입했으며, PEP 585는 Python 3.9+에서 typing 임포트 없이 list[int] 같은 내장 제네릭을 허용했습니다. 함수 시그니처의 -> None은 "아무것도 반환하지 않음"과 "표현식 컨텍스트에서 사용되어서는 안 됨"을 모두 신호합니다. 복잡한 타입의 경우 typing.Protocol, typing.TypeVar, typing.overload가 완전한 정적 타입 지정 능력을 제공합니다.

python
def greet(name: str, score: int) -> str:
    return f"{name} scored {score}"
python
def log(message: str) -> None:
    print(f"[LOG] {message}")
python
def top_scores(scores: list[int], n: int) -> list[int]:
    return sorted(scores, reverse=True)[:n]

타입 힌트는 선택사항이지만 여러 곳에서 호출될 함수에서 가치가 있습니다. 도구가 검증할 수 있는 문서입니다.

값으로서의 함수

Python의 함수는 문자열이나 숫자와 마찬가지로 값입니다. 변수에 할당하고 다른 함수에 전달할 수 있습니다. 이것이 sorted()key= 함수를 받아들이는 방식입니다.

함수는 일급 객체입니다: 타입(function)을 가지며, 변수와 컬렉션에 저장될 수 있고, 인수로 전달되거나 값으로 반환될 수 있습니다. 이것이 sorted(key=...), map(), filter() 같은 고차 함수의 기초입니다.

함수는 function 타입의 객체로 다음 속성을 가집니다: __name__, __doc__, __annotations__, __defaults__, __code__, __closure__. 복사되지 않고 참조로 전달됩니다. 함수에서 함수를 반환하면 내부 함수가 외부 스코프의 변수를 참조할 경우 클로저가 생성됩니다: 그 변수들은 f.__closure__에 저장됩니다.

python
def double(x):
    return x * 2

def apply(func, value):
    return func(value)

apply(double, 5)   # 10

함수를 인수로 전달하는 것은 sorted(), map(), filter()와 함께 끊임없이 등장합니다. Lambda, 컴프리헨션, zip 챕터에서도 보게 될 것입니다.

실전에서

함께 작동하는 두 함수: letter_grade는 점수를 문자로 변환하고, summarise는 리스트의 각 점수에 대해 이를 호출합니다:

python
def letter_grade(score: int) -> str:
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"

def summarise(scores: list[int]) -> None:
    total  = sum(scores)
    avg    = total / len(scores)
    grades = [letter_grade(s) for s in scores]
    print(f"Average: {avg:.1f}")
    print(f"Grades: {', '.join(grades)}")

summarise([87, 92, 74, 65, 91])

로그 포매터와 이를 사용하는 파일 처리기, 명시적으로 비활성화되지 않는 한 부수 효과를 방지하는 dry_run 기본 매개변수를 포함합니다:

python
def format_log(level: str, message: str) -> str:
    return f"[{level.upper():5}] {message}"

def process_file(path: str, dry_run: bool = True) -> bool:
    print(format_log("info", f"Processing {path}"))
    if dry_run:
        print(format_log("info", "Dry run, no changes made"))
        return True
    return True

process_file("report.csv")
process_file("report.csv", dry_run=False)

단일 값 노멀라이저와 그 위에 빌드된 컬럼 노멀라이저, 타입 힌트와 도크스트링을 포함합니다. 컬럼 함수는 범위를 한 번 계산하고 각 항목에 스칼라 함수를 재사용합니다:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """알려진 min과 max를 기준으로 값을 0-1 범위로 스케일링합니다."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """값의 전체 컬럼을 정규화합니다."""
    lo, hi = min(values), max(values)
    return [normalise(v, lo, hi) for v in values]

raw = [10.0, 25.0, 5.0, 40.0, 15.0]
print(normalise_column(raw))

여기서 타입 힌트는 두 가지 목적을 수행합니다: 함수가 무엇을 기대하는지 문서화하고, 실수로 문자열 리스트를 전달하는 호출자를 타입 체커가 잡을 수 있게 합니다.