← 모든 글

함수 정의와 활용 완전 가이드

Python 함수를 기초부터 고급 활용법까지 완전 정복해보자

Python 기초 · 3 / 3편

이 글의 목차

🎯 학습 목표

이 글을 통해 다음을 학습할 수 있습니다:

  • 함수의 기본 정의와 호출 방법
  • 매개변수와 인수의 다양한 활용법
  • 람다 함수와 고차 함수 활용
  • 클로저와 데코레이터의 실전 활용
  • 코딩테스트에서 자주 사용되는 함수 패턴

📝 함수가 왜 필요할까?

함수는 코드의 재사용성과 가독성을 높이는 Python의 핵심 기능입니다.

함수 없이 코딩할 때의 문제점

# ❌ 함수 없이 반복되는 코드 (비효율적)
# 학생 1의 평균 계산
student1_scores = [90, 85, 92]
student1_total = 0
for score in student1_scores:
    student1_total += score
student1_average = student1_total / len(student1_scores)
print(f"학생1 평균: {student1_average}")

# 학생 2의 평균 계산 (똑같은 코드 반복)
student2_scores = [78, 82, 88]
student2_total = 0
for score in student2_scores:
    student2_total += score
student2_average = student2_total / len(student2_scores)
print(f"학생2 평균: {student2_average}")

함수를 사용한 깔끔한 해결

# ✅ 함수로 재사용 가능한 코드 작성
def calculate_average(scores):
    """점수 리스트의 평균을 계산합니다."""
    return sum(scores) / len(scores)

# 간단하고 명확한 사용
student1_average = calculate_average([90, 85, 92])
student2_average = calculate_average([78, 82, 88])
print(f"학생1 평균: {student1_average}")
print(f"학생2 평균: {student2_average}")

🔧 기본 함수 정의와 호출

1. 기본 문법

def function_name(parameters):
    """독스트링: 함수의 설명"""
    # 함수 본문
    return value  # 선택사항

2. 간단한 함수 예제

# 인사말 함수
def greet(name):
    """이름을 받아서 인사말을 반환합니다."""
    return f"안녕하세요, {name}님!"

# 함수 호출
message = greet("파이썬")
print(message)  # 안녕하세요, 파이썬님!

# 반환값이 없는 함수
def print_info(name, age):
    """정보를 출력하는 함수"""
    print(f"이름: {name}")
    print(f"나이: {age}")
    # return 없음 (자동으로 None 반환)

print_info("김파이썬", 25)

3. 여러 값 반환하기

def get_name_age():
    """이름과 나이를 함께 반환합니다."""
    return "김파이썬", 25

# 튜플로 받기
result = get_name_age()
print(result)  # ('김파이썬', 25)

# 언패킹으로 받기
name, age = get_name_age()
print(f"이름: {name}, 나이: {age}")  # 이름: 김파이썬, 나이: 25

🚀 매개변수와 인수의 고급 활용

1. 기본값 매개변수

def introduce(name, age=20, city="서울"):
    """자기소개 함수 (기본값 제공)"""
    return f"안녕하세요! {name}입니다. {age}살이고 {city}에 살고 있어요."

# 다양한 호출 방법
print(introduce("김파이썬"))                    # 기본값 사용
print(introduce("이자바", 30))                  # age만 변경
print(introduce("박자스", city="부산"))          # city만 변경
print(introduce("최씨샵", 28, "대구"))           # 모든 값 제공

2. 키워드 인수

def create_profile(name, age, job, city):
    """프로필 생성 함수"""
    return {
        "name": name,
        "age": age,
        "job": job,
        "city": city
    }

# ✅ 키워드 인수로 명확한 호출
profile = create_profile(
    name="김파이썬",
    city="서울", 
    age=25,
    job="개발자"
)
print(profile)  # {'name': '김파이썬', 'age': 25, 'job': '개발자', 'city': '서울'}

3. 가변 인수 (*args, **kwargs)

# *args: 여러 개의 위치 인수
def sum_all(*numbers):
    """모든 숫자의 합을 계산합니다."""
    return sum(numbers)

print(sum_all(1, 2, 3))        # 6
print(sum_all(1, 2, 3, 4, 5))  # 15

# **kwargs: 여러 개의 키워드 인수
def create_student(**student_info):
    """학생 정보 딕셔너리를 생성합니다."""
    return student_info

student = create_student(name="김파이썬", age=20, major="컴퓨터과학")
print(student)  # {'name': '김파이썬', 'age': 20, 'major': '컴퓨터과학'}

