[파이썬 코딩의 기술] - 3장 함수

2025. 2. 14. 21:33·리뷰/책

3-19 함수가 여러 값을 반환하는 경우 절대로 네 값 이상을 언패킹하지 마라

  • 함수에서 여러 개의 값을 반환하는 방법
    • 튜플을 사용하여 여러 값을 반환하고, 호출하는 쪽에서 언패킹하여 사용할 수 있다.
def get_stats(numbers):
    minimum = min(numbers)
    maximum = max(numbers)
    return minimum, maximum

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

minimum, maximum = get_stats(length)

print(f'min: {minimum}, max: {maximum}')
# 출력: min: 10, max: 50
  • return minimum, maximum에서 튜플을 반환하며, 이를 언패킹하여 minimum, maximum 변수에 저장한다.

언패킹을 활용한 함수 반환

  • 두 개의 값 반환 예제
def my_function():
    return 1, 2

first, second = my_function()
assert first == 1
assert second == 2
  • 함수가 두 개의 값을 반환하면 언패킹을 통해 변수에 쉽게 할당할 수 있다.
  • 여러 개의 값을 반환하면서 일부를 리스트로 처리
def get_avg_ratio(numbers):
    average = sum(numbers) / len(numbers)
    scaled = [x / average for x in numbers]
    scaled.sort(reverse=True)
    return scaled

longest, *middle, shortest = get_avg_ratio(length)

print(f'최대 길이: {longest:>4.0%}')
print(f'최소 길이: {shortest:>4.0%}')
  • middle을 사용하여 중간 값을 리스트로 묶어서 관리할 수 있다.

네 개 이상의 변수를 언패킹하지 말아야 하는 이유

  • 변수 개수가 많아지면 실수할 가능성이 커진다.
    • 언패킹할 변수가 많으면 가독성이 떨어지고, 실수하기 쉽다.
a, b, c, d, e = some_function()  # 변수 개수가 많아 관리하기 어려움
  • 대안: namedtuple이나 작은 클래스 반환

namedtuple을 사용한 방법

from collections import namedtuple

Stats = namedtuple('Stats', ['min', 'max'])

def get_stats(numbers):
    return Stats(min(numbers), max(numbers))

stats = get_stats(length)
print(stats.min, stats.max)
# 출력: 10 50
  • namedtuple을 사용하면 반환 값이 의미를 가지므로 가독성이 향상된다.

작은 클래스를 사용하는 방법

class Stats:
    def __init__(self, minimum, maximum):
        self.minimum = minimum
        self.maximum = maximum

def get_stats(numbers):
    return Stats(min(numbers), max(numbers))

stats = get_stats(length)
print(stats.minimum, stats.maximum)
# 출력: 10 50
  • 작은 클래스를 사용하면 추가적인 메서드를 정의할 수도 있고, 더 확장 가능하다.

기억해야 할 내용

  • 여러 값을 반환할 때는 튜플을 사용할 수 있으며, 언패킹을 통해 개별 변수에 할당할 수 있다.
  • 반환 값이 많을 경우 `` 연산자를 사용하여 일부 값을 리스트로 받을 수도 있다.
  • 네 개 이상의 변수를 언패킹하는 것은 실수하기 쉬우므로 피해야 한다.
  • 대신 namedtuple이나 작은 클래스를 사용하여 반환하는 것이 더 나은 방법이다.



