3.개발언어&라이브러리/Python

4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터

쿼드큐브 2025. 5. 22. 12:42
728x90

 

Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터

 

목차

1. 함수 구조

2. 함수의 인자(Arguments)

3. 전역변수와 지역변수

4. 람다함수(Lambda function)

5. 클로저(Closure)

6. 고차 함수(High-Order Function)

7. 함수 데코레이터(Decorator)

관련 글 링크

 

 

1. 함수 구조

Python에서 함수는 입력값을 받아 로직을 수행하고, 출력값을 반환하는 코드 블록입니다.
반복되는 코드를 줄이고, 코드를 모듈화하여 유지보수를 쉽게 해줍니다.

출처:https://unstop.com/blog/python-functions

▶ 함수 정의 문법

def 함수이름(매개변수1, 매개변수2, ...):
    """선택적으로 함수 설명"""
    실행할 코드
    return 결과값

 

함수 호출 방법

함수이름(인자1, 인자2)

 

함수 예시

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

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

함수는 반드시 정의한 뒤 호출해야 합니다.

같은 파일 내에서는 실행 시점에 따라 호출이 먼저 와도 오류는 발생하지 않지만, 가독성과 유지보수 측면에서 권장되지 않습니다.

 

2. 함수의 인자(Arguments)

함수에 외부에서 값을 전달할 때 사용되는 정보를 인자(argument)라고 하며,
함수를 정의할 때 괄호 ( ) 안에 쓰는 변수는 매개변수(parameter)라고 부릅니다.

유형 설명 저장형태
위치 인자 순서대로 전달 그대로
키워드 인자 변수명과 함께 전달 그대로
기본값 인자 값이 없으면 기본값 사용 값 생략 가능
*args 가변 길이 위치 인자 튜플 (tuple)
**kwargs 가변 길이 키워드 인자 딕셔너리 (dict)

 

위치 인자

인자를 순서대로 나열하여 전달하는 방식입니다.
함수를 호출할 때 정의된 순서와 동일하게 값을 넣어야 합니다.

순서가 맞지 않으면 예상치 못한 결과가 나올 수 있습니다.

def greet(name, age):
    print(f"{name}는 {age}살입니다.")

greet("Alice", 25)

#
Alice는 25살입니다.

 

▶ 키워드 인자

함수를 호출할 때 변수명을 명시하여 인자를 전달하는 방식입니다.
순서를 무시하고도 정확하게 값 전달이 가능합니다.

키워드 인자는 순서에 관계없이 인자의 의미를 명확하게 해주므로, 협업 시 실수를 줄일 수 있습니다.

greet(age=30, name="Bob")

#
Bob는 30살입니다.

 

▶ 기본값 인자

함수를 정의할 때 매개변수에 기본값을 지정해두면, 호출 시 해당 인자를 생략할 수 있습니다.

기본값을 지정한 매개변수는 항상 뒤쪽에 위치해야 합니다.
예: def func(a, b=10) ✅ 정상

     def func(a=10, b) ❌ 오류

def greet(name="Guest"):
    print(f"Hello, {name}")

greet()          # 기본값 사용
greet("David")   # 전달된 값 사용

#
Hello, Guest
Hello, David

 

▶ 가변 인자(*args)

인자의 개수가 정해지지 않았을 때 사용합니다.
여러 개의 값을 튜플(tuple)로 받아 처리할 수 있습니다.

def total(*nums):
    print("전달된 값들:", nums)
    return sum(nums)

print(total(1, 2, 3, 4))

#
전달된 값들: (1, 2, 3, 4)
10

 

함수에 인자를 여러 개 전달할 수 있고, 내부에서는 for문 등을 통해 순회할 수 있습니다.

def print_scores(*scores):
    print("학생 점수 목록:")
    for idx, score in enumerate(scores, start=1):
        print(f"{idx}번 학생: {score}점")

# 함수 호출
print_scores(85, 92, 78, 100)

 

▶ 키워드 가변 인자(**kwargs)

이름이 있는 인자들을 딕셔너리 형태로 받아 처리할 수 있습니다.
보통 설정값이나 옵션 처리를 위해 많이 사용됩니다.

kwargs는 dictionary 타입이므로, .items()를 사용해 키-값 쌍을 순회할 수 있습니다.

def config(**options):
    for key, value in options.items():
        print(f"{key} = {value}")

config(theme="dark", fontsize=12, autosave=True)

#
theme = dark
fontsize = 12
autosave = True

 

▶ *args + **kwargs 함께 쓰기

함수 정의 시 *args와 **kwargs를 함께 사용할 수 있으며,
순서는 항상 *args → **kwargs여야 합니다.