# 혼합 사용
def flexible_function(required_arg, *args, **kwargs):
    """유연한 함수 예제"""
    print(f"필수 인수: {required_arg}")
    print(f"추가 위치 인수: {args}")
    print(f"키워드 인수: {kwargs}")

flexible_function("필수값", 1, 2, 3, name="파이썬", version=3.9)
# 필수 인수: 필수값
# 추가 위치 인수: (1, 2, 3)
# 키워드 인수: {'name': '파이썬', 'version': 3.9}

💡 실전 활용 예제

예제 1: 계산기 함수들

def add(a, b):
    """덧셈"""
    return a + b

def subtract(a, b):
    """뺄셈"""
    return a - b

def multiply(a, b):
    """곱셈"""
    return a * b

def divide(a, b):
    """나눗셈 (0으로 나누기 예외 처리)"""
    if b == 0:
        return "0으로 나눌 수 없습니다!"
    return a / b

# 계산기 딕셔너리
calculator = {
    '+': add,
    '-': subtract,
    '*': multiply,
    '/': divide
}

# 사용 예시
result = calculator['+'](10, 5)
print(f"10 + 5 = {result}")  # 10 + 5 = 15

예제 2: 리스트 처리 함수들

def filter_even_numbers(numbers):
    """짝수만 필터링"""
    return [num for num in numbers if num % 2 == 0]

def get_statistics(numbers):
    """숫자 리스트의 통계 정보"""
    if not numbers:
        return None
    
    return {
        'count': len(numbers),
        'sum': sum(numbers),
        'average': sum(numbers) / len(numbers),
        'min': min(numbers),
        'max': max(numbers)
    }

def transform_list(numbers, operation):
    """리스트의 각 요소에 연산 적용"""
    return [operation(num) for num in numbers]

# 사용 예시
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

even_nums = filter_even_numbers(numbers)
print(f"짝수들: {even_nums}")  # 짝수들: [2, 4, 6, 8, 10]

stats = get_statistics(numbers)
print(f"통계: {stats}")

# 각 숫자를 제곱
squared = transform_list(numbers, lambda x: x**2)
print(f"제곱: {squared}")  # 제곱: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

예제 3: 문자열 처리 함수

def clean_text(text):
    """텍스트 정리 함수"""
    # 앞뒤 공백 제거 및 소문자 변환
    return text.strip().lower()

def count_words(text):
    """단어 개수 세기"""
    words = text.split()
    return len(words)

def extract_numbers(text):
    """텍스트에서 숫자만 추출"""
    return ''.join(char for char in text if char.isdigit())

def validate_email(email):
    """간단한 이메일 검증"""
    return '@' in email and '.' in email.split('@')[-1]

# 텍스트 처리 파이프라인
def process_user_input(user_input):
    """사용자 입력 처리"""
    cleaned = clean_text(user_input)
    word_count = count_words(cleaned)
    numbers = extract_numbers(cleaned)
    
    return {
        'original': user_input,
        'cleaned': cleaned,
        'word_count': word_count,
        'numbers': numbers
    }

# 사용 예시
result = process_user_input("  Hello World 123! Python is AWESOME 456  ")
print(result)
# {'original': '  Hello World 123! Python is AWESOME 456  ', 
#  'cleaned': 'hello world 123! python is awesome 456', 
#  'word_count': 7,
#  'numbers': '123456'}

🧮 람다 함수 (익명 함수)

1. 기본 람다 함수

# 일반 함수
def square(x):
    return x ** 2

# 람다 함수로 동일한 기능
square_lambda = lambda x: x ** 2

print(square(5))        # 25
print(square_lambda(5)) # 25

# 여러 매개변수를 가진 람다
add_lambda = lambda x, y: x + y
print(add_lambda(3, 4))  # 7

2. 람다와 고차 함수 활용

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map: 각 요소에 함수 적용
squared = list(map(lambda x: x**2, numbers))
print(f"제곱: {squared}")  # 제곱: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# filter: 조건에 맞는 요소만 선택
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(f"짝수: {even_numbers}")  # 짝수: [2, 4, 6, 8, 10]

# sorted: 정렬 키 함수
students = [
    {"name": "김파이썬", "score": 85},
    {"name": "이자바", "score": 92},
    {"name": "박자스", "score": 78}
]

# 점수로 정렬
sorted_students = sorted(students, key=lambda student: student["score"])
print("점수순 정렬:", sorted_students)

# 이름으로 정렬
sorted_by_name = sorted(students, key=lambda student: student["name"])
print("이름순 정렬:", sorted_by_name)

3. 람다 함수 실전 활용

