Python & AI Tutorials Logo
Python 프로그래밍

23. 일급 함수와 함수형 기법

이전 장들에서는 함수를 정의하고 호출하는 방법, 매개변수와 인자를 다루는 방법, 그리고 변수 스코프를 이해하는 방법을 배웠습니다. 이제 Python을 특별하게 만드는 강력한 기능을 살펴보겠습니다. 바로 함수는 일급 객체(first-class objects)라는 점입니다. 이는 함수를 다른 값과 똑같이 다룰 수 있다는 뜻으로—변수에 저장하고, 다른 함수에 인자로 전달하고, 함수에서 반환할 수 있습니다.

이 기능은 코드를 더 유연하고 재사용 가능하며 표현력이 풍부하게 만드는 우아한 프로그래밍 기법을 열어 줍니다. 실용적인 예제를 통해 일급 함수를 활용하는 방법을 살펴보고, 클로저(자신의 환경을 “기억하는” 함수)를 이해하며, 간결한 함수 정의를 위한 lambda 표현식을 사용해 보고, 컬렉션을 효율적으로 다루기 위해 map(), filter(), any(), all() 같은 내장 함수를 적용해 보겠습니다.

23.1) 일급 객체로서의 함수

23.1.1) "일급"의 의미

Python에서 함수는 일급 객체(first-class objects)이며, 이는 함수가 다음과 같은 일을 할 수 있다는 뜻입니다.

  • 변수에 할당할 수 있음
  • 데이터 구조(리스트, 딕셔너리 등)에 저장할 수 있음
  • 다른 함수에 인자로 전달할 수 있음
  • 다른 함수에서 값으로 반환할 수 있음

이는 함수가 특별한 지위를 가져서 일반 값처럼 조작할 수 없는 일부 프로그래밍 언어와는 다릅니다. Python에서는 함수도 정수, 문자열, 리스트와 비슷한 또 하나의 객체 타입일 뿐입니다.

직접 확인해 보겠습니다.

python
# 간단한 함수 정의
def greet(name):
    return f"Hello, {name}!"
 
# 함수를 변수에 할당
say_hello = greet
 
# 새 변수를 통해 함수 호출
message = say_hello("Alice")
print(message)  # Output: Hello, Alice!
 
# 두 이름이 같은 함수를 참조하는지 확인
print(greet)      # Output: <function greet at 0x...>
print(say_hello)  # Output: <function greet at 0x...>
print(greet is say_hello)  # Output: True

say_hello = greet라고 쓸 때 함수가 호출되는 것이 아니라(괄호가 없음), 동일한 함수 객체를 참조하는 새 이름을 만드는 것임에 주목하세요. 이제 greetsay_hello는 같은 함수를 가리키며, 이는 is 연산자를 사용해 확인할 수 있습니다.

23.1.2) 데이터 구조에 함수 저장하기

함수는 객체이므로 리스트, 딕셔너리 또는 다른 어떤 컬렉션에도 저장할 수 있습니다.

python
# 연산을 딕셔너리에 저장한 계산기
def add(x, y):
    return x + y
 
def subtract(x, y):
    return x - y
 
def multiply(x, y):
    return x * y
 
def divide(x, y):
    return x / y
 
# 함수를 딕셔너리에 저장
operations = {
    '+': add,
    '-': subtract,
    '*': multiply,
    '/': divide
}
 
# 딕셔너리를 사용해 계산 수행
num1 = 10
num2 = 5
operator = '*'
 
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}")  # Output: 10 * 5 = 50

이 패턴은 유연한 시스템을 구축할 때 매우 유용합니다. 어떤 함수를 호출할지 선택하기 위해 if-elif 문을 길게 늘어놓는 대신, 딕셔너리에서 적절한 함수를 찾아 바로 호출할 수 있습니다.

23.2) 함수를 인자로 전달하기

23.2.1) 기본 개념

일급 함수의 가장 강력한 활용 중 하나는 함수를 다른 함수의 인자로 전달하는 것입니다. 이를 통해 서로 다른 동작을 처리할 수 있는 유연하고 재사용 가능한 코드를 작성할 수 있습니다.

다음은 간단한 예시입니다.

python
# 다른 함수를 값에 적용하는 함수
def apply_operation(value, operation):
    """파라미터로 전달받은 함수를 값에 적용합니다."""
    return operation(value)
 
# 서로 다른 연산들
def double(x):
    return x * 2
 
def square(x):
    return x * x
 
def negate(x):
    return -x
 