3-20 None을 반환하기보다는 예외를 발생시켜라

  • None을 반환하면, False와 동등하게 평가되므로 코드에서 의도치 않은 오류를 일으킬 수 있다.

  • 특히, None이 특별한 의미를 가지는 경우, 0, 빈 문자열(""), False 같은 값과 혼동될 위험이 있다.


    None을 반환하는 방식의 문제점

      def careful_divide(a, b):
          try:
              return a / b
          except ZeroDivisionError:
              return None  # 잘못된 경우 None 반환
    
      x, y = 0, 5
      result = careful_divide(x, y)
    
      if not result:  # result가 0이면 False로 평가됨 → 버그 발생 가능
          print("잘못된 입력")  # 의도치 않게 실행될 수 있음
    
    • result = 0일 경우 if not result:가 True로 평가되어 잘못된 입력으로 간주하는 문제가 발생할 수 있다.

    해결 방법 1: 튜플을 사용하여 명확한 반환 구조 만들기

      def careful_divide(a, b):
          try:
              return True, a / b
          except ZeroDivisionError:
              return False, None  # 오류 여부를 명확히 전달
    
      success, result = careful_divide(10, 0)
    
      if success:
          print(f"결과: {result}")
      else:
          print("잘못된 입력")
    • 튜플((성공 여부, 결과 값))을 반환하면 오류 여부를 명확히 전달할 수 있다.
    • 하지만, 함수 호출자가 튜플을 항상 언패킹해서 처리해야 하는 번거로움이 있다.

    해결 방법 2: 예외를 발생시키기 (추천)

      def careful_divide(a, b):
          if b == 0:
              raise ValueError("잘못된 입력: 0으로 나눌 수 없음")
          return a / b
    
      try:
          result = careful_divide(10, 0)
      except ValueError as e:
          print(f"오류 발생: {e}")
    
    • 예외(ValueError)를 발생시키면, 오류가 발생한 곳에서 바로 감지하고 처리할 수 있다.
    • 호출하는 쪽에서 try-except를 통해 오류를 적절히 다룰 수 있다.
    • 불필요한 None 반환을 제거하여 코드의 명확성을 높인다.

    타입 애너테이션을 사용한 None 반환 방지

      def careful_divide(a: float, b: float) -> float:
          if b == 0:
              raise ValueError("잘못된 입력: 0으로 나눌 수 없음")
          return a / b
    
    • > float 명시로 None이 반환되지 않음을 보장할 수 있다.
    • 정적 분석 도구(mypy)를 활용하면 None 반환을 방지하는 데 도움을 받을 수 있다.

    기억해야 할 내용

    • None을 반환하면 False와 혼동될 수 있어 의도치 않은 오류를 유발할 수 있다.
    • 특별한 상황을 표현할 때는 None을 반환하는 대신 예외(Exception)를 발생시켜야 한다.
    • 예외를 발생시키면 함수 호출자가 오류를 명확하게 처리할 수 있다.
    • 타입 애너테이션을 사용하면 함수가 None을 반환하지 않는다는 점을 명확히 할 수 있다.