# 데이터 변환 파이프라인
data = [
    {"name": "apple", "price": 1000, "category": "fruit"},
    {"name": "banana", "price": 500, "category": "fruit"},
    {"name": "carrot", "price": 300, "category": "vegetable"},
    {"name": "broccoli", "price": 800, "category": "vegetable"}
]

# 과일만 선택하고 가격으로 정렬
fruits = sorted(
    filter(lambda item: item["category"] == "fruit", data),
    key=lambda item: item["price"]
)
print("과일 (가격순):", fruits)

# 모든 상품에 할인가 추가
discounted_data = list(map(
    lambda item: {**item, "discounted_price": item["price"] * 0.9},
    data
))
print("할인가 추가:", discounted_data[0])

🔄 클로저와 스코프

1. 지역 변수와 전역 변수

# 전역 변수
global_var = "전역 변수"

def outer_function():
    # 외부 함수의 지역 변수
    outer_var = "외부 함수 변수"
    
    def inner_function():
        # 내부 함수의 지역 변수
        inner_var = "내부 함수 변수"
        
        # 모든 스코프의 변수에 접근 가능
        print(f"내부: {inner_var}")
        print(f"외부: {outer_var}")
        print(f"전역: {global_var}")
    
    inner_function()

outer_function()

2. 클로저 (Closure)

def create_multiplier(factor):
    """곱셈 함수를 생성하는 팩토리 함수"""
    def multiplier(number):
        return number * factor  # 외부 함수의 변수 사용
    return multiplier

# 다양한 곱셈 함수 생성
double = create_multiplier(2)
triple = create_multiplier(3)
ten_times = create_multiplier(10)

print(double(5))    # 10
print(triple(4))    # 12
print(ten_times(3)) # 30

# 카운터 클로저
def create_counter(start=0):
    """카운터 함수를 생성합니다."""
    count = start
    
    def counter():
        nonlocal count  # 외부 함수 변수 수정
        count += 1
        return count
    
    return counter

# 독립적인 카운터들
counter1 = create_counter()
counter2 = create_counter(10)

print(counter1())  # 1
print(counter1())  # 2
print(counter2())  # 11
print(counter1())  # 3

🎨 데코레이터 기초

1. 기본 데코레이터

def timing_decorator(func):
    """함수 실행 시간을 측정하는 데코레이터"""
    import time
    
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} 실행 시간: {end_time - start_time:.4f}초")
        return result
    
    return wrapper

# 데코레이터 사용
@timing_decorator
def slow_function():
    """느린 함수 시뮬레이션"""
    import time
    time.sleep(1)
    return "완료!"

result = slow_function()  # slow_function 실행 시간: 1.0012초
print(result)             # 완료!

2. 로깅 데코레이터

def log_calls(func):
    """함수 호출을 로깅하는 데코레이터"""
    def wrapper(*args, **kwargs):
        print(f"[LOG] {func.__name__} 호출됨")
        print(f"[LOG] 인수: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} 완료, 결과: {result}")
        return result
    
    return wrapper

@log_calls
def calculate_area(width, height):
    """넓이 계산 함수"""
    return width * height

area = calculate_area(5, 3)
# [LOG] calculate_area 호출됨
# [LOG] 인수: args=(5, 3), kwargs={}
# [LOG] calculate_area 완료, 결과: 15

3. 매개변수가 있는 데코레이터

def repeat(times):
    """함수를 여러 번 실행하는 데코레이터"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for i in range(times):
                result = func(*args, **kwargs)
                results.append(result)
            return results
        return wrapper
    return decorator

@repeat(3)
def say_hello(name):
    """인사말 함수"""
    return f"안녕하세요, {name}님!"

greetings = say_hello("파이썬")
print(greetings)  # ['안녕하세요, 파이썬님!', '안녕하세요, 파이썬님!', '안녕하세요, 파이썬님!']

🧮 코딩테스트 실전 예제

예제 1: 재귀 함수

def factorial(n):
    """팩토리얼 계산 (재귀)"""
    if n <= 1:
        return 1
    return n * factorial(n - 1)

def fibonacci(n):
    """피보나치 수열 (메모이제이션)"""
    memo = {}
    
    def fib_helper(num):
        if num in memo:
            return memo[num]
        if num <= 1:
            return num
        
        memo[num] = fib_helper(num - 1) + fib_helper(num - 2)
        return memo[num]
    
    return fib_helper(n)

# 사용 예시
print(f"5! = {factorial(5)}")      # 5! = 120
print(f"피보나치 10번째: {fibonacci(10)}")  # 피보나치 10번째: 55

예제 2: 정렬과 탐색

def binary_search(arr, target):
    """이진 탐색"""
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = (left + right) // 2
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    
    return -1

def quick_sort(arr):
    """퀵 정렬"""
    if len(arr) <= 1:
        return arr
    
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    
    return quick_sort(left) + middle + quick_sort(right)

# 사용 예시
numbers = [64, 34, 25, 12, 22, 11, 90]
sorted_numbers = quick_sort(numbers)
print(f"정렬된 배열: {sorted_numbers}")

index = binary_search(sorted_numbers, 25)
print(f"25의 인덱스: {index}")

예제 3: 동적 프로그래밍

def longest_common_subsequence(text1, text2):
    """최장 공통 부분 수열"""
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    
    return dp[m][n]

def knapsack(weights, values, capacity):
    """0/1 배낭 문제"""
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            if weights[i-1] <= w:
                dp[i][w] = max(
                    dp[i-1][w],  # 선택하지 않음
                    dp[i-1][w-weights[i-1]] + values[i-1]  # 선택함
                )
            else:
                dp[i][w] = dp[i-1][w]
    
    return dp[n][capacity]

# 사용 예시
text1, text2 = "abcde", "ace"
lcs_length = longest_common_subsequence(text1, text2)
print(f"LCS 길이: {lcs_length}")  # LCS 길이: 3

weights = [10, 20, 30]
values = [60, 100, 120]
capacity = 50
max_value = knapsack(weights, values, capacity)
print(f"최대 가치: {max_value}")  # 최대 가치: 220

⚡ 함수 최적화와 활용 팁

1. 타입 힌트 활용

from typing import List, Dict, Optional, Tuple

def process_scores(scores: List[int]) -> Dict[str, float]:
    """점수 리스트를 처리하여 통계를 반환합니다."""
    if not scores:
        return {}
    
    return {
        'average': sum(scores) / len(scores),
        'min': float(min(scores)),
        'max': float(max(scores))
    }

def find_student(students: List[Dict], name: str) -> Optional[Dict]:
    """이름으로 학생을 찾습니다."""
    for student in students:
        if student.get('name') == name:
            return student
    return None

# 사용 예시
scores = [85, 92, 78, 96, 88]
stats = process_scores(scores)
print(f"통계: {stats}")

2. 독스트링과 문서화

def calculate_compound_interest(principal: float, rate: float, 
                              time: int, compound_frequency: int = 1) -> float:
    """
    복리 이자를 계산합니다.
    
    Args:
        principal (float): 원금
        rate (float): 연 이자율 (소수점으로 표현, 예: 0.05 = 5%)
        time (int): 투자 기간 (년)
        compound_frequency (int, optional): 연간 복리 계산 횟수. 기본값은 1.
    
    Returns:
        float: 복리로 계산된 최종 금액
        
    Example:
        >>> calculate_compound_interest(1000, 0.05, 3, 4)
        1160.75
    """
    amount = principal * (1 + rate / compound_frequency) ** (compound_frequency * time)
    return round(amount, 2)

# 도움말 확인
help(calculate_compound_interest)

3. 함수 성능 측정

import time
import functools

def performance_timer(func):
    """함수 성능을 측정하는 데코레이터"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__}: {end - start:.6f}초")
        return result
    return wrapper

@performance_timer
def efficient_sum(numbers):
    """효율적인 합계 계산"""
    return sum(numbers)

@performance_timer
def inefficient_sum(numbers):
    """비효율적인 합계 계산"""
    total = 0
    for num in numbers:
        total = total + num
    return total

# 성능 비교
large_numbers = list(range(1000000))
result1 = efficient_sum(large_numbers)
result2 = inefficient_sum(large_numbers)

🎯 실습 문제

문제: 학생 관리 시스템

from typing import Dict, List, Optional

# 학생 데이터베이스
students_db = []

def add_student(name: str, age: int, subjects: List[str]) -> Dict:
    """새 학생을 추가합니다."""
    student = {
        'id': len(students_db) + 1,
        'name': name,
        'age': age,
        'subjects': subjects,
        'scores': {}
    }
    students_db.append(student)
    return student

def add_score(student_id: int, subject: str, score: int) -> bool:
    """학생의 과목 점수를 추가합니다."""
    student = find_student_by_id(student_id)
    if student and subject in student['subjects']:
        student['scores'][subject] = score
        return True
    return False

def find_student_by_id(student_id: int) -> Optional[Dict]:
    """ID로 학생을 찾습니다."""
    for student in students_db:
        if student['id'] == student_id:
            return student
    return None

def calculate_student_average(student_id: int) -> Optional[float]:
    """학생의 평균 점수를 계산합니다."""
    student = find_student_by_id(student_id)
    if student and student['scores']:
        return sum(student['scores'].values()) / len(student['scores'])
    return None

def get_top_students(n: int = 3) -> List[Dict]:
    """상위 n명의 학생을 반환합니다."""
    students_with_avg = []
    
    for student in students_db:
        avg = calculate_student_average(student['id'])
        if avg is not None:
            students_with_avg.append((student, avg))
    
    # 평균 점수로 정렬
    students_with_avg.sort(key=lambda x: x[1], reverse=True)
    
    return [student for student, avg in students_with_avg[:n]]

# 사용 예시
student1 = add_student("김파이썬", 20, ["수학", "과학", "영어"])
student2 = add_student("이자바", 21, ["수학", "과학", "국어"])

add_score(1, "수학", 95)
add_score(1, "과학", 87)
add_score(1, "영어", 92)

add_score(2, "수학", 88)
add_score(2, "과학", 94)
add_score(2, "국어", 85)

print(f"학생1 평균: {calculate_student_average(1)}")
print(f"학생2 평균: {calculate_student_average(2)}")

top_students = get_top_students(2)
for student in top_students:
    avg = calculate_student_average(student['id'])
    print(f"우수학생: {student['name']} (평균: {avg:.1f})")

📚 정리 및 다음 단계

오늘 배운 내용

  • ✅ 함수의 기본 정의와 호출 방법
  • ✅ 다양한 매개변수와 인수 활용법 (*args, **kwargs)
  • ✅ 람다 함수와 고차 함수 활용
  • ✅ 클로저와 스코프의 이해
  • ✅ 데코레이터를 이용한 함수 확장
  • ✅ 코딩테스트에서 자주 사용되는 함수 패턴

함수 사용 시 주의사항

# ❌ 피해야 할 패턴들

# 1. 가변 객체를 기본값으로 사용
def bad_function(items=[]):  # 위험!
    items.append("new")
    return items

# ✅ 올바른 방법
def good_function(items=None):
    if items is None:
        items = []
    items.append("new")
    return items

# 2. 함수가 너무 많은 일을 처리
def bad_process_data(data):  # 너무 복잡!
    # 데이터 정제
    # 계산 수행
    # 결과 저장
    # 이메일 발송
    # ...
    pass

# ✅ 기능별로 분리
def clean_data(data): pass
def calculate_results(data): pass
def save_results(results): pass
def send_email(results): pass

def process_data(data):
    cleaned = clean_data(data)
    results = calculate_results(cleaned)
    save_results(results)
    send_email(results)
    return results

함수 설계 베스트 프랙티스

1. 단일 책임 원칙

# ✅ 하나의 명확한 역할
def calculate_tax(income, rate):
    """소득에 대한 세금을 계산합니다."""
    return income * rate

def format_currency(amount):
    """금액을 통화 형식으로 포맷팅합니다."""
    return f"₩{amount:,.0f}"

def validate_positive_number(value):
    """양수인지 검증합니다."""
    return isinstance(value, (int, float)) and value > 0

2. 순수 함수 지향

# ✅ 순수 함수 (같은 입력 → 같은 출력, 부작용 없음)
def pure_add(a, b):
    """순수한 덧셈 함수"""
    return a + b

# ❌ 비순수 함수 (전역 상태에 의존)
global_counter = 0

def impure_add(a, b):
    global global_counter
    global_counter += 1  # 부작용
    return a + b + global_counter  # 외부 상태 의존

# ✅ 더 나은 방법
def add_with_counter(a, b, counter=0):
    """카운터와 함께 덧셈"""
    new_counter = counter + 1
    return a + b + new_counter, new_counter

3. 함수 이름 짓기 가이드

# ✅ 명확하고 서술적인 이름
def calculate_monthly_payment(principal, rate, months):
    """월 상환금 계산"""
    pass

def is_valid_email(email):
    """이메일 유효성 검사"""
    pass

def get_user_by_id(user_id):
    """ID로 사용자 조회"""
    pass

def format_phone_number(phone):
    """전화번호 포맷팅"""
    pass

# ❌ 모호하거나 너무 짧은 이름
def calc(p, r, m): pass  # 무엇을 계산하는지 불명확
def check(email): pass   # 무엇을 확인하는지 불명확
def process(data): pass  # 어떤 처리를 하는지 불명확

고급 함수 패턴

1. 함수 팩토리

