[6일차-2] Python

송정근·2026년 5월 27일

11. 사용자 정의 함수

함수는 특정 작업을 하나의 이름으로 묶어 필요할 때 호출할 수 있게 만든 코드 단위다. 파이썬은 print(), len(), range() 같은 내장 함수를 제공하지만, 프로그램의 요구사항에 맞는 동작은 직접 함수로 정의해야 한다. 이렇게 사용자가 직접 작성한 함수를 사용자 정의 함수라고 한다.

함수를 사용하면 반복되는 코드를 줄이고, 프로그램을 역할별로 분리하며, 한 번 작성한 로직을 여러 곳에서 재사용할 수 있다.

11.1 함수 정의와 호출

함수는 def 키워드로 정의한다.

def 함수명(매개변수1, 매개변수2):
    실행할 코드
    return 반환값
  • 함수명은 함수 객체를 가리키는 이름이다.
  • 매개변수는 함수가 실행될 때 외부에서 전달받을 값을 위한 이름이다.
  • return은 실행 결과를 호출한 위치로 돌려준다.
def hello():
    print("안녕하세요")

hello()

def hello(): ...가 실행되면 파이썬은 함수 객체를 생성하고 hello라는 이름을 그 객체에 연결한다. 따라서 함수는 문자열이나 리스트처럼 변수에 저장하거나 다른 함수에 전달할 수 있는 객체다.

11.2 함수 객체와 함수 호출 결과

함수 이름 뒤에 괄호를 붙이는지에 따라 의미가 달라진다.

def hello():
    print("안녕하세요")
    return "실행 완료"

a = hello
b = hello()

a = hello는 함수를 실행하지 않고 함수 객체 자체를 a에도 연결한다.

a = hello

print(a)
a()

개념적으로는 다음과 같은 상태다.

hello ----┐
          ├----> 함수 객체
a --------┘

반면 b = hello()는 함수를 즉시 실행하고, return으로 반환된 값인 "실행 완료"를 b에 저장한다.

b = hello()
print(b)
코드의미저장되는 값
a = hello함수 객체를 참조한다.함수 객체
a()a가 참조하는 함수를 실행한다.반환값을 사용할 수 있다.
b = hello()함수를 즉시 실행한다.함수의 반환값

함수 객체는 실행할 코드, 기본 매개변수 값, 전역 네임스페이스와의 연결, 클로저 정보 등을 가진다. 필요한 경우 다음 속성으로 확인할 수 있다.

def hello():
    print("안녕하세요")

print(hello)          # 함수 객체를 확인한다.			 
print(hello.__code__) # 함수의 코드 객체를 확인한다. 

# 실행 결과
# <function hello at 0x108e6b420>
# <code object hello at 0x108e80ff0, file "/var/folders/cr/h0m56hfx095g2g1b7pwy0hgh0000gn/T/ipykernel_28955/2992541879.py", line 1>

11.3 함수 호출과 호출 프레임

함수를 호출하면 해당 호출을 실행하기 위한 독립적인 공간이 만들어진다. 이 공간을 호출 프레임(call frame)이라고 하며, 매개변수와 지역 변수, 현재 실행 위치 등의 정보를 관리한다.

def add(a, b):
    result = a + b
    return result

x = add(3, 5)
print(x)

add(3, 5)가 실행되는 흐름은 다음과 같다.

  1. add 이름이 가리키는 함수 객체를 찾는다.
  2. 함수 호출을 위한 새로운 프레임을 만든다.
  3. 매개변수 a에 3, b에 5를 연결한다.
  4. 함수 본문을 실행해 지역 변수 result에 8을 연결한다.
  5. return으로 결과를 호출 위치에 반환한다.
  6. 더 이상 필요한 참조가 없다면 해당 호출 프레임은 정리된다.

같은 함수를 여러 번 호출해도 호출마다 별도의 프레임이 만들어지므로 지역 변수는 서로 충돌하지 않는다.

def add_range(start, end):
    total = 0

    for number in range(start, end + 1):
        total += number

    return total