3-21 변수 영역과 클로저의 상호작용 방식을 이해하라

  • 특정 숫자 그룹을 우선 배치하는 정렬 함수 구현

  • 클로저(Closure)를 활용하여 정렬 기준을 설정

  • 변수 영역(Scope) 문제 해결하기


    1. 클로저를 사용한 정렬 예제

      def sort_priority(values, group):
          def helper(x):  # 정렬 기준을 정하는 내부 함수
              if x in group:
                  return (0, x)  # 우선순위 그룹에 속하면 (0, x)
              return (1, x)  # 그렇지 않으면 (1, x)
    
          values.sort(key=helper)
    
      numbers = [8, 3, 1, 2, 5, 4, 7, 6]
      group = {2, 3, 5, 7}
    
      sort_priority(numbers, group)
      print(numbers)
      # 출력: [2, 3, 5, 7, 1, 4, 6, 8]  (우선순위 그룹이 앞쪽으로 정렬됨)
    
    • 이 코드가 정상 작동하는 이유
      1. 파이썬의 클로저 지원: 내부 함수 helper()가 sort_priority()의 group에 접근 가능.
      2. 일급 함수(First-Class Function): 파이썬에서는 함수를 변수에 저장하고, 인자로 전달 가능.
      3. 튜플 정렬 규칙: (0, x)와 (1, x)를 반환하면, 0이 앞에 배치되고 x 기준으로 정렬됨.

    2. 클로저 내부 변수 문제

    • 우선순위 숫자가 존재하는지 확인하는 found 변수를 추가

      def sort_priority2(numbers, group):
        found = False  # 우선순위 숫자가 존재하는지 확인하는 변수
      
        def helper(x):
            if x in group:
                found = True  # 우선순위 숫자를 찾으면 True로 설정
                return (0, x)
            return (1, x)
      
        numbers.sort(key=helper)
        return found
      
      found = sort_priority2(numbers, group)
      print('발견:', found)  # 기대값: True
      print(numbers)
      
      # 결과
      발견: False  (틀린 값)
      [2, 3, 5, 7, 1, 4, 6, 8]  (정렬은 정상적으로 수행됨)
    • 문제점: found = True가 실행되었음에도 found 값이 False로 유지됨.

    • 이유: helper() 함수 내부에서 found = True를 할당하면, 파이썬은 found를 새로운 지역 변수로 간주한다.

      • 따라서 외부의 found 변수에는 영향을 주지 않음.

    3. nonlocal을 사용하여 변수 수정

      def sort_priority3(numbers, group):
          found = False  # 우선순위 숫자 존재 여부
    
          def helper(x):
              nonlocal found  # 외부 함수의 found 변수를 수정하도록 명시
              if x in group:
                  found = True
                  return (0, x)
              return (1, x)
    
          numbers.sort(key=helper)
          return found
    
      found = sort_priority3(numbers, group)
      print('발견:', found)  # 올바르게 True 출력됨
      print(numbers)
    

    결과:

      발견: True  (올바른 값)
      [2, 3, 5, 7, 1, 4, 6, 8]  (정렬도 정상 작동)
    
    • nonlocal found를 추가하면 내부 함수에서 감싸는 함수의 found 변수를 수정할 수 있다.

    4. 변수 영역(Scope)의 동작 원리

    1. 현재 함수 내부에서 변수를 찾는다.

    2. 현재 함수를 감싸는 함수의 변수 영역을 찾는다.

    3. 현재 코드가 포함된 모듈(전역 변수)에서 찾는다.

    4. 파이썬 내장 영역에서 찾는다.

      nonlocal을 사용하면 지역 변수(helper() 내부)가 아닌 외부 함수(sort_priority3())의 변수를 수정할 수 있다.


    기억해야 할 내용

    • 클로저 함수는 자신이 정의된 함수의 변수도 참조할 수 있다.
    • 클로저 내부에서 변수를 할당하면 새로운 지역 변수가 생성되므로, 감싸는 함수의 변수를 수정하려면 nonlocal을 사용해야 한다.
    • 하지만, nonlocal을 사용하면 코드가 복잡해질 수 있으므로 간단한 함수가 아닐 경우 사용을 지양해야 한다.




3-22 변수 위치 인자를 사용해 시각적인 잡음을 줄여라

  • 가변 위치 인자(*args)를 사용하면 함수 호출이 더 깔끔해지고 시각적 잡음이 줄어든다.

  • 일반적인 방식 vs *args 사용 방식 비교

    • *args 예시

        # 일반적인 함수
        def add(a, b):
            return a + b
      
        # *args를 사용한 함수
        def add_many(*args):
            return sum(args)
      
        # 사용 예시
        print(add(1, 2,)        # 3 , 만약에 add(1, 2, 3)하면 오류남
        print(add_many(1, 2))   # 3
        print(add_many(1, 2, 3, 4, 5))  # 15

1. 고정된 인자 개수를 사용하는 방식

def log(message, values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print(f'{message}: {values_str}')

log('내 숫자는', [1, 2])
log('안녕', [])

출력:

내 숫자는: 1, 2
안녕:
  • values가 리스트이므로 빈 리스트라도 항상 전달해야 함 → 불필요한 시각적 잡음 발생

2. *args를 사용한 가변 인자 방식 (추천)

def log(message, *values):  # values가 가변 인자가 됨
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print(f'{message}: {values_str}')

log('내 숫자는', 1, 2)  # 가변 인자로 숫자 전달
log('안녕')  # 두 번째 인자 생략 가능

출력:

내 숫자는: 1, 2
안녕
  • *args 덕분에 필요한 인자만 입력하면 되고, 불필요한 리스트 감싸기를 없앨 수 있다.

3. 시퀀스를 *args에 전달하는 방법 (``* 연산자 활용)

favorites = [7, 3, 9]
log('선호하는 숫자는', *favorites)

출력:

선호하는 숫자는: 7, 3, 9
  • favorites를 사용하면 리스트의 원소가 개별 인자로 전달된다.
  • 리스트를 직접 넘기는 것보다 직관적이고 깔끔함.

4. *args 사용 시 주의할 점

(1) *args는 항상 튜플로 변환됨

def example(*args):
    print(args)  # args는 튜플

example(1, 2, 3)  # 출력: (1, 2, 3)
  • 가변 인자는 자동으로 튜플로 변환되므로, 수정할 수 없다.

(2) 제너레이터에 *`` 연산자를 사용하면 메모리 초과 위험

def generator():
    for i in range(10**6):  # 100만 개 생성
        yield i

# * 연산자 사용 시 주의
log('큰 숫자 목록', *generator())  # ❌ 메모리 초과 위험!
  • generator()를 사용하면 모든 값이 튜플에 저장되므로 메모리 과부하 발생 가능
  • 해결 방법: ``를 사용하지 않고, 직접 순회하면서 처리해야 한다.

(3) *args를 받는 함수에 새로운 위치 인자를 추가할 경우 주의

def func(*args):
    print(args)

func(1, 2, 3)  # 출력: (1, 2, 3)

def func_with_new_arg(x, *args):
    print(x, args)

func_with_new_arg(1, 2, 3)  # 출력: 1 (2, 3)
  • *args 앞에 새로운 위치 인자를 추가하면 기존 함수 호출 방식이 깨질 수 있음.
  • 기존 코드와 호환성을 유지하려면 신중하게 추가해야 함.

기억해야 할 내용

  • *args를 사용하면 함수가 가변 위치 인자를 받을 수 있어 코드가 깔끔해진다.

  • * 연산자를 사용하면 시퀀스의 원소를 개별 인자로 전달할 수 있다.

  • 제너레이터에 ``* 연산자를 사용하면 메모리를 과도하게 사용할 위험이 있으므로 주의해야 한다.

  • *args를 받는 함수에 새로운 위치 인자를 추가하면 예상치 못한 버그가 발생할 수 있다.

  • 인자(매개변수)

    함수와 인자 간단 정리

    1. 기본 인자 (Default Arguments)

      def greet(name, message="안녕하세요"):  # message는 기본값 있음
          print(f"{message}, {name}님!")
    
      # 사용 예시
      greet("철수")              # "안녕하세요, 철수님!"
      greet("철수", "좋은 아침")  # "좋은 아침, 철수님!"
    

    2. 위치 인자 (Positional Arguments)

      def introduce(name, age):
          print(f"이름: {name}, 나이: {age}")
    
      # 사용 예시
      introduce("철수", 20)  # 순서대로 들어감
    

    3. 키워드 인자 (Keyword Arguments)

      def introduce(name, age):
          print(f"이름: {name}, 나이: {age}")
    
      # 사용 예시
      introduce(name="철수", age=20)  # 이름 지정
      introduce(age=20, name="철수")  # 순서 바꿔도 됨
    

    4. 가변 인자 (*args)

      def print_names(*names):  # 여러 개의 값을 받을 수 있음
          for name in names:
              print(name)
    
      # 사용 예시
      print_names("철수")                    # 하나만 전달
      print_names("철수", "영희", "민수")     # 여러 개 전달
    

    5. 키워드 가변 인자 (kwargs)**

      def print_info(**info):  # 이름=값 형태로 여러 개 받음
          for key, value in info.items():
              print(f"{key}: {value}")
    
      # 사용 예시
      print_info(name="철수", age=20, city="서울")
    

    인자 사용 시 주의사항

    1. 위치 인자는 순서가 중요함
    2. 키워드 인자는 순서 상관없음
    3. 기본 인자는 선택적으로 사용 가능
    4. args는 여러 개의 위치 인자를 튜플로 받음
    5. *kwargs는 여러 개의 키워드 인자를 딕셔너리로 받음

    실제 활용 예시

      def user_profile(name,                # 필수 위치 인자
                      age,                  # 필수 위치 인자
                      location="서울",       # 기본 인자
                      *hobbies,            # 가변 인자
                      **additional_info):   # 키워드 가변 인자
          print(f"이름: {name}")
          print(f"나이: {age}")
          print(f"위치: {location}")
          print("취미:", ", ".join(hobbies))
          print("추가정보:")
          for key, value in additional_info.items():
              print(f"- {key}: {value}")
    
      # 함수 호출
      user_profile("철수",
                  20,
                  "부산",
                  "축구", "게임", "독서",
                  직업="학생",
                  이메일="chul@example.com")
    




3-23 키워드 인자로 선택적인 기능을 제공하라

기억해야 할 내용

  • 함수 인자는 위치 인자 또는 키워드 인자로 지정할 수 있다.
  • 키워드 인자를 사용하면 여러 인자의 목적을 명확히 할 수 있어 가독성이 향상된다.
  • 키워드 인자와 디폴트 값을 함께 사용하면 기존 코드 변경 없이 새로운 기능을 쉽게 추가할 수 있다.
  • 선택적 키워드 인자는 항상 키워드를 사용해 전달해야 한다.

예제 코드

# 딕셔너리 준비
user_info = {
    "name": "철수",
    "age": 20,
    "city": "서울"
}

# 방법 1: 키워드 인자로 직접 전달
def introduce(name, age, city):
    print(f"{name}는 {age}살이고 {city}에 삽니다")

introduce(age=20, name="철수", city="서울")

# 방법 2: ** 연산자로 딕셔너리 풀어서 전달
introduce(**user_info)  # 위와 동일한 결과

# 결과
# 철수는 20살이고 서울에 삽니다
# 철수는 20살이고 서울에 삽니다

이 방법을 활용하면 기본값을 추가하여 새로운 기능을 쉽게 확장할 수도 있다.

def introduce(name, age, city="알 수 없음"):
    print(f"{name}는 {age}살이고 {city}에 삽니다")

introduce(name="영희", age=25)  # city 기본값 사용

# 결과
# 영희는 25살이고 알 수 없음에 삽니다



3-24 None과 독스트링을 사용해 동적인 디폴트 인자를 지정하라

  • 디폴트 인자는 함수 정의 시 한 번만 평가되므로, 동적인 값이 필요할 때 문제가 발생할 수 있다.
  • 해결 방법: None을 기본값으로 설정하고, 함수 내에서 동적 값을 직접 할당하는 방식 사용

1. 잘못된 예제: 디폴트 인자로 datetime.now() 사용

from time import sleep
from datetime import datetime

def log(message, when=datetime.now()):  # 잘못된 방법
    print(f'{when}: {message}')

log('안녕')
sleep(0.1)
log('다시 안녕')

출력:

2025-01-31 12:00:00.000000: 안녕
2025-01-31 12:00:00.000000: 다시 안녕
  • 문제점: datetime.now()가 함수 정의 시점에 한 번만 실행되므로, 이후 함수 호출에서도 변경되지 않는 동일한 값이 사용됨.

2. 올바른 예제: None을 사용하여 동적 값 설정

def log(message, when=None):
    """메시지와 타임스탬프를 로그에 남긴다.

    Args:
        message: 출력할 메시지.
        when: 메시지가 발생한 시각(datetime).
              디폴트 값은 현재 시간이다.
    """
    if when is None:  # 호출 시점마다 새로운 시간 할당
        when = datetime.now()
    print(f'{when}: {message}')

log('안녕')
sleep(0.1)
log('다시 안녕')

출력:

2025-01-31 12:00:00.000000: 안녕
2025-01-31 12:00:00.100000: 다시 안녕
  • None을 기본값으로 설정한 후, 함수 내부에서 datetime.now()를 호출하여 새로운 시간을 할당.
  • 이제 함수 호출 시마다 올바른 시간 값이 사용됨.

3. 타입 애너테이션을 활용한 예제

from typing import Optional

def log(message: str, when: Optional[datetime] = None) -> None:
    """메시지와 타임스탬프를 로그에 남긴다.

    Args:
        message (str): 출력할 메시지.
        when (Optional[datetime]): 메시지가 발생한 시각.
            기본값은 현재 시간(datetime.now())이다.
    """
    if when is None:
        when = datetime.now()
    print(f'{when}: {message}')
  • Optional[datetime]을 사용하여 None을 기본값으로 설정할 수 있음을 명시
  • 정적 분석 도구(mypy 등)에서 더 정확한 검사를 수행할 수 있음

기억해야 할 내용

  • 디폴트 인자는 함수 정의 시점에 한 번만 평가되므로, datetime.now(), {}, [] 같은 동적인 값은 잘못된 동작을 유발할 수 있다.
  • 동적인 값을 디폴트 값으로 사용하려면 None을 설정하고, 함수 내부에서 필요한 값을 할당해야 한다.
  • 독스트링에 None이 사용되는 이유와 동작 방식에 대한 설명을 추가해야 한다.
  • 타입 애너테이션을 사용할 때도 Optional을 활용해 None을 기본값으로 설정하는 방식을 적용할 수 있다.



3-25 위치로만 인자를 지정하게 하거나 키워드로만 인자를 지정하게 해서 함수 호출을 명확하게 만들라.

  • 키워드 인자를 사용하면 함수 호출의 의도를 명확히 할 수 있다.

  • 위치 전용 인자를 사용하면 함수 호출과 구현 간의 결합을 줄일 수 있다.

  • /와 * 기호를 사용하여 위치 전용 인자와 키워드 전용 인자를 지정할 수 있다.


    1. 키워드 전용 인자 (* 사용)

      def process_data(value, *, mode="default"):
          print(f"Processing {value} with mode: {mode}")
    
      # 올바른 호출 방식
      process_data(10, mode="fast")
    
      # 잘못된 호출 방식 (mode를 키워드 없이 위치로 전달)
      process_data(10, "fast")  # TypeError 발생
    
    • mode 인자는 `` 뒤에 위치하므로 반드시 키워드 인자로 전달해야 한다.
    • 함수 호출 시 mode="fast"처럼 명시적으로 키워드를 사용해야 한다.

    2. 위치 전용 인자 (/ 사용)

      def multiply(x, y, /):
          return x * y
    
      # 올바른 호출 방식
      result = multiply(2, 3)
    
      # 잘못된 호출 방식 (키워드를 사용하면 오류 발생)
      result = multiply(x=2, y=3)  # TypeError 발생
    
    • x와 y는 / 앞에 있으므로 위치 인자로만 전달 가능하다.
    • 키워드 인자로 전달하면 TypeError가 발생한다.

    3. 위치 또는 키워드 사용 가능 (/ 와 * 사이)

      def configure(a, b, /, c, d, *, e, f):
          print(a, b, c, d, e, f)
    
      # 올바른 호출 방식
      configure(1, 2, 3, d=4, e=5, f=6)  # 위치 및 키워드 혼용 가능
    
      # 잘못된 호출 방식
      configure(a=1, b=2, c=3, d=4, e=5, f=6)  # TypeError 발생
    
    • a, b는 위치 전용 (/ 앞)
    • c, d는 위치 또는 키워드 사용 가능 (/ 와 * 사이)
    • e, f는 키워드 전용 (* 뒤)

    기억해야 할 내용

    • 키워드 전용 인자 (*뒤에 위치) → 반드시 키워드로 지정해야 한다.
    • 위치 전용 인자 (/ 앞에 위치) → 반드시 위치로만 전달해야 한다.
    • /와 * 사이의 인자는 위치 또는 키워드로 전달할 수 있다.
    • 명확한 함수 호출을 위해 적절히 활용해야 한다.




3-26 functools.wraps을 사용해 함수 데코레이터를 정의하라.

  • 데코레이터는 기존 함수를 수정하지 않고 추가 기능을 덧붙일 수 있는 문법이다.

    • 데코레이터란?

        def 데코레이터_함수(func):
            def wrapper():
                # 함수 실행 전 작업
                func()  # 원래 함수 실행
                # 함수 실행 후 작업
            return wrapper
      
        @데코레이터_함수
        def 일반_함수():
            pass

      실용적인 예제:

        from functools import wraps
        import time
      
        def 시간측정(func):
            @wraps(func)
            def wrapper():
                시작 = time.time()
                func()
                종료 = time.time()
                print(f"실행시간: {종료 - 시작:.2f}초")
            return wrapper
      
        @시간측정
        def 테스트():
            time.sleep(1)  # 1초 대기
            print("작업 완료")
      
        # 함수 실행
        테스트()  # 이 부분이 추가되었습니다

      실행 결과:

        작업 완료
        실행시간: 1.00초
  • wraps을 사용하면 원래 함수의 이름과 독스트링이 유지된다.

    • wraps란?

      wraps란?

      • 파이썬의 데코레이터를 위한 데코레이터

      • 원본 함수의 정보(이름, 설명문 등)를 보존하는 역할

        실제 상황으로 비유

      • 선물 포장 시 "내용물 설명서"를 붙이는 것과 비슷

      • wraps를 사용하지 않으면 포장지의 정보만 보이고, 안에 무엇이 들었는지 알 수 없음.

      • wraps를 사용하면 포장을 했어도 원래 선물의 정보를 그대로 볼 수 있음.

        from functools import wraps
        
        # wraps 미사용
        def 데코레이터1(func):
          def wrapper():
              return func()
          return wrapper
        
        # wraps 사용
        def 데코레이터2(func):
          @wraps(func)
          def wrapper():
              return func()
          return wrapper
        
        @데코레이터1
        def 함수1():
          """첫 번째 함수입니다"""
          pass
        
        @데코레이터2
        def 함수2():
          """두 번째 함수입니다"""
          pass
        
        # 결과 확인
        print(함수1.__name__)  # 출력: wrapper
        print(함수2.__name__)  # 출력: 함수2

        즉, wraps는 데코레이터를 사용할 때 원본 함수의 정체성을 지켜주는 도우미.

  • 데코레이터를 사용하면 디버거 등 인트로스펙션 도구가 잘못 작동할 수 있으므로 functools.wraps를 활용해야 한다.


    1. wraps 없이 데코레이터를 사용한 경우

      import time
    
      def 시간측정(func):
          def wrapper():
              시작 = time.time()
              func()
              종료 = time.time()
              print(f"실행시간: {종료 - 시작:.2f}초")
          return wrapper
    
      @시간측정
      def 테스트():
          """1초 동안 대기하는 테스트 함수"""
          time.sleep(1)
          print("작업 완료")
    
      # 함수 이름과 독스트링 확인
      print(테스트.__name__)  # wrapper (원래 함수명이 유지되지 않음)
      print(테스트.__doc__)   # None (독스트링이 사라짐)
    
    • 테스트.__name__이 wrapper로 변경됨 → 원래 함수 이름이 유지되지 않음.
    • 테스트.__doc__이 None이 됨 → 함수의 설명이 사라짐.

    2. wraps을 사용한 경우

      from functools import wraps
      import time
    
      def 시간측정(func):
          @wraps(func)  # 원래 함수의 메타데이터 유지
          def wrapper():
              시작 = time.time()
              func()
              종료 = time.time()
              print(f"실행시간: {종료 - 시작:.2f}초")
          return wrapper
    
      @시간측정
      def 테스트():
          """1초 동안 대기하는 테스트 함수"""
          time.sleep(1)
          print("작업 완료")
    
      # 함수 실행
      테스트()
    
      # 함수 이름과 독스트링 확인
      print(테스트.__name__)  # 테스트 (원래 함수명이 유지됨)
      print(테스트.__doc__)   # "1초 동안 대기하는 테스트 함수" (독스트링 유지됨)
    

    출력 예시:

      작업 완료
      실행시간: 1.00초
      테스트
      1초 동안 대기하는 테스트 함수
    
    • wraps을 사용하면 원래 함수 이름(테스트)과 독스트링이 유지됨.

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

      def 반복(횟수):
          def decorator(func):
              @wraps(func)
              def wrapper(*args, **kwargs):
                  for _ in range(횟수):
                      func(*args, **kwargs)
              return wrapper
          return decorator
    
      @반복(3)
      def 인사(이름):
          """이름을 입력받아 인사를 출력하는 함수"""
          print(f"안녕하세요, {이름}님!")
    
      인사("훈")  # 3번 반복 출력
    
      print(인사.__name__)  # 인사
      print(인사.__doc__)   # "이름을 입력받아 인사를 출력하는 함수"
    
    • wraps을 사용하지 않으면 인사.__name__이 wrapper로 바뀌고, 독스트링도 사라진다.
    • wraps을 사용하면 원래 함수의 메타데이터를 유지하면서 기능을 확장할 수 있다.

    기억해야 할 내용

    • 데코레이터는 함수 호출 전후로 추가 기능을 실행할 수 있다.
    • 데코레이터를 사용할 경우 원래 함수의 메타데이터(__name__, __doc__)가 변경될 수 있다.
    • 이를 방지하려면 functools.wraps을 사용해야 한다.
    • 매개변수를 받는 데코레이터도 wraps을 적용할 수 있다.



반응형
저작자표시 비영리 변경금지 (새창열림)

'리뷰 > 책' 카테고리의 다른 글

[파이썬 코딩의 기술] - 2장 리스트와 딕셔너리  (0) 2025.02.14
[파이썬 코딩의 기술] - 1장 파이썬답게 생각하기  (0) 2025.02.14
Do it! 첫 코딩 - 5장  (0) 2025.02.14
Do it! 첫 코딩 - 4장  (0) 2025.02.14
Do it! 첫 코딩 - 3장  (0) 2025.02.14
'리뷰/책' 카테고리의 다른 글
  • [파이썬 코딩의 기술] - 2장 리스트와 딕셔너리
  • [파이썬 코딩의 기술] - 1장 파이썬답게 생각하기
  • Do it! 첫 코딩 - 5장
  • Do it! 첫 코딩 - 4장
생각 기록실
생각 기록실
AI(LLM)와 서비스 기획을 공부하며 작성하는 기술 블로그입니다. (feat. 영화리뷰를 곁들인..)
    반응형
  • 생각 기록실
    이러쿵 저러쿵
    생각 기록실
  • 전체
    오늘
    어제
  • 링크

    • Github
    • LindeIn
    • 분류 전체보기 (115)
      • 이러쿵 저러쿵 (5)
      • 정보통계 (7)
        • 데이터마이닝 (2)
        • 금융공학 (4)
      • IT (26)
        • Python (10)
        • AWS (5)
        • Github (2)
        • Project (8)
      • 리뷰 (29)
        • 영화 (22)
        • 책 (7)
      • 기타 (48)
  • hELLO· Designed By정상우.v4.10.3
생각 기록실
[파이썬 코딩의 기술] - 3장 함수
상단으로

티스토리툴바