def create_validator(min_length, max_length):
    """문자열 길이 검증 함수를 생성합니다."""
    def validator(text):
        return min_length <= len(text) <= max_length
    
    validator.min_length = min_length
    validator.max_length = max_length
    return validator

# 다양한 검증 함수 생성
username_validator = create_validator(3, 20)
password_validator = create_validator(8, 50)

print(username_validator("파이썬"))    # True
print(password_validator("123"))      # False

2. 부분 적용 (Partial Application)

from functools import partial

def multiply(a, b, c):
    """세 수의 곱"""
    return a * b * c

# 일부 인수를 고정한 새로운 함수 생성
double = partial(multiply, 2, 1)  # a=2, b=1로 고정
triple = partial(multiply, 3, 1)  # a=3, b=1로 고정

print(double(5))  # 2 * 1 * 5 = 10
print(triple(4))  # 3 * 1 * 4 = 12

# 실전 활용: 로그 함수
import datetime

def log_message(level, message, timestamp=None):
    """로그 메시지 출력"""
    if timestamp is None:
        timestamp = datetime.datetime.now()
    print(f"[{timestamp}] {level}: {message}")

# 로그 레벨별 함수 생성
log_info = partial(log_message, "INFO")
log_error = partial(log_message, "ERROR")
log_warning = partial(log_message, "WARNING")

log_info("프로그램 시작")
log_error("파일을 찾을 수 없습니다")
log_warning("메모리 사용량이 높습니다")

3. 커링 (Currying)

def curry_add(a):
    """커링된 덧셈 함수"""
    def add_b(b):
        def add_c(c):
            return a + b + c
        return add_c
    return add_b

# 단계별 함수 적용
add_5 = curry_add(5)
add_5_and_3 = add_5(3)
result = add_5_and_3(2)  # 5 + 3 + 2 = 10

print(result)  # 10

# 더 실용적인 커링 예제
def create_api_client(base_url):
    """API 클라이언트 생성"""
    def with_auth(auth_token):
        def make_request(endpoint, method="GET"):
            full_url = f"{base_url}/{endpoint}"
            headers = {"Authorization": f"Bearer {auth_token}"}
            return f"{method} {full_url} with headers {headers}"
        return make_request
    return with_auth

# 사용 예시
github_client = create_api_client("https://api.github.com")
authenticated_client = github_client("my_token_123")
response = authenticated_client("users/octocat", "GET")
print(response)

메타프로그래밍과 동적 함수 생성

1. getattr와 setattr 활용

class MathOperations:
    """수학 연산 클래스"""
    
    def add(self, a, b):
        return a + b
    
    def subtract(self, a, b):
        return a - b
    
    def multiply(self, a, b):
        return a * b

def create_operation_function(operation_name):
    """연산 함수를 동적으로 생성"""
    math_ops = MathOperations()
    
    def operation_function(a, b):
        operation = getattr(math_ops, operation_name)
        return operation(a, b)
    
    operation_function.__name__ = f"dynamic_{operation_name}"
    return operation_function

# 동적으로 함수 생성
dynamic_add = create_operation_function("add")
dynamic_multiply = create_operation_function("multiply")

print(dynamic_add(5, 3))      # 8
print(dynamic_multiply(4, 6)) # 24

2. exec와 eval 활용 (주의해서 사용)

아래는 코드에 직접 적은 표현식만 사용하는 학습 예제입니다. eval에 외부 입력을 전달하면 임의 코드가 실행될 수 있습니다. ast.literal_eval은 리터럴을 읽는 함수로, x**2 같은 일반 수식을 계산하는 대체재가 아닙니다. 정해진 연산만 필요하다면 앞의 계산기처럼 함수 목록을 사용하는 편이 명확합니다. Python 공식 문서

def create_math_function(expression_template):
    """수학 표현식 함수를 동적으로 생성"""
    def math_function(x):
        # 직접 작성한 예제 문자열만 사용합니다. 외부 입력을 받는 용도가 아닙니다.
        return eval(expression_template, {}, {"x": x})
    
    return math_function

# 다양한 수학 함수 생성
square_function = create_math_function("x**2")
cube_function = create_math_function("x**3")
linear_function = create_math_function("2*x + 1")

print(square_function(5))   # 25
print(cube_function(3))     # 27
print(linear_function(4))   # 9

함수형 프로그래밍 고급 패턴

1. 함수 합성

def compose(*functions):
    """여러 함수를 합성합니다."""
    def composed_function(x):
        result = x
        for func in reversed(functions):
            result = func(result)
        return result
    return composed_function

# 기본 함수들
def add_one(x):
    return x + 1