# 같은 apply_operation 함수를 서로 다른 연산과 함께 사용
number = 5
print(apply_operation(number, double))   # Output: 10
print(apply_operation(number, square))   # Output: 25
print(apply_operation(number, negate))   # Output: -5

apply_operation 함수는 구체적으로 어떤 연산을 수행하는지 알지도, 신경 쓰지도 않습니다. 그저 전달받은 함수를 호출할 뿐입니다. 이런 관심사 분리는 코드를 더 모듈화하고 확장하기 쉽게 만듭니다.

23.2.2) 커스텀 함수로 컬렉션 처리하기

흔한 패턴 중 하나는 컬렉션의 각 아이템을 인자로 전달된 함수를 사용해 처리하는 것입니다.

python
# 주어진 함수를 사용해 리스트의 각 항목을 처리
def process_list(items, processor):
    """Apply processor function to each item in the list."""
    results = []
    for item in items:
        results.append(processor(item))
    return results
 
# 서로 다른 처리 함수들
def uppercase(text):
    return text.upper()
 
def add_exclamation(text):
    return text + "!"
 
def get_length(text):
    return len(text)
 
# 같은 리스트를 서로 다른 방식으로 처리
words = ["hello", "world", "python"]
 
print(process_list(words, uppercase))        # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation))  # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length))       # Output: [5, 5, 6]

이 패턴은 너무 유용해서 Python은 map()filter() 같은 내장 함수를 제공하며, 이들도 같은 방식으로 동작합니다(23.6절에서 살펴보겠습니다).

23.2.3) key 함수를 사용한 커스텀 정렬 (간단 소개)

Python의 sorted() 함수는 key 매개변수를 받는데, 이는 아이템을 어떻게 비교할지 결정하는 함수입니다.

python
# 서로 다른 기준으로 학생 정렬
students = [
    {"name": "Alice", "grade": 85, "age": 20},
    {"name": "Bob", "grade": 92, "age": 19},
    {"name": "Charlie", "grade": 78, "age": 21},
    {"name": "Diana", "grade": 95, "age": 20}
]
 
# 점수를 추출하는 함수
def get_grade(student):
    return student["grade"]
 
# 이름을 추출하는 함수
def get_name(student):
    return student["name"]
 
# 점수 기준 정렬(오름차순)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Charlie: 78
#   Alice: 85
#   Bob: 92
#   Diana: 95
 
# 이름 기준 정렬(알파벳순)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Alice: 85
#   Bob: 92
#   Charlie: 78
#   Diana: 95

key 함수는 각 아이템마다 한 번 호출되며, 그 반환값이 비교에 사용됩니다. 이는 커스텀 정렬 로직을 직접 작성해야 하는 것보다 훨씬 유연합니다.

함수를 전달해 동작을 커스터마이즈하는 이 패턴은 Python에서 아주 흔하게 사용됩니다. 더 고급 정렬 기법은 챕터 38에서 자세히 다루겠습니다.

23.3) 함수에서 함수를 반환하기

23.3.1) 함수를 만드는 함수

함수를 인자로 전달할 수 있는 것처럼, 다른 함수에서 함수를 반환할 수도 있습니다. 이를 통해 특화된 함수를 동적으로 만들 수 있습니다.

python
# 새 함수를 만들어 반환하는 함수
def create_multiplier(factor):
    """Create a function that multiplies by the given factor."""
    def multiplier(x):
        return x * factor
    return multiplier
 
# 특화된 곱셈 함수 생성
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
 
# 생성된 함수 사용
print(double(5))      # Output: 10
print(triple(5))      # Output: 15
print(times_ten(5))   # Output: 50

여기서 무슨 일이 일어나고 있을까요? create_multiplier 함수는 multiplier라는 내부 함수를 정의한 뒤 이를 반환합니다. create_multiplier를 다른 factor로 호출할 때마다, 그 특정 factor를 “기억하는” 새 함수를 돌려받습니다. 이것이 클로저(closures)의 첫 맛보기이며, 다음 절에서 자세히 살펴보겠습니다.

23.3.2) 맞춤형 검증기 만들기

함수를 반환하는 방식은 맞춤형 검증 함수나 처리 함수를 만드는 데 특히 유용합니다.

python
# 범위 검증기를 동적으로 생성
def create_range_validator(min_value, max_value):
    """Create a function that validates if a number is in range."""
    def validator(number):
        return min_value <= number <= max_value
    return validator
 
# 구체적인 검증기 생성
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
 
# 검증기 사용
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}")  # Output: True
 
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}")  # Output: True
 
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}")  # Output: False

23.4) 클로저 이해하기: 기억하는 함수

23.4.1) 클로저란?