def show_info(title, *args, **kwargs):
    print("제목:", title)
    print("위치 인자:", args)
    print("키워드 인자:", kwargs)

show_info("로그인 정보", "admin", "user", level=5, active=True)

#
제목: 로그인 정보
위치 인자: ('admin', 'user')
키워드 인자: {'level': 5, 'active': True}

 

3. 전역변수와 지역변수

Python 변수는 정의 위치에 따라 범위가 달라집니다.

구분 정의 위치 사용 범위
전역변수 함수 외부 파일 전체에서 사용
지역변수 함수 내부 함수 내부에서만 사용
▶ 전역 vs 지역 변수
x = 10  # 전역변수

def example():
    y = 5  # 지역변수
    print("내부:", x + y)

example()
print("외부:", x)
# print(y)  # ❌ 오류: y는 함수 외부에서 접근 불가
▶ global 키워드 사용
def set_global():
    global g
    g = "전역 설정됨"

set_global()
print(g)  # 전역 설정됨
  • global g로 선언된 변수는 함수 외부에서도 사용할 수 있는 전역 변수
  • 함수 안에서 선언했지만, 외부에서도 접근 가능

▶ 전역변수 없이 지역변수만 수정하려고 할 때 발생하는 문제

x = 5

def change_x():
    x = x + 1  # ❌ 오류: 지역 변수 참조 전에 사용됨

change_x()

함수 내부에서 x를 지역 변수로 취급하면서 동시에 수정하려고 했기 때문에 오류 발생 : UnboundLocalError 오류

 

전역변수 수정 예시: 정상 처리

x = 5

def change_x():
    global x
    x = x + 1

change_x()
print(x)  # 6

 

 

4. 람다함수(Lambda function)

람다(lambda) 함수는 간단한 연산을 한 줄로 정의할 수 있는 익명 함수입니다.
def를 사용하여 함수를 만들지 않고도, 즉석에서 함수를 정의하고 변수에 할당하거나 인자로 전달할 수 있습니다.

이러한 람다 함수는 map(), filter(), sorted() 같은 고차 함수(high-order function)와 함께 자주 사용됩니다

 

기본 문법

lambda 인자1, 인자2, ... : 표현식
  • lambda는 함수 정의 키워드입니다.
  • : 앞에는 인자(argument)를, 뒤에는 반환할 표현식(expression)을 작성합니다.
  • 여러 줄 로직이나 if 문, 반복문은 사용할 수 없습니다.
  • 람다 함수는 한 줄짜리 표현식만 사용할 수 있으며, return 키워드는 생략합니다.

일반함수 vs 람다 함수 

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

# 람다 함수 정의
square_lambda = lambda x: x * x

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

 

▶ 인자가 여러 개인 람다 함수

add = lambda a, b: a + b
print(add(3, 7))  # 10

 

▶ 조건문을 포함한 람다 함수 (삼항 연산자 사용)

check_even = lambda x: "짝수" if x % 2 == 0 else "홀수"
print(check_even(4))  # 짝수
print(check_even(7))  # 홀수

 

 

5. 클로저(Closure)

클로저(Closure)는 내부 함수가 외부 함수의 지역 변수에 접근하고, 그 상태를 유지하는 함수입니다.
즉, 내부 함수가 외부 함수의 실행 컨텍스트(변수)를 기억한 채로 나중에 호출되더라도 그 값을 사용할 수 있는 구조입니다.

이 개념은 특히 다음과 같은 경우에 유용하게 사용됩니다.

  • 함수 실행 후에도 특정 상태를 기억하고 있어야 할 때
  • 상태를 은닉하고 캡슐화하려 할 때
  • 데코레이터나 이벤트 핸들러 등에서 상태 기반의 동작 제어가 필요할 때

▶ 기본 구조

def outer(외부변수):
    def inner():
        # 외부변수 사용 가능
        print(외부변수)
    return inner

inner() 함수는 outer() 내부에 정의되어 있지만, outer()가 실행된 후에도 그 안의 msg 값을 계속 기억합니다.

 

▶ 외부 변수 접근

def outer(msg):
    def inner():
        print(f"메시지: {msg}")
    return inner

f = outer("안녕하세요")
f()  # 메시지: 안녕하세요
  • outer() 함수는 "안녕하세요"라는 값을 가진 msg를 갖고 있습니다.
  • inner()는 msg를 참조하지만, outer()의 실행이 끝난 후에도 msg를 기억하고 있어 출력이 가능합니다.

 

여러 클로저 인스턴스 만들기