def multiply_by_two(x):
    return x * 2

def square(x):
    return x ** 2

# 함수 합성
complex_operation = compose(square, multiply_by_two, add_one)
result = complex_operation(3)  # ((3+1)*2)^2 = 64
print(result)  # 64

# 파이프라인 스타일
def pipe(value, *functions):
    """값을 여러 함수에 순차적으로 적용"""
    result = value
    for func in functions:
        result = func(result)
    return result

# 사용 예시
result = pipe(3, add_one, multiply_by_two, square)
print(result)  # 64

2. 함수 캐싱 (Memoization)

def memoize(func):
    """함수 결과를 캐시하는 데코레이터"""
    cache = {}
    
    def wrapper(*args, **kwargs):
        # 키 생성 (해시 가능한 인수만)
        key = (args, tuple(sorted(kwargs.items())))
        
        if key not in cache:
            cache[key] = func(*args, **kwargs)
            print(f"캐시 미스: {key}")
        else:
            print(f"캐시 히트: {key}")
        
        return cache[key]
    
    wrapper.cache = cache  # 캐시 접근 가능
    return wrapper

@memoize
def expensive_calculation(n):
    """비싼 계산 함수"""
    import time
    time.sleep(1)  # 1초 대기로 비싼 계산 시뮬레이션
    return n ** 2

# 첫 번째 호출: 캐시 미스
print(expensive_calculation(5))  # 1초 후 25

# 두 번째 호출: 캐시 히트 (즉시 반환)
print(expensive_calculation(5))  # 즉시 25

비동기 함수 기초

1. async/await 기본

import asyncio
import time

async def async_operation(name, delay):
    """비동기 작업 시뮬레이션"""
    print(f"{name} 시작")
    await asyncio.sleep(delay)  # 비동기 대기
    print(f"{name} 완료")
    return f"{name} 결과"

async def main():
    """메인 비동기 함수"""
    # 순차 실행
    start_time = time.time()
    result1 = await async_operation("작업1", 1)
    result2 = await async_operation("작업2", 1)
    sequential_time = time.time() - start_time
    
    print(f"순차 실행 시간: {sequential_time:.2f}초")
    
    # 동시 실행: 대기하는 동안 다른 코루틴이 진행합니다.
    start_time = time.time()
    results = await asyncio.gather(
        async_operation("작업3", 1),
        async_operation("작업4", 1),
        async_operation("작업5", 1)
    )
    concurrent_time = time.time() - start_time
    
    print(f"동시 실행 시간: {concurrent_time:.2f}초")
    print(f"결과: {results}")

# 실행 (Jupyter 노트북이 아닌 경우)
# asyncio.run(main())

🚀 실전 프로젝트: 간단한 웹 크롤러

함수를 활용한 모듈식 설계

아래는 함수 분리를 연습하는 크롤러 예제이며 requests 설치가 필요합니다. 링크 추출은 단순 정규식이므로 모든 HTML을 처리하지 못합니다. 상대 주소 결합에는 표준 라이브러리 urljoin을 사용합니다. Python URL 처리 문서

import requests
from typing import List, Dict, Optional
import time
from functools import wraps