클로저(closure)는 자신이 생성된 스코프의 변수를 “기억하는” 함수로, 그 스코프의 실행이 끝난 뒤에도 해당 변수에 접근할 수 있습니다. 23.3절의 예시에서 우리는 이미 클로저를 명시적으로 이름 붙이지 않았을 뿐, 사용하고 있었습니다.

클로저가 어떻게 동작하는지 살펴보겠습니다.

python
def create_counter(start=0):
    """Create a counter function that remembers its count."""
    count = start  # 이 변수는 클로저에 의해 "캡처"됩니다
    
    def counter():
        nonlocal count  # 캡처된 변수에 접근
        count += 1
        return count
    
    return counter
 
# 서로 독립적인 두 카운터 생성
counter1 = create_counter(0)
counter2 = create_counter(100)
 
# 각 카운터는 자신만의 count를 유지
print(counter1())  # Output: 1
print(counter1())  # Output: 2
print(counter1())  # Output: 3
 
print(counter2())  # Output: 101
print(counter2())  # Output: 102
 
print(counter1())  # Output: 4 (counter1 is independent of counter2)

내부 counter 함수는 count 변수에 대해 클로저를 형성합니다. create_counter가 실행을 끝냈더라도, 반환된 counter 함수는 여전히 count에 접근할 수 있습니다. create_counter를 호출할 때마다 자신만의 count 변수를 가진 새롭고 독립적인 클로저가 만들어집니다.

23.4.2) 클로저가 변수를 캡처하는 방식

함수가 다른 함수 내부에 정의되면, 외부 함수의 스코프에 있는 변수에 접근할 수 있습니다. 이러한 변수들은 "캡처"되어 외부 함수가 반환된 후에도 계속 접근 가능합니다.

Python이 내부 함수를 생성할 때는 함수 코드만 저장하는 것이 아니라, 내부 함수가 사용하는 외부 함수의 변수들에 대한 참조도 함께 저장합니다. 이 과정을 변수를 "캡처한다"고 합니다.

python
def create_greeter(greeting):
    """Create a greeting function with a custom greeting."""
    def greet(name):
        return f"{greeting}, {name}!"
    return greet
 
# 서로 다른 인사 함수 생성
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
 
# 각 인사 함수는 자신만의 greeting을 기억
print(say_hello("Alice"))    # Output: Hello, Alice!
print(say_hi("Bob"))         # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!

greeting 매개변수는 클로저에 의해 캡처됩니다. 각 greeter 함수는 호출될 때마다 사용할 자신만의 캡처된 greeting 값을 가집니다.

23.4.3) 실용적 활용: 설정 함수

클로저는 사전 설정된 동작을 가진 함수를 만드는 데 매우 좋습니다.

python
# 서로 다른 세율을 가진 가격 계산기 생성
def create_price_calculator(tax_rate):
    """Create a calculator that applies a specific tax rate."""
    def calculate_total(price):
        tax = price * tax_rate
        return price + tax
    return calculate_total
 
# 지역별 계산기 생성
us_calculator = create_price_calculator(0.07)    # 7% tax
uk_calculator = create_price_calculator(0.20)    # 20% VAT
japan_calculator = create_price_calculator(0.10) # 10% consumption tax
 
# 지역별로 가격 계산
item_price = 100
 
print(f"US total: ${us_calculator(item_price):.2f}")      # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}")      # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.00

23.4.4) 언제 클로저를 사용할까?

클로저는 특히 다음이 필요할 때 유용합니다.

  • 사전 설정된 동작을 가진 함수를 만들 때
  • 클래스를 사용하지 않고 함수 호출 사이에 상태를 유지할 때
  • 컨텍스트를 기억해야 하는 콜백 함수를 구현할 때
  • 특화된 함수를 만들어내는 함수 팩토리를 만들 때

23.5) 짧은 익명 함수를 위한 lambda 사용하기

23.5.1) Lambda 표현식이란?

lambda 표현식(lambda expression)은 작고 익명의 함수, 즉 이름이 없는 함수를 만듭니다. lambda 표현식은 짧은 기간 동안 간단한 함수가 필요하지만 def로 정식 정의하고 싶지 않을 때 유용합니다.

문법은 다음과 같습니다.

python
lambda parameters: expression

lambda는 매개변수(일반 함수와 같음)를 받고, 표현식을 평가한 결과를 반환합니다. 다음은 간단한 예시입니다.

python
# 일반 함수
def add(x, y):
    return x + y
 
# 동등한 lambda 표현식
add_lambda = lambda x, y: x + y
 
# 둘 다 같은 방식으로 동작
print(add(3, 5))        # Output: 8
print(add_lambda(3, 5)) # Output: 8