def make_greeting(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

hello = make_greeting("Hello")
hi = make_greeting("Hi")

print(hello("Alice"))  # Hello, Alice!
print(hi("Bob"))       # Hi, Bob!
  • hello, hi는 서로 다른 greeting 상태를 기억하는 클로저입니다.
  • 각각 "Hello", "Hi"라는 값을 내부 함수가 유지하고 있습니다.

▶ 상태를 유지하는 클로저 (Counter)

def counter():
    count = 0
    def increment():
        nonlocal count  # 외부 변수 count를 수정하려면 nonlocal 필요
        count += 1
        return count
    return increment

c = counter()
print(c())  # 1
print(c())  # 2

d = counter()
print(d())  # 1 (새 인스턴스는 count를 0부터 시작)
  • c()는 내부적으로 count 값을 유지하고, 호출할 때마다 1씩 증가합니다.
  • nonlocal 키워드는 외부 함수의 지역변수를 수정하기 위해 필요합니다.
  • 새로운 클로저 d()는 자체적으로 count를 갖고 독립적인 상태를 유지합니다.

클러저와 일반함수 차이

def regular_counter():
    count = 0
    count += 1
    return count

print(regular_counter())  # 항상 1 반환
print(regular_counter())  # 또 1 반환
  • regular_counter()는 호출할 때마다 count를 새로 선언하므로 상태가 유지되지 않음
  • 클로저는 한 번 생성된 함수 안에서 상태를 누적/유지 가능

▶ 클로저의 유용한 활용 예시

def decorator(func):
    def wrapper():
        print("함수 호출 전 작업")
        func()
        print("함수 호출 후 작업")
    return wrapper

@decorator
def say_hello():
    print("Hello!")

say_hello()
  • wrapper()는 내부에서 func()를 호출하며, func는 decorator의 인자로 들어온 함수입니다.
  • 이 때 wrapper는 func를 기억하는 클로저 구조입니다.

▶ 클로저를 사용해야 하는 상황 예시

상황 설명
함수 호출 횟수 누적 내부 상태를 유지하며 카운트 가능
사용자별 맞춤 메시지 구성 각 사용자마다 greeting 상태 유지 가능
데코레이터 구현 시 함수 저장 필요 대상 함수 기억 후 래핑 가능
외부 접근 없이 상태 은닉 필요 함수 안에만 변수를 두어 외부에서 접근 차단 가능

 

 

6. 고차함수(High-Order Function)

고차 함수(High-Order Function)란 다음 조건 중 하나 이상을 만족하는 함수를 말합니다.

  1. 다른 함수를 인자로 받는 함수
  2. 다른 함수를 반환하는 함수

즉, 함수도 일급 객체로 취급되는 Python에서는 함수를 인자처럼 넘기거나, 함수 내부에서 새 함수를 생성해 반환할 수 있습니다.

특징 설명
함수는 값처럼 전달됨 인자로 전달하거나, 반환값으로 리턴 가능
함수 조합 가능 여러 함수를 조합하여 유연하고 확장성 높은 로직 작성 가능
함수형 프로그래밍 지원 map, filter, reduce 등과 결합하여 선언형 코딩 가능
  • 함수를 값으로 전달하므로, 전달하는 함수의 형태나 리턴값에 주의해야 함
  • 가독성이 떨어질 수 있으므로 람다 중첩 사용은 자제
  • 디버깅이나 로깅 시, 함수 객체를 전달하기 때문에 추적이 어려울 수 있음

 

▶ 함수를 인자로 받는 고차 함수

apply_func는 다른 함수를 인자로 받아 실행하므로 고차 함수입니다.

def apply_func(f, x):
    return f(x)

# 제곱 함수 정의
def square(n):
    return n * n

result = apply_func(square, 5)
print(result)  # 25

 

▶ 함수를 반환하는 고차 함수 (클로저 포함)

make_multiplier()는 함수를 반환하는 고차 함수이며, 내부에서 n 값을 기억하는 클로저 구조입니다.

def make_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

double = make_multiplier(2)
print(double(5))  # 10

triple = make_multiplier(3)
print(triple(5))  # 15

 

▶ map(), filter(), reduce()는 모두 고차 함수

# map(function, iterable)
nums = [1, 2, 3]
squared = list(map(lambda x: x ** 2, nums))
print(squared)  # [1, 4, 9]

# filter(function, iterable)
even = list(filter(lambda x: x % 2 == 0, nums))
print(even)  # [2]

# reduce(function, iterable) – functools 필요
from functools import reduce

total = reduce(lambda x, y: x + y, nums)
print(total)  # 6

 

▶ sorted()의 key는 고차 함수 활용 사례

sorted()는 내부적으로 key 인자로 함수를 받아 처리하므로 고차 함수입니다.

words = ["apple", "banana", "kiwi", "grape"]
sorted_words = sorted(words, key=lambda x: len(x))
print(sorted_words)  # ['kiwi', 'apple', 'grape', 'banana']

 

 

7. 함수 데코레이터(Decorator)

데코레이터(Decorator)는 함수를 인자로 받아, 기능을 확장한 새로운 함수를 반환하는 함수입니다.
기존 함수를 수정하지 않고도 코드 전후에 기능을 추가하거나, 반복적인 처리를 공통화할 때 매우 유용합니다.

☞ 데코레이터는 클로저(Closure) 구조를 기반으로 동작합니다.

 

▶ 기본 문법

def 데코레이터이름(func):       # 함수를 인자로 받음
    def wrapper():              # 내부에서 감싼 함수
        # 전처리 코드
        func()                  # 원래 함수 실행
        # 후처리 코드
    return wrapper              # 감싼 함수 반환

@데코레이터이름
def 원래_함수():
    ...

@데코레이터이름은 원래_함수 = 데코레이터이름(원래_함수) 와 같은 의미입니다.

 

▶ 실행 전후 메시지 추가

def logger(func):
    def wrapper():
        print("함수 실행 전")
        func()
        print("함수 실행 후")
    return wrapper

@logger
def say_hello():
    print("Hello!")

say_hello()
'''
함수 실행 전
Hello!
함수 실행 후
'''
  1. logger(func) 라는 이름의 데코레이터 함수가 정의됨. 이 함수는 다른 함수를 인자로 받아 내부에서 wrapper() 함수를 감싸서 반환함
  2. wrapper()는 func()를 실행하기 전과 후에 메시지를 출력하는 함수로 정의됨
  3. logger()는 감싼 함수 wrapper를 반환함
  4. @logger는 say_hello = logger(say_hello) 와 같은 의미임. 즉, say_hello()를 실행하면 원래 함수가 아니라 wrapper()가 호출됨
  5. say_hello() 호출 시 → 실제로는 wrapper() 실행 → 내부에서 print, func(), print 순서로 실행됨

▶ @logger가 하는 일

@logger
def say_hello():
    print("Hello!")

say_hello()

이 코드는 다음과 동일한 의미입니다:

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

say_hello = logger(say_hello)
  1. say_hello 함수 자체를 logger()에 넣는다
  2. logger()는 이 함수를 받아서 내부에 wrapper() 함수를 만든다
  3. 그 wrapper()를 다시 say_hello에 덮어쓴다

바뀐 이후 say_hello는?

  • 이제 say_hello는 더 이상 print("Hello!")만 실행하는 함수가 아닙니다
  • 새로운 함수(wrapper)를 가리키는 변수입니다.
def wrapper():
    print("함수 실행 전")
    say_hello()  # 이건 원래 함수가 아님. 이 라인 없고 내부에서 func() 호출됨
    print("함수 실행 후")

그래서 say_hello()를 실행하면:

  1. wrapper()가 실행되고
  2. 그 안에서 원래 say_hello() (즉, func())가 호출됨
  3. 결과적으로 "Hello!" 출력은 여전히 실행되지만
  4. 전후로 추가된 print도 같이 실행됨

▶ 인자와 반환값 처리

def trace(func):
    def wrapper(*args, **kwargs):
        print(f"[trace] 호출된 함수: {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[trace] 반환값: {result}")
        return result
    return wrapper

@trace
def add(x, y):
    return x + y

add(3, 5)
'''
[trace] 호출된 함수: add((3, 5), {})
[trace] 반환값: 8
'''

 

▶ 중첩 데코레이터

def bold(func):
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def say():
    return "Hello"

print(say())  # <b><i>Hello</i></b>

 

▶ 실행 시간 측정

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"실행 시간: {end - start:.5f}초")
        return result
    return wrapper

@timer
def long_task():
    time.sleep(1)
    return "완료"

long_task()
'''
실행 시간: 1.00123초
'''

 

functools.wraps를 사용하는 이유

  • @wraps(func)는 wrapper() 함수가 func의 이름, docstring, metadata를 그대로 유지하게 해줍니다.
  • 없으면 __name__, __doc__ 등이 wrapper로 바뀌어 디버깅이나 문서화에 불편합니다.

 


 

 

 

관련 글 링크

2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수

 

2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수

Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수 목차 1. 지역변수 vs 전역변수 2. global 키워드의 역할과 주의사항 3. nonlocal 키워드 4. 변수처럼 다루는 함수-일급객체로서의 함수 5. 클래스

quadcube.tistory.com

 

728x90