def retry(max_attempts: int = 3, delay: float = 1.0):
    """재시도 데코레이터"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts - 1:
                        print(f"시도 {attempt + 1} 실패: {e}")
                        time.sleep(delay)
                    
            raise last_exception
        return wrapper
    return decorator

def validate_url(url: str) -> bool:
    """URL 유효성 검사"""
    return url.startswith(('http://', 'https://'))

@retry(max_attempts=3, delay=2.0)
def fetch_page(url: str, timeout: int = 10) -> Optional[str]:
    """웹 페이지 내용을 가져옵니다."""
    if not validate_url(url):
        raise ValueError(f"잘못된 URL: {url}")
    
    response = requests.get(url, timeout=timeout)
    response.raise_for_status()
    return response.text

def extract_links(html_content: str, base_url: str = "") -> List[str]:
    """HTML에서 링크를 추출합니다."""
    import re
    from urllib.parse import urljoin, urlsplit
    
    # 간단한 링크 추출 (실제로는 BeautifulSoup 사용 권장)
    link_pattern = r'href=["\']([^"\']*)["\']'
    links = re.findall(link_pattern, html_content)
    
    # 상대 URL을 절대 URL로 변환
    absolute_links = []
    for link in links:
        absolute_url = urljoin(base_url, link)
        if urlsplit(absolute_url).scheme in ('http', 'https'):
            absolute_links.append(absolute_url)
    
    return list(set(absolute_links))  # 중복 제거

def save_to_file(data: str, filename: str) -> bool:
    """데이터를 파일에 저장합니다."""
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(data)
        return True
    except Exception as e:
        print(f"파일 저장 실패: {e}")
        return False

def create_web_crawler(max_pages: int = 10, delay: float = 1.0):
    """웹 크롤러 함수를 생성합니다."""
    
    def crawl(start_url: str) -> Dict:
        """실제 크롤링을 수행합니다."""
        visited_urls = set()
        crawl_data = {
            'pages': [],
            'errors': [],
            'total_links': 0
        }
        
        urls_to_visit = [start_url]
        
        while urls_to_visit and len(crawl_data['pages']) < max_pages:
            current_url = urls_to_visit.pop(0)
            
            if current_url in visited_urls:
                continue
            
            try:
                print(f"크롤링 중: {current_url}")
                html_content = fetch_page(current_url)
                links = extract_links(html_content, current_url)
                
                page_data = {
                    'url': current_url,
                    'title': extract_title(html_content),
                    'links_count': len(links),
                    'content_length': len(html_content)
                }
                
                crawl_data['pages'].append(page_data)
                crawl_data['total_links'] += len(links)
                visited_urls.add(current_url)
                
                # 새로운 URL 추가 (도메인 제한)
                base_domain = extract_domain(start_url)
                for link in links:
                    if (extract_domain(link) == base_domain and 
                        link not in visited_urls and 
                        link not in urls_to_visit):
                        urls_to_visit.append(link)
                
                time.sleep(delay)  # 서버 부하 방지
                
            except Exception as e:
                error_info = {
                    'url': current_url,
                    'error': str(e)
                }
                crawl_data['errors'].append(error_info)
                print(f"오류: {current_url} - {e}")
        
        return crawl_data
    
    return crawl

def extract_title(html_content: str) -> str:
    """HTML에서 제목을 추출합니다."""
    import re
    title_match = re.search(r'<title[^>]*>([^<]*)</title>', html_content, re.IGNORECASE)
    return title_match.group(1).strip() if title_match else "제목 없음"

def extract_domain(url: str) -> str:
    """URL에서 도메인을 추출합니다."""
    import re
    domain_match = re.search(r'https?://([^/]+)', url)
    return domain_match.group(1) if domain_match else ""

# 사용 예시
def main():
    """메인 함수"""
    # 크롤러 생성 (최대 5페이지, 2초 간격)
    crawler = create_web_crawler(max_pages=5, delay=2.0)
    
    # 크롤링 실행
    results = crawler("https://example.com")
    
    # 결과 출력
    print(f"\n=== 크롤링 결과 ===")
    print(f"총 페이지: {len(results['pages'])}")
    print(f"총 링크: {results['total_links']}")
    print(f"오류 수: {len(results['errors'])}")
    
    for page in results['pages']:
        print(f"- {page['title']} ({page['url']})")
    
    # 결과를 JSON 파일로 저장
    import json
    json_data = json.dumps(results, ensure_ascii=False, indent=2)
    save_to_file(json_data, "crawl_results.json")

# 실행
# main()

다음에 학습할 내용

  • 클래스와 객체지향 프로그래밍 - 캡슐화, 상속, 다형성
  • 예외 처리와 디버깅 - try/except, 커스텀 예외, 로깅
  • 모듈과 패키지 관리 - import 시스템, pip, 가상환경
  • 파일 입출력과 데이터 처리 - CSV, JSON, 데이터베이스 연동
  • 정규표현식 - 패턴 매칭과 텍스트 처리

개인 학습 목표

  • 함수형 프로그래밍 패러다임 깊이 있게 이해하기
  • 비동기 프로그래밍과 병렬 처리 마스터하기
  • 클린 코드 원칙을 적용한 함수 설계 능력 향상
  • 실제 프로젝트에서 재사용 가능한 함수 라이브러리 구축

이 글은 함수 정의와 호출에서 시작해 람다, 클로저와 데코레이터까지 정리했습니다. 각 기능은 반복되는 로직을 묶고 함수의 동작을 조합하는 방법으로 살펴볼 수 있습니다.

이후 학습 주제로는 데이터와 동작을 함께 구조화하는 클래스와 객체지향 프로그래밍을 살펴볼 수 있습니다.


🔗 참고 자료:


yellow.log · 2025.08.11에 작성 · 2026.09.29에 수정

Python 기초 전체 3편

  1. 1.변수와 자료형 기초 가이드
  2. 2.List Comprehension 가이드
  3. 3.함수 정의와 활용 완전 가이드읽는 중