lambda 표현식은 단일 표현식으로 제한되며, if, for 같은 문(statement)이나 여러 줄 코드를 포함할 수 없습니다. 이 제한은 lambda를 단순하고 집중된 도구로 유지해 줍니다.

23.5.2) 인자로서의 Lambda 표현식

lambda 표현식은 간단한 함수를 인자로 전달해야 하고, 별도의 이름 있는 함수를 정의하고 싶지 않을 때 진가를 발휘합니다.

python
# lambda를 사용해 점수 기준으로 학생 정렬
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 92},
    {"name": "Charlie", "grade": 78},
    {"name": "Diana", "grade": 95}
]
 
# 대신 별도 함수를 정의할 수도 있음:
# def get_grade(student):
#     return student["grade"]
# sorted_students = sorted(students, key=get_grade)
 
# lambda를 직접 사용할 수 있음:
sorted_students = sorted(students, key=lambda student: student["grade"])
 
print("Students sorted by grade:")
for student in sorted_students:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Charlie: 78
#   Alice: 85
#   Bob: 92
#   Diana: 95

함수가 단순하고 한 번만 사용된다면, 이 방식이 더 간결합니다. lambda lambda student: student["grade"]는 학생을 받아 점수를 반환하는 함수와 동등합니다.

23.5.3) 여러 매개변수를 받는 Lambda

lambda 표현식은 일반 함수처럼 여러 매개변수를 받을 수 있습니다.

python
# lambda로 계산기 연산 정의
operations = {
    'add': lambda x, y: x + y,
    'subtract': lambda x, y: x - y,
    'multiply': lambda x, y: x * y,
    'divide': lambda x, y: x / y if y != 0 else "Error"
}
 
# lambda 표현식 사용
print(operations['add'](10, 5))       # Output: 15
print(operations['multiply'](10, 5))  # Output: 50
print(operations['divide'](10, 0))    # Output: Error

lambda 안에서는 조건 표현식(x / y if y != 0 else "Error")을 사용할 수 있지만, 여러 줄이 필요한 if 문은 사용할 수 없습니다.

23.5.4) Lambda vs 이름 있는 함수: 언제 무엇을 쓸까?

lambda 표현식을 사용할 때:

  • 함수가 매우 단순할 때(표현식 하나)
  • 함수가 한 번만 쓰이거나 매우 국소적인 맥락에서만 사용될 때
  • 이름 있는 함수를 정의하는 것이 불필요하게 장황해질 때

이름 있는 함수를 사용할 때:

  • 함수가 복잡하거나 여러 문이 필요할 때
  • 함수가 여러 곳에서 재사용될 때
  • 명확성을 위해 설명적인 이름이 필요할 때
  • docstring이 필요할 때

23.5.5) Lambda의 한계와 대안

lambda 표현식에는 중요한 한계가 있습니다.

python
# ❌ 동작하지 않음 - lambda에는 문(statement)을 넣을 수 없음
# bad_lambda = lambda x: 
#     if x > 0:
#         return x
#     else:
#         return -x
 
# ✅ 대신 조건 표현식을 사용
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5))  # Output: 5
print(absolute_value(3))   # Output: 3
 
# ✅ 여러 작업에는 일반 함수를 사용
def process_and_double(x):
    print(f"Processing: {x}")
    return x * 2
 
result = process_and_double(5)  # Output: Processing: 5
print(result)                    # Output: 10

lambda 표현식은 특정 상황을 위한 도구입니다. 코드를 더 명확하고 간결하게 만들 때는 사용하세요. 코드를 이해하기 어렵게 만든다면, 대신 일반적인 이름 있는 함수를 사용하세요.

23.6) 간단한 함수와 함께 map() 및 filter() 사용하기

23.6.1) map() 함수

map() 함수는 주어진 함수(function)를 이터러블(리스트, 튜플, 문자열 등)의 각 항목에 적용하고, 결과를 담은 이터레이터를 반환합니다. 명시적인 루프를 작성하지 않고도 컬렉션의 모든 요소를 변환하는 방법입니다.

python
map(function, iterable, *iterables)

파라미터:

  • function (필수): 하나 이상의 인자를 받아 처리하고 값을 반환하는 함수입니다. 이 함수는 iterable(s)의 각 요소마다 한 번씩 호출됩니다.
  • iterable (필수): 요소들이 함수에 전달될 시퀀스(리스트, 튜플, 문자열 등)입니다.
  • *iterables (선택): 여러 인자를 받는 function을 위한 추가 이터러블입니다.