print(add_range(1, 10))
print(add_range(1, 100))

두 번의 호출에서 사용하는 total은 각각의 호출 프레임에 속한 별개의 지역 변수다.

11.4 매개변수와 반환값이 없는 함수

매개변수와 반환값이 없는 함수는 전달받을 데이터 없이 정해진 동작만 수행한다.

def print_message():
    print("처음으로 만드는 함수다.")

print_message()
print_message()

for _ in range(3):
    print_message()

return을 작성하지 않은 함수도 실행이 끝나면 자동으로 None을 반환한다.

def print_message():
    print("메시지를 출력한다.")

result = print_message()
print(result)  # None

11.5 매개변수가 있는 함수

매개변수는 함수가 작업에 사용할 값을 전달받는 통로다.

def print_number(number):
    print(f"입력받은 숫자: {number}")

print_number(10)
print_number(4)

여러 개의 매개변수를 전달받을 수도 있다.

def print_sum(start, end):
    total = 0

    for number in range(start, end + 1):
        total += number

    print(f"{start}부터 {end}까지의 합: {total}")

print_sum(1, 10)
print_sum(1, 100)

함수를 호출할 때는 필요한 인자의 개수에 맞게 값을 전달해야 한다.

def add(num1, num2):
    return num1 + num2

print(add(10, 5))
# print(add(10))  # TypeError가 발생한다.

11.6 반환값이 있는 함수

return은 함수가 처리한 결과를 호출한 위치로 전달한다. 반환된 값은 출력하거나 다른 변수에 저장해 이후 연산에 사용할 수 있다.

def add(num1, num2):
    total = num1 + num2
    return total

print(add(10, 5))

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

함수를 호출한 결과와 함수 자체는 서로 다르다.

def get_gift():
    return "선물"

function_object = get_gift
returned_value = get_gift()

print(function_object)  # 함수 객체가 출력된다.
print(returned_value)   # 선물이 출력된다.

11.7 중첩 함수 호출과 스택 구조

함수 안에서 다른 함수를 호출할 수 있다. 이때 새 함수 호출 프레임이 기존 프레임 위에 추가되고, 가장 마지막에 호출된 함수부터 실행을 마친다. 이러한 순서를 LIFO(Last In, First Out)라고 한다.

def func_a():
    print("A 시작")
    result = func_b()
    print("A 끝")
    return result

def func_b():
    print("B 시작")
    result = func_c()
    print("B 끝")
    return result

def func_c():
    print("C 실행")
    return 100

value = func_a()
print("최종 결과:", value)

출력 순서는 다음과 같다.

A 시작
B 시작
C 실행
B 끝
A 끝
최종 결과: 100

호출 순서는 func_a()에서 func_b(), 다시 func_c()로 들어가지만, 종료 순서는 func_c(), func_b(), func_a() 순서가 된다.

11.8 기본값이 있는 매개변수

매개변수에 기본값을 지정하면 호출 시 해당 인자를 생략할 수 있다.

def add(num1=0, num2=0):
    return num1 + num2

print(add())            # 0
print(add(10))          # 10
print(add(10, 3))       # 13
print(add(num2=3))      # 3

기본값이 있는 매개변수를 사용할 때는 변경 가능한 객체를 기본값으로 직접 지정하는 경우를 주의해야 한다. 기본값은 함수가 호출될 때마다 생성되는 것이 아니라, 함수가 정의될 때 한 번 만들어져 재사용된다.

def add_item(item, items=[]): # items=[] 라는 리스트가 선언되면 해시 메모리에 저장되기 때문에 생기는 오류
    items.append(item)
    return items

print(add_item("사과"))   # ['사과']
print(add_item("바나나")) # ['사과', '바나나']
print(add_item("오렌지")) # ['사과', '바나나', '오렌지']

각 호출에서 새 리스트를 원했다면 의도하지 않은 결과다. 이러한 경우에는 기본값으로 None을 사용하고, 함수 내부에서 새 리스트를 생성한다.

def add_item(item, items=None):
    if items is None:
        items = []

    items.append(item)
    return items

print(add_item("사과"))   # ['사과']
print(add_item("바나나")) # ['바나나']
print(add_item("오렌지")) # ['오렌지']

11.9 None

None은 값이 없음을 나타내는 파이썬의 특별한 객체다. 함수에서 return을 명시하지 않았을 때 반환되는 값이며, 아직 값이 정해지지 않은 상태를 나타낼 때도 자주 사용한다.

result = None

if result is None:
    print("아직 결과가 없다.")

None인지 확인할 때는 ==보다 is를 사용하는 것이 권장된다.

value = None

print(value is None)  # True

None은 조건식에서 False로 평가되지만 0, 빈 문자열 "", 빈 리스트 []와는 의미가 다르다.

11.10 가변 위치 인자: *args

함수 정의에서 *args를 사용하면 전달된 위치 인자를 개수와 관계없이 튜플로 받을 수 있다.

def collect_numbers(*args):
    return args

print(collect_numbers())            # ()
print(collect_numbers(10))          # (10,)
print(collect_numbers(10, 30, 50))  # (10, 30, 50)

함수를 호출하는 위치에서 *를 사용하면 리스트나 튜플의 요소를 각각의 위치 인자로 펼쳐 전달할 수 있다.

def add_three(a, b, c):
    return a + b + c

numbers = [1, 2, 3]

print(add_three(*numbers))  # add_three(1, 2, 3)과 같다.

함수 정의에서 사용하는 *args는 여러 인자를 모으는 역할을 하고, 함수 호출에서 사용하는 *numbers는 컬렉션의 요소를 펼치는 역할을 한다.

11.11 키워드 인자와 **kwargs

함수 호출 시 매개변수명=값 형태로 전달하면 순서에 관계없이 원하는 매개변수에 값을 연결할 수 있다.

def print_user(user_id, name, age):
    print(f"아이디: {user_id}")
    print(f"이름: {name}")
    print(f"나이: {age}")

print_user("apple", "김사과", 20)
print_user(age=30, user_id="orange", name="오렌지")

# 실행 결과
# 아이디: apple
# 이름: 김사과
# 나이: 20
# 아이디: orange
# 이름: 오렌지
# 나이: 30

딕셔너리의 키가 매개변수 이름과 같다면 **를 사용해 키워드 인자로 펼칠 수 있다.

user = {
    "age": 25,
    "user_id": "banana",
    "name": "반하나"
}

# 딕셔너리의 키는 반드시 문자열 형태
print_user(**user)

# 실행 결과
# 아이디: banana
# 이름: 반하나
# 나이: 25

이때 딕셔너리 키는 문자열이어야 하며 함수의 매개변수 이름과 일치해야 한다.

함수를 정의할 때 **kwargs를 사용하면 정해지지 않은 개수의 키워드 인자를 딕셔너리로 받을 수 있다.

def print_profile(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_profile(name="김사과", age=20, city="서울")

# 실행 결과
# name: 김사과
# age: 20
# city: 서울

*와 **의 역할은 다음과 같이 정리할 수 있다.

사용 위치문법역할
함수 정의*args여러 위치 인자를 튜플로 모은다.
함수 호출*numbers시퀀스 요소를 위치 인자로 펼친다.
함수 정의**kwargs여러 키워드 인자를 딕셔너리로 모은다.
함수 호출**user딕셔너리를 키워드 인자로 펼친다.

11.12 여러 개의 반환값

파이썬 함수는 여러 값을 쉼표로 구분해 반환할 수 있다. 실제로는 여러 값이 하나의 튜플로 묶여 반환된다.

def calculate(num1=0, num2=1):
    return num1 + num2, num1 - num2, num1 * num2, num1 / num2

result = calculate(10, 3)
print(result)
# (13, 7, 30, 3.3333333333333335)

튜플 언패킹을 사용하면 각 결과를 별도의 변수에 저장할 수 있다.

addition, subtraction, multiplication, division = calculate(10, 3)

print(f"덧셈: {addition}")
print(f"뺄셈: {subtraction}")
print(f"곱셈: {multiplication}")
print(f"나눗셈: {division}")

사용하지 않을 값은 관례적으로 _에 받는다.

_, _, multiplication, _ = calculate(10, 3)

print(f"곱셈: {multiplication}")

이 방식은 enumerate()에서 인덱스가 필요하지 않은 경우에도 사용할 수 있다.

numbers = [10, 20, 30, 40, 50]

for _, value in enumerate(numbers):
    print(f"값: {value}")

12. 변수의 범위

변수의 범위(scope)는 변수 이름을 참조하거나 변경할 수 있는 영역을 뜻한다. 함수 내부에서 만든 지역 변수와 함수 외부에서 만든 전역 변수는 접근 가능한 범위가 다르다.

12.1 지역 변수

지역 변수(local variable)는 함수 내부에서 정의된 변수다. 해당 함수의 호출 프레임 안에서 사용되며, 함수 밖에서는 직접 접근할 수 없다.

def local_example():
    local_value = "지역 변수"
    print(local_value)

local_example()
# print(local_value)  # NameError가 발생한다.

함수를 다시 호출하면 새로운 프레임과 새로운 지역 변수가 만들어진다.

def count_up():
    count = 0
    count += 1
    print(count)

count_up()  # 1
count_up()  # 1

각 호출의 count는 서로 다른 지역 변수이므로 값이 누적되지 않는다.

12.2 전역 변수

전역 변수(global variable)는 함수 외부에서 정의된 변수다. 같은 모듈 안의 함수에서는 전역 변수의 값을 참조할 수 있다.

global_message = "전역 변수다."

def print_global_message():
    print(global_message)

print_global_message()

함수 내부에서 전역 변수와 같은 이름에 값을 대입하면 기본적으로 새로운 지역 변수를 만든다. 전역 변수 자체를 수정하려면 global 키워드를 명시해야 한다.

global_number = 10

def modify_global():
    global global_number
    global_number = 20

modify_global()
print(global_number)  # 20

전역 변수의 수정은 프로그램의 여러 부분에 영향을 줄 수 있으므로 꼭 필요한 경우에만 사용하는 것이 좋다. 가능한 경우에는 함수가 값을 반환하고, 호출한 위치에서 결과를 저장하는 방식이 더 명확하다.

number = 10

def doubled(value):
    return value * 2

number = doubled(number)
print(number)  # 20

12.3 이름 탐색 순서

함수 안에서 이름을 사용할 때 파이썬은 가까운 범위부터 차례대로 이름을 찾는다. 이를 LEGB 규칙이라고 한다.

순서범위의미
LLocal현재 함수의 지역 범위다.
EEnclosing바깥 함수의 지역 범위다.
GGlobal모듈의 전역 범위다.
BBuilt-inprint, len 같은 내장 이름의 범위다.
message = "전역"

def outer():
    message = "바깥 함수"

    def inner():
        message = "현재 함수"
        print(message)

    inner()

outer()  # 현재 함수

inner()에서는 가장 가까운 지역 범위에 message가 있으므로 "현재 함수"를 출력한다.

마무리

파이썬의 반복문은 데이터를 처리하는 기본 도구이며, 함수는 반복되는 로직을 재사용 가능한 단위로 나누는 도구다. while 문과 for 문으로 흐름을 만들고, range(), enumerate(), zip(), 컴프리헨션으로 반복 코드를 명확하게 표현할 수 있다.

함수에서는 함수 객체와 함수 호출 결과의 차이를 이해하는 것이 중요하다. 또한 기본값으로 변경 가능한 객체를 사용할 때의 주의점, *args와 **kwargs의 역할, 지역 변수와 전역 변수의 범위를 알고 있으면 더 안전하고 읽기 쉬운 파이썬 코드를 작성할 수 있다.

출처 및 참고 자료

본 정리는 강의 내용과 아래 자료를 참고하여 학습 목적으로 작성하였습니다.

https://ryuzyproject.tistory.com/287

profile
기록하며 성장하는 개발자

0개의 댓글