함수
프로그램이 커지다 보면 같은 로직을 여러 곳에 반복해서 작성하게 됩니다. 함수를 사용하면 로직을 한 번만 작성하고 이름을 붙여서 어디서든 사용할 수 있습니다. 한 곳만 고치면 그 함수를 호출하는 모든 곳에 수정 사항이 자동으로 반영됩니다.
def greet(name):
return f"Hello, {name}!"
print(greet("민지")) # "Hello, 민지!"
print(greet("준호")) # "Hello, 준호!"한 번만 작성하고, 어디서든 사용하고, 한 곳에서 고치세요.
함수 정의하기
def 키워드로 함수 정의를 시작하고, 그 뒤에 이름, 괄호, 콜론, 들여쓰기된 본문이 이어집니다. 함수는 호출하기 전까지는 아무 일도 하지 않습니다. def로 정의한 다음, 이름과 ()로 호출하세요.
def say_hello():
print("Hello!")
say_hello() # 함수를 호출합니다def name():를 쓰고 그 아래에 본문을 들여쓰기 하세요. name()으로 호출하기 전까지는 아무 일도 일어나지 않습니다, def는 그저 준비만 해두는 것뿐이니까요. 그리고 return을 빼먹으면 함수는 조용히 None을 돌려주는데, 저도 초반에 이것 때문에 여러 번 당했습니다. 매개변수와 인자
매개변수는 함수가 기대하는 입력값입니다. 괄호 안에 나열합니다. 함수를 호출할 때 전달하는 값들은 순서대로 매개변수와 매칭됩니다.
def greet(name, greeting):
print(f"{greeting}, {name}!")
greet("민지", "Hello") # "Hello, 민지!"
greet("준호", "Hi") # "Hi, 준호!"**매개변수(parameter)**는 함수 정의에 나오는 이름입니다. **인자(argument)**는 호출할 때 실제로 전달하는 값입니다. 실무에서는 이 두 용어를 섞어서 쓰는 경우가 많고, 이 구분이 중요해지는 건 주로 문서를 읽을 때입니다.
기본값
매개변수에는 기본값을 지정할 수 있습니다. 호출하는 쪽에서 해당 인자를 넘기지 않으면 기본값이 사용됩니다. 기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 합니다.
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("민지") # "Hello, 민지!"
greet("민지", "Hi") # "Hi, 민지!"기본값이 있는 매개변수는 기본값이 없는 매개변수 뒤에 와야 합니다.
키워드 인자
함수를 호출할 때 인자에 이름을 붙일 수 있습니다. 이렇게 하면 특히 매개변수가 많은 함수를 호출할 때 가독성이 좋아지고, 원하는 순서대로 전달할 수 있습니다.
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) # 혼합: 위치 인자가 먼저score=87) 그 호출 자체가 설명이 되고, 순서도 원하는 대로 정할 수 있습니다. 규칙은 딱 하나, 위치 인자는 이름 붙인 인자보다 먼저 와야 한다는 것뿐입니다. 매개변수가 많은 함수에서 특히 유용합니다. 반환값
return은 호출한 쪽으로 값을 돌려줍니다. return이 없으면 함수는 None을 돌려줍니다. return이 실행되는 순간 함수는 즉시 종료됩니다. 그 블록 안에서 그 뒤에 있는 코드는 실행되지 않습니다.
def add(a, b):
return a + b
result = add(3, 4) # result = 7
print(result)return은 함수를 즉시 종료시킵니다. 그 블록 안에서 그 뒤에 있는 코드는 실행되지 않습니다.
return은 함수를 호출한 쪽에 값을 건네주고, 그 자리에서 함수가 멈추면서 그 뒤의 코드는 건너뜁니다. return을 빼면 None이 돌아옵니다. print는 값을 화면에 보여주는 것이고 return은 그 값을 다시 쓸 수 있게 돌려주는 것인데, 저도 이 차이를 체감하기까지 시간이 좀 걸렸습니다. 여러 값 반환하기
파이썬에서는 값을 쉼표로 구분해서 여러 개를 한꺼번에 반환할 수 있습니다. 호출한 쪽은 이를 튜플로 받아서 한 줄에서 여러 개의 이름으로 풀어낼 수 있습니다.
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([3, 7, 1, 9, 4])
print(low, high) # 1 9low, high = ... 구문은 **언패킹(unpacking)**입니다. 파이썬은 반환된 각 값을 대응하는 이름에 할당합니다.
low, high = min_max(...)처럼 한 줄에서 값마다 이름 하나씩 받아낼 수 있습니다. 함수가 자연스럽게 두 개 이상의 답을 만들어낼 때 유용합니다. 스코프
함수 안에서 만든 변수는 그 함수 안에서만 존재합니다. 밖에서는 볼 수 없습니다. 모든 함수 밖에서 정의한 변수는 어디서나 보이지만, 명시적으로 선언하지 않으면 함수 안에서 그 값을 바꿀 수는 없습니다.
global 선언이 필요합니다. 이건 거의 쓸 일이 없습니다. 값을 넘기고 결과를 반환받으면 함수가 따라가기 쉬운 상태로 남습니다. def calculate():
result = 42 # 이 함수 안에서만 유효한 로컬 변수
return result
calculate()
print(result) # NameError, 여기서는 result가 존재하지 않습니다count = 0
def increment():
global count # 전역 변수를 수정하겠다고 선언합니다
count += 1
increment()
print(count) # 1global은 최후의 수단으로만 써야 합니다. 코드를 이해하기 어렵게 만듭니다. 값을 넘기고 다시 반환받는 방식을 선호하세요. 스코프는 변수와 타입 챕터에서 다룬, 할당이 이름을 값에 묶는 방식을 그대로 기반으로 합니다.
*args와 **kwargs
함수가 몇 개의 인자를 받을지 미리 알 수 없는 경우가 있습니다. *args는 임의 개수의 위치 인자를 튜플로 모아줍니다. **kwargs는 임의 개수의 키워드 인자를 딕셔너리로 모아줍니다. args와 kwargs라는 이름 자체는 관례일 뿐이고, 중요한 건 *와 **입니다.
def total(*args):
return sum(args)
total(1, 2, 3) # 6
total(1, 2, 3, 4, 5) # 15def display(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
display(name="민지", score=87, level=5)일반 매개변수와 함께 섞어서 쓸 수도 있습니다. 일반 매개변수가 먼저 옵니다:
def describe(title, *tags, **metadata):
print(f"{title} | tags: {tags} | meta: {metadata}")
describe("Python intro", "beginner", "python", author="민지", year=2024)*args는 얼마든지 많은 추가 위치 인자를 튜플로 모아주고, **kwargs는 이름 붙은 추가 인자를 딕셔너리로 모아줍니다. args와 kwargs라는 단어 자체는 관례일 뿐이고, 실제로 일하는 건 *와 **입니다. 몇 개가 들어올지 미리 말할 수 없을 때 유용합니다. 독스트링
독스트링(docstring)은 함수가 무엇을 하는지 설명하는, 함수 맨 위에 쓰는 문자열입니다. 파이썬 에디터와 도구들은 함수 호출 위에 마우스를 올렸을 때 이 내용을 도움말로 보여줍니다. 삼중 인용부호를 쓰고, 단순한 함수라면 한 줄로 작성하세요.
def normalise(value, min_val, max_val):
"""알려진 최소값과 최대값을 기준으로 값을 0-1 범위로 스케일링합니다."""
return (value - min_val) / (max_val - min_val)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}"이름과 시그니처만으로 목적이 분명하지 않은 함수에는 독스트링을 작성하세요.
타입 힌트
타입 힌트를 쓰면 함수가 어떤 타입을 받고 반환하는지 표시할 수 있습니다. 파이썬은 실행 시점에 이를 강제하지 않지만, 에디터는 실행하기 전에 이를 이용해 실수를 잡아냅니다. 콜론 앞의 ->는 반환 타입을 지정합니다.
def greet(name: str, score: int) -> str:
return f"{name} scored {score}"def log(message: str) -> None:
print(f"[LOG] {message}")def top_scores(scores: list[int], n: int) -> list[int]:
return sorted(scores, reverse=True)[:n]타입 힌트는 필수는 아니지만, 여러 곳에서 호출되는 함수라면 가치가 있습니다. 도구가 검증할 수 있는 문서인 셈입니다.
name: str이나 반환값에 대한 -> str처럼요. 파이썬은 실행할 때 이를 강제하지 않지만, 에디터는 이를 읽고 실행하기 전에 경고해줍니다. 도구가 검사할 수 있는 메모라고 생각하면 됩니다. 값으로서의 함수
파이썬에서 함수는 문자열이나 숫자처럼 하나의 값입니다. 변수에 할당할 수도 있고 다른 함수에 넘길 수도 있습니다. sorted()가 key= 함수를 받을 수 있는 것도 이런 이유입니다.
def double(x):
return x * 2
def apply(func, value):
return func(value)
apply(double, 5) # 10함수를 인자로 넘기는 방식은 sorted(), map(), filter()에서 끊임없이 등장합니다. 람다와 컴프리헨션 챕터에서도 이를 다시 만나게 됩니다.
sorted()에 key=로 함수를 넘길 수 있는 것이죠. 처음에는 낯설었는데, 한번 이해하고 나니 여기저기서 계속 보이더군요. 실전 예제
함께 동작하는 두 함수입니다. letter_grade는 점수를 등급 문자로 바꾸고, summarise는 리스트에 있는 모든 점수에 대해 이를 호출합니다:
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 = []
for s in scores:
grades.append(letter_grade(s))
print(f"Average: {avg:.1f}")
print(f"Grades: {', '.join(grades)}")
summarise([87, 92, 74, 65, 91])