여러 이터러블이 제공되면, function은 그만큼의 인자를 받아야 합니다 map()은 가장 짧은 이터러블이 소진되면 멈춥니다

반환값:

function이 각 입력 요소에 대해 반환한 결과를 담은 map 객체(이터레이터)입니다.

중요: map 객체는 이터레이터이며, 리스트 같은 시퀀스가 아닙니다.

python
# 리스트의 모든 숫자를 두 배로 만들기
numbers = [1, 2, 3, 4, 5]
 
def double(x):
    return x * 2
 
# 각 숫자에 double 적용
doubled = map(double, numbers)
result = list(doubled)  # map 객체(이터레이터)를 리스트로 변환
print(result)  # Output: [2, 4, 6, 8, 10]

23.6.2) lambda와 함께 map() 사용하기

lambda 표현식은 간단한 변환에서 map()과 완벽하게 어울립니다.

python
# 섭씨를 화씨로 변환
celsius_temps = [0, 10, 20, 30, 40]
 
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps)  # Output: [32.0, 50.0, 68.0, 86.0, 104.0]

23.6.3) filter() 함수

filter() 함수는 주어진 함수(function)를 이터러블의 각 항목에 적용하고, 함수가 True를 반환하는 항목만 담은 이터레이터를 반환합니다. 명시적인 루프를 작성하지 않고도 컬렉션에서 요소를 선택하는 방법입니다.

python
filter(function, iterable)

파라미터:

  • function: 하나의 인자를 받아 평가하고 True 또는 False를 반환하는 함수입니다. 이 함수는 iterable의 각 요소마다 한 번씩 호출됩니다.
  • iterable: function에 의해 테스트될 요소들을 가진 시퀀스(리스트, 튜플, 문자열 등)입니다.

반환값:

functionTrue를 반환한 요소들만 담은 filter 객체(이터레이터)입니다.

중요: filter 객체는 이터레이터이며, 리스트 같은 시퀀스가 아닙니다.

예제)

python
# 짝수만 유지하기
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
def is_even(x):
    return x % 2 == 0
 
# 각 숫자에 is_even 적용, True를 반환하는 것만 유지
even_numbers = filter(is_even, numbers)
result = list(even_numbers)  # filter 객체를 리스트로 변환
print(result)  # Output: [2, 4, 6, 8, 10]

23.6.4) lambda와 함께 filter() 사용하기

lambda 표현식은 간결한 필터링을 위해 filter()와 함께 흔히 사용됩니다.

python
# 합격한 학생 필터링(grade >= 60)
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 55},
    {"name": "Charlie", "grade": 92},
    {"name": "Diana", "grade": 48},
    {"name": "Eve", "grade": 73}
]
 
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
    print(f"  {student['name']}: {student['grade']}")
# Output:
#   Alice: 85
#   Charlie: 92
#   Eve: 73

23.6.5) map()과 filter() 결합하기

map()filter() 연산을 체인으로 연결하여 복잡한 변환을 수행할 수 있습니다.

python
# 짝수의 제곱 얻기
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
# 먼저 짝수를 필터링하고, 그다음 제곱
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result)  # Output: [4, 16, 36, 64, 100]

시각적 비교: map() vs filter()

filter() - 일부 항목만 유지

입력: [1, 2, 3, 4, 5]

테스트: is_even(x)

출력: [2, 4]
(같거나 더 짧음)

map() - 모든 항목 변환

입력: [1, 2, 3, 4, 5]

적용: double(x) = x * 2

출력: [2, 4, 6, 8, 10]
(같은 길이)

주요 차이점:

  • map(): 함수를 적용하여 모든 항목을 변환 → 출력의 길이가 동일
  • filter(): 각 항목을 테스트하고 통과한 것만 유지 → 출력의 길이가 같거나 더 짧음

이 장에서는 Python의 강력한 함수형 프로그래밍 기능을 살펴보았습니다. 함수가 일급 객체라서 다른 값처럼 여기저기 전달할 수 있으며, 이를 통해 유연하고 재사용 가능한 코드 패턴이 가능하다는 것을 배웠습니다. 함수가 다른 함수를 반환할 수 있어 자신의 환경을 기억하는 클로저가 만들어진다는 것도 알아보았습니다. 간결한 함수 정의를 위한 lambda 표현식을 탐구했고, 컬렉션을 우아하게 처리하기 위해 map(), filter()을 사용했습니다.

이 개념들은 고급 Python 프로그래밍 기법의 기반을 이룹니다. 38장에서는 이 지식을 바탕으로 Python의 가장 우아한 기능 중 하나인 데코레이터를 마스터하게 될 것입니다.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai